Compare commits

..
Author SHA1 Message Date
Kacper f787306fce Merge pull request #15457 from svetch/fix/26.3-seventh-republish
Prepare: accept Mojang's seventh 26.3 republish (9f67e10e, index e99f9973)
2026-11-12 04:16:20 +01:00
Kacper d8bf54d1e6 Merge pull request #14494 from vnsmoreira/fix/explosion-creaking-heart-xp
A player's blast awards a natural creaking heart's experience
2026-11-12 04:16:11 +01:00
Kacper f165e7fa29 Merge pull request #15419 from yahya-nusseibeh/fix/launcher-sign-in-bounds
Launcher: bound the sign-in services' interval and lifetimes
2026-11-12 04:16:01 +01:00
Kacper 9d73f6a26a Merge pull request #15151 from MSedgeMC/fix/chiseled-shelf-can-take
fix: a chiseled bookshelf only gives a book to a hopper with room
2026-11-12 04:15:48 +01:00
Kacper 4a60f12112 Merge pull request #13630 from MSedgeMC/fix/goat-long-jump
A goat keeps its long-jump cooldown in its chunk save
2026-11-12 04:09:39 +01:00
Kacper 192be7194f Merge pull request #15120 from MSedgeMC/port_map/farmland-is-near-water
port_map: FarmlandBlock.isNearWater is the water fluid search
2026-11-12 04:01:45 +01:00
Kacper 5f3caf9be4 Merge pull request #13881 from Skr1zzzz/split-renderer
Move building and adopting the renderer out of game.rs into game/render_settings.rs
2026-11-12 04:00:55 +01:00
Kacper 571ef142c5 Merge pull request #15477 from Ragbie/fix/stack-dump-windows-unused
horizon-threads: stack_dump's Unix-only code no longer fails clippy on Windows
2026-11-12 03:48:06 +01:00
Kacper 21845f7949 Merge pull request #14376 from Joansitoh/fix/14371-rod-neighbours
Lightning rod: a strike updates the front block's neighbours without redstone experiments
2026-11-12 03:46:31 +01:00
Kacper 27a4dc65c9 Merge pull request #14365 from oloion/perf/range-dispatch-binary-search
assets: avoid heap allocation in item model range dispatch binary search
2026-11-12 03:46:19 +01:00
Kacper deaab60ba9 Merge pull request #15418 from yahya-nusseibeh/fix/protocol-inflate-growth
Protocol: grow the inflate buffer instead of reserving the declared size
2026-11-12 03:38:58 +01:00
Kacper c2c3cea8e3 Merge pull request #15478 from Bl4ckspell7/refactor/gfx-census-module
Renderer: residency and memory figures move out of gfx.rs into gfx/census.rs
2026-11-12 03:33:14 +01:00
Kacpherek e72b69caca Merge commit '24b778c3b4' into unstick/14494
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-12 02:54:54 +01:00
Kacpherek d9d89df9d8 Merge commit 'aab654d4e8' into unstick/14494
# Conflicts:
#	crates/horizon-client/src/game/explosion.rs
2026-11-12 02:12:20 +01:00
Joansitoh f8e59c3e3b Merge upstream main into fix/14371-rod-neighbours 2026-11-12 01:31:16 +01:00
Kacper 1dcb48e1ec Merge branch 'main' into fix/26.3-seventh-republish 2026-11-12 01:12:39 +01:00
Kacper 80570541d0 Merge branch 'main' into fix/goat-long-jump 2026-11-12 01:11:14 +01:00
Kacper 44699b4c51 Merge branch 'main' into fix/protocol-inflate-growth 2026-11-12 01:05:19 +01:00
Kacper 370854a657 Merge branch 'main' into fix/explosion-creaking-heart-xp 2026-11-12 01:00:16 +01:00
Bl4ckspellandClaude Opus 5.5 83703c28b2 refactor(gfx): move residency and memory figures into gfx/census.rs
gfx.rs is over the 3000-line limit and may not grow, and the 4x terrain MSAA work adds to it. The impl block from resident_sections to cpu_census moves unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 00:57:12 +01:00
Kacper 1b016ba22e Merge branch 'main' into fix/chiseled-shelf-can-take 2026-11-12 00:55:44 +01:00
Kacper 023cef202f Merge branch 'main' into fix/launcher-sign-in-bounds 2026-11-12 00:55:27 +01:00
Kacper fdc97df507 Merge branch 'main' into split-renderer 2026-11-12 00:55:09 +01:00
Kacper fd7c423476 Merge branch 'main' into perf/range-dispatch-binary-search 2026-11-12 00:54:10 +01:00
Kacper 731699144c Merge branch 'main' into port_map/farmland-is-near-water 2026-11-12 00:54:01 +01:00
Evrim CicekandClaude Opus 5.5 1fc67d4d10 stack_dump: the Unix-only code is unused on Windows without failing clippy
after_handler is only called on the signal path (Linux, Android, Apple),
and the tests' busy thread only serves the test that reads another
thread's stack there: on Windows clippy -D warnings stopped in
horizon-threads, and every build warned about after_handler. The function
takes the same cfg_attr(.., allow(dead_code)) as Watched::thread, and the
test helpers and their imports the test's cfg.

Fixes #15476.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 02:45:58 +03:00
Kacper 54ea398de4 Merge branch 'main' into fix/protocol-inflate-growth 2026-11-11 23:57:50 +01:00
Kacpherek 71d150b028 Read the redstone experiments flag from the enabled features in the lightning rod test 2026-11-11 23:54:20 +01:00
Kacper 6c58b77fed Merge branch 'main' into fix/explosion-creaking-heart-xp 2026-11-11 23:52:41 +01:00
Kacpherek 9aae4a6671 Merge commit '3298a6dcd5' into unstick/14376 2026-11-11 23:50:18 +01:00
Kacper 195dceeb94 Merge branch 'main' into fix/chiseled-shelf-can-take 2026-11-11 23:48:07 +01:00
Kacper 8dc176f4e6 Merge branch 'main' into fix/launcher-sign-in-bounds 2026-11-11 23:47:50 +01:00
Kacper 13fe0905ba Merge branch 'main' into split-renderer 2026-11-11 23:47:32 +01:00
Kacper 7d7fa98a44 Merge branch 'main' into perf/range-dispatch-binary-search 2026-11-11 23:46:17 +01:00
Kacper fd2f881a32 Merge branch 'main' into port_map/farmland-is-near-water 2026-11-11 23:46:08 +01:00
Kacper b9abf293aa Merge branch 'main' into fix/14371-rod-neighbours 2026-11-11 23:17:47 +01:00
Román BenjáminandClaude Opus 5.5 f5a3990fbf Prepare: accept Mojang's seventh 26.3 republish (9f67e10e, index e99f9973)
Fixes #15456.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-11 23:12:02 +01:00
Vinícius Moreira 190aa00a63 Merge commit 'c8e1eb66199f696d1a6218fbbaeef1863ce5cc00' into fix/explosion-creaking-heart-xp
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-11 17:57:10 -03:00
Kacper 1d2b14b01d Merge branch 'main' into fix/14371-rod-neighbours 2026-11-11 21:49:49 +01:00
rdbtCVSandGPT 6.1 Sol 1bcc5ec3f9 assets: preserve vanilla range-dispatch binary-search hits
Address kacpherek's request on PR #14365: remove the post-hit tie scan while keeping slice-based search and the short-list upper bound. Extend the existing regression test with 16-entry duplicate and signed-zero lists; expected indices were observed on the JVM and the pinned vanilla method was checked with javap.

Validation: cargo check -p horizon-assets --locked; cargo test -p horizon-assets --locked (53 passed); cargo fmt -p horizon-assets -- --check; cargo clippy -p horizon-assets --all-targets --locked -- -D warnings.

Co-Authored-By: GPT 6.1 Sol <[email protected]>
2026-11-11 20:31:36 +00:00
Kacpherek b61cff5625 Merge main into fix/chiseled-shelf-can-take 2026-11-11 20:44:41 +01:00
Kacpherek 37f6d169d7 Merge main into port_map/farmland-is-near-water 2026-11-11 20:44:26 +01:00
Kacpherek ac4204f395 Merge main into split-renderer 2026-11-11 20:42:09 +01:00
Kacpherek 7714b1d37a Merge main into fix/goat-long-jump 2026-11-11 20:41:46 +01:00
yahya-nusseibehandClaude Opus 5.5 5f61926401 Launcher: bound the sign-in services' interval and lifetimes
The device-code interval, its expiry and the Minecraft token lifetime came from the services
unchecked; Instant + a huge interval panicked the sign-in thread and left the window on the
code. The interval is clamped to 1-60 s (slow-down too), the device code's expiry to an hour
and the token lifetime to a week, all added with saturating_add.

Split out of #15247 at review.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-11 21:31:24 +02:00
yahya-nusseibehandClaude Opus 5.5 98b40e8e5e protocol: grow the inflate buffer instead of reserving the declared size
A compressed frame declares its uncompressed size up front, and the inflater reserved that
much at once: a few bytes declaring 8 MiB made the reader allocate 8 MiB per frame. The
buffer now grows as output arrives (capped by DEFLATE's ~1032:1 ratio), so a lying frame
is refused after a small allocation and a real large body still inflates whole.

Split out of #15249 at review.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-11 21:16:47 +02:00
rdbtCVSandGPT 6.1 Sol 8c893c55f0 Merge main into split-renderer to pick up deterministic CI fixture
Preserve the contributor's renderer extraction unchanged while including main's fix for the Git maintenance race in test_git_slim. The diff against main remains the same four moved methods and port-map citation.

Co-Authored-By: GPT 6.1 Sol <[email protected]>
2026-11-11 11:59:51 +00:00
MSedgeMC 30e4e488b2 port_map: ChiseledBookShelfBlockEntity.canTakeItem asks the target's room
The hopper's path applies the `hasAnyMatching` rule through
`can_take_into`.
2026-11-11 08:49:01 +00:00
MSedgeMC e2ee8ad7d9 test: a full hopper cannot take a book from a chiseled shelf
`a_full_hopper_cannot_take_a_book_from_a_chiseled_shelf` asks the rule
with a hopper full of stone (false) and an empty one (true). Without the
shelf rule both answer true.
2026-11-11 08:40:34 +00:00
MSedgeMC c59a4d6ffd fix: a chiseled bookshelf only gives a book to a hopper with room
`ChiseledBookShelfBlockEntity.canTakeItem` is
`target.hasAnyMatching(other -> other.isEmpty() || (same item and
components && other.count + stack.count <= target.getMaxStackSize(other)))`:
a hopper may take a book only when it has a free slot or a matching stack
with room.

Horizon's `Containers::can_take` had no shelf rule, so a full hopper
still pulled a book out (and put it back). The hopper's path now asks
`can_take_into`, which applies the shelf rule before the existing checks.
2026-11-11 08:40:13 +00:00
MSedgeMC fb05c1acfc port_map: FarmlandBlock.isNearWater is the water fluid search
Vanilla's predicate is `state.getFluidState().is(FluidTags.WATER)`, which
the inline 9x2x9 search already matches; the row's `canBeHydrated` gap
was wrong.
2026-11-11 05:02:45 +00:00
Vinícius Moreira 306dca0a21 Merge remote-tracking branch 'upstream/main' into fix/explosion-creaking-heart-xp 2026-11-11 00:03:46 -03:00
Vinícius MoreiraandClaude Opus 5.5 26d32147ea Wrap the zombie owner's summon
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-10 22:43:53 -03:00
Vinícius MoreiraandClaude Opus 5.5 0b2ed50a68 A nested blast drops the TNT owner; the zombie owner stays and leaves no drops
A native explosion inside a TNT blast is Level.explode(null, ...): it
now takes blast_owner, blast_guest and blast_owner_player aside with
the rest of the blast's state. The group keeps its zombie persistent
and clears the zombie's drops.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-10 22:43:46 -03:00
Vinícius MoreiraandClaude Opus 5.5 386d0560a1 A mob-owned TNT awards no creaking heart experience; block_drops off pops none
PrimedTnt.getOwner names the mob a saved owner UUID points at, so its
blast's indirect source is no player and tryAwardExperience awards
nothing. Keep whether the owner is a player beside blast_owner (a chain
reaction hands a mob owner on as a mob). Block.popExperience draws the
amount, then pops nothing with block_drops off.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-10 22:32:56 -03:00
Vinícius MoreiraandClaude Opus 5.5 4d6923514e A player's blast awards a natural creaking heart's experience
CreakingHeartBlock.onExplosionHit awards the heart's experience
(tryAwardExperience) when the explosion's indirect source is a player, a
primed TNT's owner, unless that player is in creative or spectator
(preventsBlockDrops). Horizon always broke the heart as if no player
caused it.

Part of #11330

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-10 21:59:34 -03:00
Oloion afb34764ec assets: eliminate heap allocations in RangeSelectItemModel binary search 2026-11-10 17:00:17 +03:00
JoansitohandClaude Sonnet 5.5 f23a03f6c7 Lightning rod: update the front block's neighbours without redstone experiments
Fixes #14371

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-09 21:23:34 +01:00
Horizon agentandClaude Opus 5.5 10b318cd6b Merge main into split-renderer
main moved more of game.rs out (#13877, #13878, #13880...): the conflict is only the
neighbours of the moved functions; game.rs takes main and drops the four functions
this branch moves to game/render_settings.rs, unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:08:14 +00:00
Horizon agentandClaude Opus 5.5 3877ae52d8 Merge origin/main into the branch
The verify job compared big files against this branch's old base, so main's own growth of options_ui.rs since then failed the size check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:35:03 +01:00
MSedgeMC 166073d4bc Re-run checks: the previous run failed in an unrelated fixture / harness 2026-11-09 14:52:23 +00:00
Skr1zzzzandClaude Opus 5.5 71e2401edc Move building and adopting the renderer out of game.rs into game/render_settings.rs
Part of #11857. new_renderer, adopt_renderer, forget_atlas_slots and all_sections_changed join game/render_settings.rs. Methods that were private in game.rs become
pub(super), the same reach.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:29:00 +03:00
MSedgeMC 35fc116bd3 fix: drop the goat's old call and pass the memory value to number 2026-11-09 06:41:45 +00:00
MSedgeMC 2a95475e8f refactor: keep the goat's save code in goat.rs
`entity_nbt.rs` is over the 3000-line limit and may not grow (docs/layers.md
#file-size), so the goat's save pair moves into `mobs/goat.rs`: the screaming
and horn fields `Goat.addAdditionalSaveData` writes plus the long jump
cooldown memory, and their read side. `entity_nbt` keeps a one-line call.
2026-11-09 05:52:47 +00:00
MSedgeMC 47894e831f port_map: a goat's long-jump cooldown is saved and read
`Goat.addAdditionalSaveData`'s row in `tools/port_map/game-012.tsv` names the
three own fields; it now also names the `GoatAi` `LONG_JUMP_COOLDOWN_TICKS`
brain memory that the save covers.
2026-11-09 05:18:42 +00:00
MSedgeMC 5e735bd030 test: a goat's long-jump cooldown through the chunk save
`mob_to_nbt`/`mob_from_nbt` are the pair `Game::mob_tree_nbt` and
`Game::load_mob` go through. A goat with 57 ticks of long-jump cooldown saves
`Brain.memories."minecraft:long_jump_cooling_down"` as `{value: 57}` and the
reloaded goat's `frog::jump_cooldown` is 57 again. A goat with no cooldown
writes no memory, even over a compound that still names one.

Without the write the save has no memory (left `None`, right `Some(Compound({"value": Int(57)}))`), and the
reloaded goat has no cooldown (left `None`, right `Some(57)`).
2026-11-09 05:18:42 +00:00
MSedgeMC da4591bc88 fix: a goat keeps its long-jump cooldown in its chunk save
`GoatAi` sets `LONG_JUMP_COOLDOWN_TICKS` when a goat long jumps, and
`CountDownCooldownTicks` counts it down; `Brain.Packed` saves every brain
memory with `LivingEntity.addAdditionalSaveData` and reads it back.

Horizon kept the cooldown in the goat's `extra.memories` (the shared
`frog::jump_cooldown` the long jump goal asks) and wrote no `Brain`, so a goat
that had just long jumped unloaded with its chunk and came back ready to jump
again. The save now writes `minecraft:long_jump_cooling_down` as the integer
memory it is and the load puts it back in the goat's memories.
2026-11-09 05:18:42 +00:00
22 changed files with 669 additions and 261 deletions

No files matched your search

+8 -9
View File
@@ -1764,14 +1764,13 @@ fn stack_from_template(template: &Value) -> Option<StackView> {
/// `RangeSelectItemModel.lastIndexLessOrEqual`.
fn last_index_less_or_equal(entries: &[(f32, ItemModelNode)], needle: f32) -> Option<usize> {
let haystack: Vec<f32> = entries.iter().map(|e| e.0).collect();
let found = if haystack.len() < 16 {
haystack
let found = if entries.len() < 16 {
entries
.iter()
.position(|h| *h > needle)
.map_or(haystack.len() as i64 - 1, |i| i as i64 - 1)
.position(|e| e.0 > needle)
.map_or(entries.len() as i64 - 1, |i| i as i64 - 1)
} else {
match binary_search(&haystack, needle) {
match binary_search(entries, needle) {
Ok(index) => index as i64,
Err(insertion) => insertion as i64 - 1,
}
@@ -1780,8 +1779,8 @@ fn last_index_less_or_equal(entries: &[(f32, ItemModelNode)], needle: f32) -> Op
}
/// `Arrays.binarySearch(float[], float)`, including its handling of equal
/// values (-0.0 below 0.0, NaN above everything).
fn binary_search(a: &[f32], key: f32) -> std::result::Result<usize, usize> {
let (mut low, mut high) = (0i64, a.len() as i64 - 1);
fn binary_search(entries: &[(f32, ItemModelNode)], key: f32) -> std::result::Result<usize, usize> {
let (mut low, mut high) = (0i64, entries.len() as i64 - 1);
let bits = |v: f32| -> i32 {
if v.is_nan() {
0x7fc0_0000
@@ -1791,7 +1790,7 @@ fn binary_search(a: &[f32], key: f32) -> std::result::Result<usize, usize> {
};
while low <= high {
let mid = (low + high) >> 1;
let value = a[mid as usize];
let value = entries[mid as usize].0;
if value < key {
low = mid + 1;
} else if value > key {
@@ -158,3 +158,61 @@ fn a_patched_stack_reads_its_patch_over_its_items_defaults() {
}
assert_eq!(view.to_map(), *merged);
}
#[test]
fn last_index_less_or_equal_linear_and_binary() {
use super::last_index_less_or_equal;
let make = |thresholds: &[f32]| -> Vec<(f32, ItemModelNode)> {
thresholds
.iter()
.map(|&t| (t, ItemModelNode::Empty))
.collect()
};
// Empty
assert_eq!(last_index_less_or_equal(&[], 0.0), None);
// Short (< 16): linear search
let short = make(&[-1.0, 0.0, 1.0, 2.5, 5.0]);
assert_eq!(last_index_less_or_equal(&short, -2.0), None);
assert_eq!(last_index_less_or_equal(&short, -1.0), Some(0));
assert_eq!(last_index_less_or_equal(&short, -0.5), Some(0));
assert_eq!(last_index_less_or_equal(&short, 0.0), Some(1));
assert_eq!(last_index_less_or_equal(&short, 2.5), Some(3));
assert_eq!(last_index_less_or_equal(&short, 3.0), Some(3));
assert_eq!(last_index_less_or_equal(&short, 5.0), Some(4));
assert_eq!(last_index_less_or_equal(&short, 10.0), Some(4));
// Long (>= 16): binary search
let thresholds: Vec<f32> = (0..32).map(|i| i as f32 * 10.0).collect();
let long = make(&thresholds);
assert_eq!(last_index_less_or_equal(&long, -5.0), None);
assert_eq!(last_index_less_or_equal(&long, 0.0), Some(0));
assert_eq!(last_index_less_or_equal(&long, 5.0), Some(0));
assert_eq!(last_index_less_or_equal(&long, 10.0), Some(1));
assert_eq!(last_index_less_or_equal(&long, 150.0), Some(15));
assert_eq!(last_index_less_or_equal(&long, 155.0), Some(15));
assert_eq!(last_index_less_or_equal(&long, 310.0), Some(31));
assert_eq!(last_index_less_or_equal(&long, 1000.0), Some(31));
// Short lists use upper-bound tie behavior, including signed zero.
let duplicates = make(&[1.0, 1.0, 2.0]);
assert_eq!(last_index_less_or_equal(&duplicates, 1.0), Some(1));
let zeroes = make(&[-0.0, 0.0]);
assert_eq!(last_index_less_or_equal(&zeroes, -0.0), Some(1));
assert_eq!(last_index_less_or_equal(&zeroes, 0.0), Some(1));
// At 16 entries, vanilla returns Arrays.binarySearch's hit, not the last tie.
// Expected indices were checked on the JVM.
let duplicates = make(&[
0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 7.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0,
]);
assert_eq!(last_index_less_or_equal(&duplicates, 7.0), Some(7));
let zeroes = make(&[
-8.0, -7.0, -6.0, -5.0, -4.0, -3.0, -2.0, -0.0, -0.0, 0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0,
]);
assert_eq!(last_index_less_or_equal(&zeroes, -0.0), Some(7));
assert_eq!(last_index_less_or_equal(&zeroes, 0.0), Some(9));
}
+1 -154
View File
@@ -192,6 +192,7 @@ fn packed_wanted() -> bool {
pub(crate) mod animate;
mod arena;
pub mod capture;
mod census;
pub mod compositor;
pub(crate) mod core_lines;
pub(crate) mod core_sky;
@@ -2547,160 +2548,6 @@ enum FirstFrame {
fn on_off(on: bool) -> &'static str {
if on { "on" } else { "off" }
}
impl Renderer {
/// Sections with GPU buffers, for soak tests watching residency.
pub fn resident_sections(&self) -> usize {
self.sections.len()
}
/// Quads of the far terrain tiles with GPU buffers.
pub fn lod_quads(&self) -> u64 {
self.lods
.values()
.map(|tile| u64::from(tile.mesh.solid + tile.mesh.translucent))
.sum()
}
/// Tiles and quads of the far terrain tiles with GPU buffers, by level.
pub fn lod_levels(&self) -> Vec<[u64; 2]> {
let mut levels = Vec::new();
for (key, tile) in &self.lods {
let level = usize::from(key.level);
if levels.len() <= level {
levels.resize(level + 1, [0, 0]);
}
levels[level][0] += 1;
levels[level][1] += u64::from(tile.mesh.solid + tile.mesh.translucent);
}
levels
}
/// Whether solid quads go to the arena in the compact layout
/// (`packed_quads`), and whether far tiles' own records are on
/// (`lod::packed::far_packing`).
pub fn packed_layout(&self) -> [bool; 2] {
[self.packed_quads, crate::lod::packed::far_packing()]
}
/// Bytes of the arena the resident chunk sections hold, and of the one far
/// tiles hold: the same split `gpu_census` reports, for `status`, so the
/// compact layouts can be seen working.
pub fn vertex_bytes_split(&self) -> [u64; 2] {
let stride = std::mem::size_of::<TerrainVertex>() as u64;
let used = |s: &SectionGpu| (s.solid_slots + u64::from(s.translucent) * 4) * stride;
[
self.sections.values().map(used).sum(),
self.lods.values().map(|tile| used(&tile.mesh)).sum(),
]
}
/// Bytes of the section vertex arenas' buffers, and of the vertices
/// resident sections hold in them.
pub fn section_vertex_bytes(&self) -> [u64; 2] {
let stride = std::mem::size_of::<TerrainVertex>() as u64;
let vertices: u64 = self
.sections
.values()
.chain(self.lods.values().map(|tile| &*tile.mesh))
.map(|s| s.solid_slots + u64::from(s.translucent) * 4)
.sum();
let translucent = self.translucent_arena.as_ref();
let arenas = self.arena.bytes() + translucent.map_or(0, arena::Arena::bytes);
[arenas, vertices * stride]
}
/// Bytes of the renderer's largest GPU resources, by name (`scene_replay --memory`):
/// the terrain arenas (their pages, and what resident sections and far tiles use of
/// them), the quad indices, the block sprites with their mips, the LabPBR maps, flat
/// draws' buffers and the visibility buffer's.
pub fn gpu_census(&self) -> Vec<(&'static str, u64)> {
let stride = std::mem::size_of::<TerrainVertex>() as u64;
let used = |s: &SectionGpu| (s.solid_slots + u64::from(s.translucent) * 4) * stride;
let textures = |t: &[wgpu::Texture]| t.iter().map(texture_bytes).sum::<u64>();
let translucent = self.translucent_arena.as_ref();
vec![
("section_arena_pages", self.arena.bytes()),
(
"translucent_arena_pages",
translucent.map_or(0, arena::Arena::bytes),
),
("sorted_index_pages", self.sorted_indices.bytes()),
(
"near_section_vertices",
self.sections.values().map(used).sum(),
),
(
"held_section_vertices",
self.hold
.sections()
.filter_map(|held| held.gpu.as_ref())
.map(used)
.sum(),
),
(
"far_tile_vertices",
self.lods.values().map(|tile| used(&tile.mesh)).sum(),
),
("quad_indices", self.indices.size()),
("block_sprites", textures(&self.sprite_textures)),
(
"pbr_maps",
self.pbr_maps
.as_ref()
.map_or(0, |maps| maps.iter().flatten().map(texture_bytes).sum()),
),
(
"flat_draws",
self.flat.as_ref().map_or(0, flat::Flat::bytes),
),
(
"visibility_buffer",
self.visbuf.as_ref().map_or(0, visbuffer::Visbuffer::bytes),
),
]
}
/// Device memory the backend's allocator holds, by the label of what holds
/// it, largest first, with its allocated and reserved totals (Vulkan and
/// DirectX 12; none on Metal and OpenGL) (`horizon-ctl memory`).
pub fn gpu_memory_by_label(&self) -> Option<(u64, u64, Vec<(String, u64)>)> {
let report = self.device.generate_allocator_report()?;
let mut by_label: HashMap<String, u64> = HashMap::new();
for a in report.allocations {
*by_label.entry(a.name).or_default() += a.size;
}
let mut labels: Vec<_> = by_label.into_iter().collect();
labels.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
Some((
report.total_allocated_bytes,
report.total_reserved_bytes,
labels,
))
}
/// Heap bytes of the renderer's per-section bookkeeping (`horizon-ctl memory`):
/// resident sections, far tiles, block entities and light sources by section.
pub fn cpu_census(&self) -> [(&'static str, usize); 4] {
use std::mem::size_of;
let per_section = |map_len: usize, lists: usize| {
map_len * (size_of::<SectionKey>() + size_of::<Vec<([i32; 3], u32)>>() + 1)
+ lists * size_of::<([i32; 3], u32)>()
};
let lists = |map: &crate::fast_hash::FxHashMap<SectionKey, Vec<([i32; 3], u32)>>| {
per_section(
map.capacity(),
map.values().map(Vec::capacity).sum::<usize>(),
)
};
let animated: usize = self
.sections
.values()
.map(|s| s.animated_layers.len() * size_of::<u16>())
.sum();
[
("resident_sections", self.sections.heap_bytes() + animated),
(
"far_tiles",
self.lods.capacity()
* (size_of::<crate::lod::TileKey>() + size_of::<lod::Tile>() + 1),
),
("block_entities", lists(&self.block_entities)),
("light_sources", lists(&self.light_blocks)),
]
}
}
impl Renderer {
pub async fn new(
window: Arc<Window>,
@@ -0,0 +1,164 @@
//! What the renderer holds, for status, soak tests and memory reports (`horizon-ctl memory`,
//! `scene_replay --memory`): resident sections and far tiles, and the bytes of its largest
//! GPU resources and of its per-section bookkeeping.
use std::collections::HashMap;
use super::{
Renderer, SectionGpu, SectionKey, TerrainVertex, arena, flat, lod, texture_bytes, visbuffer,
};
impl Renderer {
/// Sections with GPU buffers, for soak tests watching residency.
pub fn resident_sections(&self) -> usize {
self.sections.len()
}
/// Quads of the far terrain tiles with GPU buffers.
pub fn lod_quads(&self) -> u64 {
self.lods
.values()
.map(|tile| u64::from(tile.mesh.solid + tile.mesh.translucent))
.sum()
}
/// Tiles and quads of the far terrain tiles with GPU buffers, by level.
pub fn lod_levels(&self) -> Vec<[u64; 2]> {
let mut levels = Vec::new();
for (key, tile) in &self.lods {
let level = usize::from(key.level);
if levels.len() <= level {
levels.resize(level + 1, [0, 0]);
}
levels[level][0] += 1;
levels[level][1] += u64::from(tile.mesh.solid + tile.mesh.translucent);
}
levels
}
/// Whether solid quads go to the arena in the compact layout
/// (`packed_quads`), and whether far tiles' own records are on
/// (`lod::packed::far_packing`).
pub fn packed_layout(&self) -> [bool; 2] {
[self.packed_quads, crate::lod::packed::far_packing()]
}
/// Bytes of the arena the resident chunk sections hold, and of the one far
/// tiles hold: the same split `gpu_census` reports, for `status`, so the
/// compact layouts can be seen working.
pub fn vertex_bytes_split(&self) -> [u64; 2] {
let stride = std::mem::size_of::<TerrainVertex>() as u64;
let used = |s: &SectionGpu| (s.solid_slots + u64::from(s.translucent) * 4) * stride;
[
self.sections.values().map(used).sum(),
self.lods.values().map(|tile| used(&tile.mesh)).sum(),
]
}
/// Bytes of the section vertex arenas' buffers, and of the vertices
/// resident sections hold in them.
pub fn section_vertex_bytes(&self) -> [u64; 2] {
let stride = std::mem::size_of::<TerrainVertex>() as u64;
let vertices: u64 = self
.sections
.values()
.chain(self.lods.values().map(|tile| &*tile.mesh))
.map(|s| s.solid_slots + u64::from(s.translucent) * 4)
.sum();
let translucent = self.translucent_arena.as_ref();
let arenas = self.arena.bytes() + translucent.map_or(0, arena::Arena::bytes);
[arenas, vertices * stride]
}
/// Bytes of the renderer's largest GPU resources, by name (`scene_replay --memory`):
/// the terrain arenas (their pages, and what resident sections and far tiles use of
/// them), the quad indices, the block sprites with their mips, the LabPBR maps, flat
/// draws' buffers and the visibility buffer's.
pub fn gpu_census(&self) -> Vec<(&'static str, u64)> {
let stride = std::mem::size_of::<TerrainVertex>() as u64;
let used = |s: &SectionGpu| (s.solid_slots + u64::from(s.translucent) * 4) * stride;
let textures = |t: &[wgpu::Texture]| t.iter().map(texture_bytes).sum::<u64>();
let translucent = self.translucent_arena.as_ref();
vec![
("section_arena_pages", self.arena.bytes()),
(
"translucent_arena_pages",
translucent.map_or(0, arena::Arena::bytes),
),
("sorted_index_pages", self.sorted_indices.bytes()),
(
"near_section_vertices",
self.sections.values().map(used).sum(),
),
(
"held_section_vertices",
self.hold
.sections()
.filter_map(|held| held.gpu.as_ref())
.map(used)
.sum(),
),
(
"far_tile_vertices",
self.lods.values().map(|tile| used(&tile.mesh)).sum(),
),
("quad_indices", self.indices.size()),
("block_sprites", textures(&self.sprite_textures)),
(
"pbr_maps",
self.pbr_maps
.as_ref()
.map_or(0, |maps| maps.iter().flatten().map(texture_bytes).sum()),
),
(
"flat_draws",
self.flat.as_ref().map_or(0, flat::Flat::bytes),
),
(
"visibility_buffer",
self.visbuf.as_ref().map_or(0, visbuffer::Visbuffer::bytes),
),
]
}
/// Device memory the backend's allocator holds, by the label of what holds
/// it, largest first, with its allocated and reserved totals (Vulkan and
/// DirectX 12; none on Metal and OpenGL) (`horizon-ctl memory`).
pub fn gpu_memory_by_label(&self) -> Option<(u64, u64, Vec<(String, u64)>)> {
let report = self.device.generate_allocator_report()?;
let mut by_label: HashMap<String, u64> = HashMap::new();
for a in report.allocations {
*by_label.entry(a.name).or_default() += a.size;
}
let mut labels: Vec<_> = by_label.into_iter().collect();
labels.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
Some((
report.total_allocated_bytes,
report.total_reserved_bytes,
labels,
))
}
/// Heap bytes of the renderer's per-section bookkeeping (`horizon-ctl memory`):
/// resident sections, far tiles, block entities and light sources by section.
pub fn cpu_census(&self) -> [(&'static str, usize); 4] {
use std::mem::size_of;
let per_section = |map_len: usize, lists: usize| {
map_len * (size_of::<SectionKey>() + size_of::<Vec<([i32; 3], u32)>>() + 1)
+ lists * size_of::<([i32; 3], u32)>()
};
let lists = |map: &crate::fast_hash::FxHashMap<SectionKey, Vec<([i32; 3], u32)>>| {
per_section(
map.capacity(),
map.values().map(Vec::capacity).sum::<usize>(),
)
};
let animated: usize = self
.sections
.values()
.map(|s| s.animated_layers.len() * size_of::<u16>())
.sum();
[
("resident_sections", self.sections.heap_bytes() + animated),
(
"far_tiles",
self.lods.capacity()
* (size_of::<crate::lod::TileKey>() + size_of::<lod::Tile>() + 1),
),
("block_entities", lists(&self.block_entities)),
("light_sources", lists(&self.light_blocks)),
]
}
}
+2 -14
View File
@@ -916,15 +916,7 @@ pub fn mob_to_nbt(
Tag::Int(m.extra.more_carrot_ticks),
);
}
"minecraft:goat" => {
map.insert(
"IsScreamingGoat".into(),
Tag::Byte(i8::from(m.extra.screaming)),
);
let [left, right] = m.extra.lost_horns;
map.insert("HasLeftHorn".into(), Tag::Byte(i8::from(!left)));
map.insert("HasRightHorn".into(), Tag::Byte(i8::from(!right)));
}
"minecraft:goat" => crate::mobs::goat::write_nbt(m, map),
"minecraft:creeper" => {
map.insert("Fuse".into(), Tag::Short(m.max_swell as i16));
map.insert(
@@ -2353,11 +2345,7 @@ pub fn mob_from_nbt_with_random(
_ => 0,
};
}
crate::mobs::Species::Goat => {
m.extra.screaming = flag(tag, "IsScreamingGoat");
let horn = |key| number(tag.get(key)).is_some_and(|v| v == 0);
m.extra.lost_horns = [horn("HasLeftHorn"), horn("HasRightHorn")];
}
crate::mobs::Species::Goat => crate::mobs::goat::read_nbt(&mut m, tag),
_ => {}
}
// `Rabbit.readAdditionalSaveData`.
-44
View File
@@ -8321,22 +8321,6 @@ impl Game<'_> {
}
self.tracked_special_box(id as i32)
}
/// The renderer for the window, with the current block sprites, GUI
/// atlas and world border texture.
/// A new renderer's atlas holds only the pack's sprites: what was drawn into it at run time
/// (skins, maps, the themed UI's glyphs and corners) has to be drawn again.
pub(super) fn forget_atlas_slots(&mut self) {
self.skin_slots.forget();
self.maps.slots.forget();
self.trim_slots.forget();
self.map_patches.clear();
self.kept_block_entities.get_mut().clear();
horizon_client::ui_theme::reset_atlas();
}
fn new_renderer(&self, window: Arc<Window>) -> Result<Renderer> {
self.renderer_for(window, &self.pack, &self.gui)
}
/// Rebuild GPU-owned state after the backend reports device loss.
fn recover_device(&mut self) -> Result<()> {
if !self.renderer.as_ref().is_some_and(Renderer::device_lost) {
@@ -8399,34 +8383,6 @@ impl Game<'_> {
log::info!("Horizon: graphics device recovered; rebuilding world meshes");
Ok(())
}
/// A renderer made again from the resources loaded takes over: what named the old one's
/// atlas slots is forgotten, the world is meshed again, and the shader pack and the
/// settings are applied again.
pub(super) fn adopt_renderer(&mut self, mut renderer: Renderer) {
// A new renderer starts with the vanilla column and without the world border's wall.
renderer.clear_sections(self.world.layout());
self.border_renderer.invalidate();
self.renderer = Some(renderer);
self.forget_atlas_slots();
self.clear_lod();
self.pending_uploads.clear();
let context = self.worker_context();
self.streaming.reload_resources(&mut self.world, context);
self.native_shaders = native_shaders::NativeShaders::picking(&std::mem::take(
&mut self.native_shaders.picked,
));
self.apply_ui_settings();
}
/// `LevelExtractor.allChanged`: displayed sections and mesh work are
/// discarded, then every loaded section is rebuilt.
fn all_sections_changed(&mut self) {
self.clear_lod();
self.pending_uploads.clear();
if let Some(renderer) = self.renderer.as_mut() {
renderer.clear_sections(self.world.layout());
}
self.streaming.reload_sections(&mut self.world);
}
/// A translated message for the player the game acts for: `overlay` is
/// `sendOverlayMessage` (the action bar), else `sendSystemMessage` (chat).
/// A guest gets a packet, so its own client translates it.
+22 -10
View File
@@ -247,8 +247,16 @@ impl Game<'_> {
}
/// `CreakingHeartBlock.playerWillDestroy`: the heart's creaking twitches to
/// death and a natural heart drops experience. `onExplosionHit` does the
/// same for the creaking (`by_player` false), without the experience.
/// same for the creaking, and awards the experience only when its
/// blast's indirect source is a player (`creaking_heart_broken_by`).
pub(super) fn creaking_heart_broken(&mut self, pos: [i32; 3], by_player: bool) {
let award = by_player && !self.creative && !self.spectator();
self.creaking_heart_broken_by(pos, award);
}
/// `creaking_heart_broken` once the breaker is known: `award` is
/// `tryAwardExperience`'s player check (a player who neither builds
/// instantly, `preventsBlockDrops`, nor spectates).
pub(super) fn creaking_heart_broken_by(&mut self, pos: [i32; 3], award: bool) {
let key = (pos[0], pos[1], pos[2]);
if !matches!(
self.world.block_entities.get(&key),
@@ -271,16 +279,20 @@ impl Game<'_> {
.state(state)
.and_then(|s| s.properties.get("natural").cloned())
.is_some_and(|v| v == "true");
if by_player && natural && !self.creative && !self.spectator() {
if award && natural {
let xp = 20 + self.world.random.next_int_bound(5).unwrap_or(0) as u32;
self.spawn_orbs(
[
f64::from(pos[0]) + 0.5,
f64::from(pos[1]) + 0.5,
f64::from(pos[2]) + 0.5,
],
xp,
);
// Block.popExperience: the amount is drawn, then nothing pops
// with `block_drops` off.
if self.ui.rules.bool("block_drops") {
self.spawn_orbs(
[
f64::from(pos[0]) + 0.5,
f64::from(pos[1]) + 0.5,
f64::from(pos[2]) + 0.5,
],
xp,
);
}
}
self.world
.block_entities
+11 -3
View File
@@ -1341,10 +1341,18 @@ impl Game<'_> {
}
let name = self.res.facts().get(state).name.clone();
let is_tnt = &*name == "minecraft:tnt";
// CreakingHeartBlock.onExplosionHit: its creaking dies (no
// experience, as Horizon does not track a TNT's owner).
// CreakingHeartBlock.onExplosionHit: its creaking dies, and a blast
// whose indirect source is a player (a primed TNT's owner) awards
// the heart's experience (`tryAwardExperience`).
if &*name == "minecraft:creaking_heart" {
self.creaking_heart_broken(pos, false);
// tryAwardExperience checks the TNT owner's own abilities.
let award = match self.combat.blast_guest {
Some(conn) => self
.command_guest(conn)
.is_some_and(|g| !g.creative() && !g.spectator()),
None => self.combat.blast_owner_player && !self.creative && !self.spectator(),
};
self.creaking_heart_broken_by(pos, award);
}
// Block.onExplosionHit: the drops copy from the block entity, which
// then spills its contents as the block turns to air.
+46 -8
View File
@@ -445,14 +445,14 @@ impl Game<'_> {
// is the same block as before, so its placement does not schedule
// it, and without this it stayed powered for good.
b.schedule_tick(level, ticks, BlockPos::new(p[0], p[1], p[2]), 8);
if level.redstone_experiments {
b.lightning_rod_update_neighbors(
level,
ticks,
BlockPos::new(p[0], p[1], p[2]),
state,
);
}
// `updateNeighbours`: unconditional; the experiments only decide
// whether the update carries an orientation.
b.lightning_rod_update_neighbors(
level,
ticks,
BlockPos::new(p[0], p[1], p[2]),
state,
);
});
}
// Level event 3002 with the rod's axis: sparks along it.
@@ -831,6 +831,44 @@ mod tests {
}
}
/// `LightningRodBlock.onLightningStrike` updates the neighbours of the block
/// in front of the rod whether or not redstone experiments are on (#14371):
/// a wire beside that block is strongly powered by the struck rod.
#[test]
#[ignore = "needs prepared resources (HORIZON_TEST_RESOURCES)"]
fn a_struck_rod_updates_the_wire_beside_the_block_behind_it() {
let mut game = super::super::native_machines::tests::server_game();
let registry = game.pack.assets.registry;
let stone = registry.default_state("minecraft:stone").unwrap();
let wire = registry.default_state("minecraft:redstone_wire").unwrap();
let mut rod = registry
.state(registry.default_state("minecraft:lightning_rod").unwrap())
.cloned()
.unwrap();
rod.properties.insert("facing".into(), "up".into());
let rod = registry.id(&rod).unwrap();
// The rod on stone at (8, 64, 8); wire on stone beside that block.
game.world.set(8, 64, 8, stone).unwrap();
game.world.set(9, 63, 8, stone).unwrap();
game.world.set(9, 64, 8, wire).unwrap();
game.world.set(8, 65, 8, rod).unwrap();
assert!(
!game
.level
.enabled_features
.iter()
.any(|feature| feature == "minecraft:redstone_experiments")
);
game.strike_rod([8.5, 66.0, 8.5]);
let power = registry
.state(game.world.block(9, 64, 8))
.unwrap()
.properties
.get("power")
.cloned();
assert_eq!(power.as_deref(), Some("15"));
}
#[test]
fn viewer_box_reaches_15_around_and_down_and_21_up() {
let bolt = [0.0, 64.0, 0.0];
+86 -1
View File
@@ -489,6 +489,29 @@ impl Containers<'_> {
}
}
/// `canTakeItemThroughFace` (furnaces and brewing stands restrict it).
/// `Container.canTakeItem` for a hopper's slot of `source`: the vanilla
/// shelf rule needs to know where the item is going.
fn can_take_into(
&self,
view: &View,
slot: usize,
stack: &ItemStack,
face: Direction,
target: &View,
) -> bool {
if view.kind == Kind::Bookshelf {
// `ChiseledBookShelfBlockEntity.canTakeItem`: the target must have
// a free slot or a matching stack with room (`hasAnyMatching`).
return (0..target.len()).any(|i| match self.get(target, i) {
None => true,
Some(other) => {
other.same_item_same_components(stack)
&& other.count + stack.count <= self.max_stack(other)
}
});
}
self.can_take(view, slot, stack, face)
}
fn can_take(&self, view: &View, slot: usize, stack: &ItemStack, face: Direction) -> bool {
if let Kind::Composter(level) = view.kind {
// OutputContainer.canTakeItemThroughFace.
@@ -1443,7 +1466,7 @@ impl Game<'_> {
let Some(stack) = c.insertion_item(source, slot).map(Cow::into_owned) else {
continue;
};
if !c.can_take(source, slot, &stack, Direction::Down) {
if !c.can_take_into(source, slot, &stack, Direction::Down, hopper) {
continue;
}
}
@@ -2482,3 +2505,65 @@ mod tests {
#[cfg(test)]
#[path = "machines_chest_cat_tests.rs"]
mod machines_chest_cat_tests;
#[cfg(test)]
mod can_take_tests {
use super::*;
use crate::game::native_machines::tests::server_game;
use horizon_client::containers::ChiseledBookshelf;
/// `ChiseledBookShelfBlockEntity.canTakeItem`: a hopper takes a book only
/// when it has a free slot or a matching stack with room.
#[test]
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
fn a_full_hopper_cannot_take_a_book_from_a_chiseled_shelf() {
let mut game = server_game();
let shelf_pos = (2, 4, 3);
let hopper_pos = (2, 5, 3);
let shelf_state = game
.pack
.assets
.registry
.default_state("minecraft:chiseled_bookshelf")
.unwrap();
let hopper_state = game
.pack
.assets
.registry
.default_state("minecraft:hopper")
.unwrap();
game.world
.set(shelf_pos.0, shelf_pos.1, shelf_pos.2, shelf_state)
.unwrap();
game.world
.set(hopper_pos.0, hopper_pos.1, hopper_pos.2, hopper_state)
.unwrap();
let book = ItemStack::new("minecraft:book", 1);
let mut shelf = ChiseledBookshelf::default();
shelf.items[0] = Some(book.clone());
game.world
.block_entities
.insert(shelf_pos, BlockEntity::ChiseledBookshelf(Box::new(shelf)));
let stone = ItemStack::new("minecraft:stone", 64);
game.world
.block_entities
.insert(hopper_pos, BlockEntity::Chest(vec![Some(stone.clone()); 5]));
let shelf_view = game.containers().view(shelf_pos, false).expect("shelf");
let full_hopper = game.containers().view(hopper_pos, false).expect("hopper");
assert!(
!game
.containers()
.can_take_into(&shelf_view, 0, &book, Direction::Down, &full_hopper),
"a full hopper takes no book"
);
game.world
.block_entities
.insert(hopper_pos, BlockEntity::Chest(vec![None; 5]));
let empty_hopper = game.containers().view(hopper_pos, false).expect("hopper");
assert!(
game.containers()
.can_take_into(&shelf_view, 0, &book, Direction::Down, &empty_hopper),
"an empty hopper takes the book"
);
}
}
@@ -221,6 +221,53 @@ impl Game<'_> {
self.streaming.radius
}
}
/// The renderer for the window, with the current block sprites, GUI
/// atlas and world border texture.
/// A new renderer's atlas holds only the pack's sprites: what was drawn into it at run time
/// (skins, maps, the themed UI's glyphs and corners) has to be drawn again.
pub(super) fn forget_atlas_slots(&mut self) {
self.skin_slots.forget();
self.maps.slots.forget();
self.trim_slots.forget();
self.map_patches.clear();
self.kept_block_entities.get_mut().clear();
horizon_client::ui_theme::reset_atlas();
}
pub(super) fn new_renderer(&self, window: Arc<Window>) -> Result<Renderer> {
self.renderer_for(window, &self.pack, &self.gui)
}
/// A renderer made again from the resources loaded takes over: what named the old one's
/// atlas slots is forgotten, the world is meshed again, and the shader pack and the
/// settings are applied again.
pub(super) fn adopt_renderer(&mut self, mut renderer: Renderer) {
// A new renderer starts with the vanilla column and without the world border's wall.
renderer.clear_sections(self.world.layout());
self.border_renderer.invalidate();
self.renderer = Some(renderer);
self.forget_atlas_slots();
self.clear_lod();
self.pending_uploads.clear();
let context = self.worker_context();
self.streaming.reload_resources(&mut self.world, context);
self.native_shaders = native_shaders::NativeShaders::picking(&std::mem::take(
&mut self.native_shaders.picked,
));
self.apply_ui_settings();
}
/// `LevelExtractor.allChanged`: displayed sections and mesh work are
/// discarded, then every loaded section is rebuilt.
pub(super) fn all_sections_changed(&mut self) {
self.clear_lod();
self.pending_uploads.clear();
if let Some(renderer) = self.renderer.as_mut() {
renderer.clear_sections(self.world.layout());
}
self.streaming.reload_sections(&mut self.world);
}
}
#[cfg(test)]
+2 -2
View File
@@ -627,14 +627,14 @@ pub(super) fn count_down_cooldown(m: &mut Mob) {
}
}
/// `LONG_JUMP_COOLDOWN_TICKS`.
fn jump_cooldown(m: &Mob) -> Option<i64> {
pub(crate) fn jump_cooldown(m: &Mob) -> Option<i64> {
m.extra
.memories
.iter()
.find(|x| x.0 == LONG_JUMP_COOLDOWN)
.map(|x| x.1)
}
fn set_jump_cooldown(m: &mut Mob, ticks: Option<i64>) {
pub(crate) fn set_jump_cooldown(m: &mut Mob, ticks: Option<i64>) {
let memories = &mut m.extra.memories;
let at = memories.iter().position(|x| x.0 == LONG_JUMP_COOLDOWN);
match (at, ticks) {
+109
View File
@@ -555,6 +555,115 @@ fn finish_ram(m: &mut Mob, events: &mut Vec<MobEvent>) {
m.extra.goat.phase = Phase::Done;
}
/// `Goat.addAdditionalSaveData` and the `GoatAi` brain memory Horizon writes
/// with it: the screaming flag, the missing horns and the long jump cooldown
/// (`LONG_JUMP_COOLDOWN_TICKS`, an integer memory in the brain).
pub fn write_nbt(m: &Mob, map: &mut horizon_world::nbt::Compound) {
use horizon_world::nbt::Tag;
map.insert(
"IsScreamingGoat".into(),
Tag::Byte(i8::from(m.extra.screaming)),
);
let [left, right] = m.extra.lost_horns;
map.insert("HasLeftHorn".into(), Tag::Byte(i8::from(!left)));
map.insert("HasRightHorn".into(), Tag::Byte(i8::from(!right)));
let brain = map
.entry("Brain".into())
.or_insert_with(|| Tag::Compound(Default::default()));
if !matches!(brain, Tag::Compound(_)) {
*brain = Tag::Compound(Default::default());
}
let Tag::Compound(brain) = brain else { return };
let memories = brain
.entry("memories".into())
.or_insert_with(|| Tag::Compound(Default::default()));
if !matches!(memories, Tag::Compound(_)) {
*memories = Tag::Compound(Default::default());
}
let Tag::Compound(memories) = memories else {
return;
};
match crate::mobs::frog::jump_cooldown(m) {
Some(ticks) => {
memories.insert(
"minecraft:long_jump_cooling_down".into(),
Tag::compound([("value", Tag::Int(ticks as i32))]),
);
}
None => {
memories.remove(&"minecraft:long_jump_cooling_down".into());
}
}
}
/// `Goat.readAdditionalSaveData` and `Brain.Packed`'s memory: the three own
/// fields and the ticks left of the long jump cooldown.
pub fn read_nbt(m: &mut Mob, tag: &horizon_world::nbt::Tag) {
m.extra.screaming = crate::entity_nbt::flag(tag, "IsScreamingGoat");
let horn = |key| crate::save::number(tag.get(key)).is_some_and(|v| v == 0);
m.extra.lost_horns = [horn("HasLeftHorn"), horn("HasRightHorn")];
let cooldown = tag
.get("Brain")
.and_then(|b| b.get("memories"))
.and_then(|b| b.get("minecraft:long_jump_cooling_down"))
.and_then(|m| crate::save::number(m.get("value")));
crate::mobs::frog::set_jump_cooldown(m, cooldown);
}
#[cfg(test)]
mod long_jump_save_tests {
use super::*;
use crate::entity_nbt::{mob_from_nbt, mob_to_nbt};
use horizon_world::nbt::Tag;
use std::sync::Arc;
fn goat() -> Mob {
let mut kind = crate::mobs::sensor_scan_tests::kind();
let k = Arc::make_mut(&mut kind);
k.id = "minecraft:goat".into();
k.species = crate::mobs::Species::Goat;
Mob::new(7, kind, Vec3::new(0.5, 0.0, 0.5), 0.0)
}
/// `GoatAi`'s `LONG_JUMP_COOLDOWN_TICKS` is a brain memory, and
/// `Brain.Packed` saves every memory: `mob_to_nbt`/`mob_from_nbt` are the
/// pair `Game::mob_tree_nbt` and `Game::load_mob` go through. Horizon
/// counted the cooldown in the goat's memories and wrote no `Brain`, so a
/// goat that had just long jumped came back ready to jump again.
#[test]
fn a_goat_keeps_its_long_jump_cooldown_through_a_save() {
let mut m = goat();
assert_eq!(crate::mobs::frog::jump_cooldown(&m), None);
crate::mobs::frog::set_jump_cooldown(&mut m, Some(57));
let saved = mob_to_nbt(&m, [1, 2, 3, 4], None, None, |_| None);
assert_eq!(
saved
.get("Brain")
.and_then(|b| b.get("memories"))
.and_then(|b| b.get("minecraft:long_jump_cooling_down")),
Some(&Tag::compound([("value", Tag::Int(57))])),
"the save names the ticks left"
);
let types = std::collections::HashMap::from([(m.kind.id.clone(), m.kind.clone())]);
let back = mob_from_nbt(&saved, &types, 2, None).unwrap();
assert_eq!(
crate::mobs::frog::jump_cooldown(&back),
Some(57),
"the reloaded cooldown"
);
// No cooldown writes no memory, even over a compound that names one.
let saved = mob_to_nbt(&goat(), [1, 2, 3, 4], Some(&saved), None, |_| None);
assert_eq!(
saved
.get("Brain")
.and_then(|b| b.get("memories"))
.and_then(|b| b.get("minecraft:long_jump_cooling_down")),
None,
"no cooldown, no key"
);
}
}
#[cfg(test)]
mod sound_tests {
use super::*;
+48 -6
View File
@@ -123,12 +123,32 @@ impl Inflater {
/// The zlib stream at the front of `input`, which must hold exactly `size` bytes.
fn inflate(&mut self, input: &[u8], size: usize) -> Result<Vec<u8>> {
self.0.reset(true);
// One byte more than declared: a longer stream fills it.
let mut out = Vec::with_capacity(size + 1);
let status = self
.0
.decompress_vec(input, &mut out, FlushDecompress::Finish)
.map_err(|e| Error::new(format!("Badly compressed packet - {e}")))?;
// One byte more than declared: a longer stream fills it. Reserved as the output grows, not
// all at once: a few bytes declaring 8 MiB would otherwise allocate 8 MiB per frame.
// DEFLATE expands at most about 1032:1.
let limit = size + 1;
let mut out =
Vec::with_capacity(limit.min(input.len().saturating_mul(1032).saturating_add(64)));
let status = loop {
let consumed = usize::try_from(self.0.total_in()).unwrap_or(input.len());
let status = self
.0
.decompress_vec(
input.get(consumed..).unwrap_or_default(),
&mut out,
FlushDecompress::Finish,
)
.map_err(|e| Error::new(format!("Badly compressed packet - {e}")))?;
// Done, past the limit, or stopped with room left (the input ran out).
if matches!(status, Status::StreamEnd)
|| out.len() >= limit
|| out.len() < out.capacity()
{
break status;
}
let more = out.len().max(4096).min(limit - out.len());
out.reserve_exact(more);
};
if out.len() != size || !matches!(status, Status::StreamEnd) {
return Err(Error::new(format!(
"Badly compressed packet - actual length {} does not match declared size {size}",
@@ -228,6 +248,28 @@ mod tests {
}
}
#[test]
fn large_bodies_inflate_whole_and_short_streams_are_refused() {
// 2 MiB of zeros deflate to a few KiB: inflated whole as the buffer grows.
let body = vec![0u8; 2 << 20];
let frame = contents(&body, Some(256)).unwrap();
let mut inflater = Inflater::default();
let Body::Inflated(out) = super::body(&frame, Some(256), &mut inflater).unwrap() else {
panic!("compressed");
};
assert!(out == body);
// A few bytes declaring 8 MiB: refused (and no longer 8 MiB reserved for them).
let mut lying = Vec::new();
write_varint(&mut lying, MAX_UNCOMPRESSED as i32);
lying.extend_from_slice(&frame[3..20]);
assert!(super::body(&lying, Some(256), &mut inflater).is_err());
// The same inflater still reads a good frame after.
assert!(matches!(
super::body(&frame, Some(256), &mut inflater).unwrap(),
Body::Inflated(_)
));
}
/// Split one complete frame off the front of `buffer`, if available.
fn split(buffer: &mut Vec<u8>) -> Result<Option<Vec<u8>>> {
let Some((start, len)) = header(buffer)? else {
+9
View File
@@ -67,6 +67,10 @@ impl Watched {
/// signal trampoline's, the one frame the system puts between the handler and the interrupted
/// code (named `__restore_rt`, `_sigtramp`, or after whatever symbol precedes it). Unchanged when
/// the handler's frame isn't found.
#[cfg_attr(
not(any(target_vendor = "apple", target_os = "linux", target_os = "android")),
allow(dead_code)
)]
fn after_handler(text: &str) -> String {
fn frame_name(line: &str) -> Option<&str> {
let (n, name) = line.trim().split_once(": ")?;
@@ -157,12 +161,16 @@ mod imp {
#[cfg(test)]
mod tests {
use super::*;
// The busy thread below serves the test that reads another thread's stack, which only the
// systems with a signal path run.
#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
mpsc,
};
#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
#[inline(never)]
fn spin(mut n: u64) -> u64 {
for _ in 0..1_000_000 {
@@ -173,6 +181,7 @@ mod tests {
/// Its time goes to a call: Apple's arm64 unwinder loses the frame the signal interrupted
/// (it resumes from the return address), while every caller's frame stays.
#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))]
#[inline(never)]
fn busy_in_a_named_frame(stop: &AtomicBool) {
let mut n = 0u64;
+10 -4
View File
@@ -184,8 +184,14 @@ pub fn device_code(ep: &Endpoints, client_id: &str) -> Result<DeviceCode, String
.get("interval")
.and_then(Value::as_u64)
.unwrap_or(5)
.max(1),
expires_at: now() + v.get("expires_in").and_then(Value::as_u64).unwrap_or(900),
.clamp(1, 60),
// The service's numbers, bounded: Instant + a huge interval panics the sign-in thread.
expires_at: now().saturating_add(
v.get("expires_in")
.and_then(Value::as_u64)
.unwrap_or(900)
.min(3600),
),
device_code,
}),
_ => Err(oauth_error(&v)),
@@ -259,7 +265,7 @@ pub fn wait_for_sign_in(
}
match poll(ep, client_id, code)? {
Poll::Pending => {}
Poll::SlowDown => interval += 5,
Poll::SlowDown => interval = (interval + 5).min(60),
Poll::Done(tokens) => return Ok(tokens),
}
}
@@ -345,7 +351,7 @@ fn minecraft_login(
let (token, expires_in) = minecraft_token(ep, &format!("XBL3.0 x={uhs};{xsts_token}"))?;
Ok(Minecraft {
token,
expires_at: now() + expires_in,
expires_at: now().saturating_add(expires_in.min(7 * 86_400)),
xuid,
})
}
+4 -2
View File
@@ -7,7 +7,8 @@
"702fe59163c6ee6578607daa85811d9bc9c7cc40",
"61fff1316bd50312624a858bbb5eb18fa45208ed",
"5df1795e16eea19d75b923d66ecca9ce183353c5",
"80608ff6753705b6a82e2cb86a5937d4d233abd4"
"80608ff6753705b6a82e2cb86a5937d4d233abd4",
"9f67e10ed013deeaad20ac3c800652765d493408"
],
"accepted_manifest_note": "Other 26.3 version JSONs Mojang republished: only assetIndex differs (non-English translations), jars and libraries are identical. A launcher may still hold any of them.",
"accepted_asset_index_sha1": [
@@ -15,7 +16,8 @@
"1e4e4a68dece1c0f51b8f93b45da8fe8da51bcd3",
"0c8f43b28f837869c1e54a6aeb22c396a1548946",
"917ff6d74b9f635c46bac0d28857206bbcc1ffe4",
"331e5c80937c9cd79647ab34bdc1cbb5ef09f456"
"331e5c80937c9cd79647ab34bdc1cbb5ef09f456",
"e99f9973047785b5a9d52eaf72020b97545053c9"
],
"accepted_asset_index_note": "The asset indexes (id 34) of those republished version JSONs: a launcher keeps one 34.json, the last it downloaded, whichever version JSON it reads.",
"client_sha1": "e877b6a07acd633fb3bb475002175cec036e7b87",
+38
View File
@@ -7866,6 +7866,43 @@ def explosion_player_experience():
f'/forceload remove {x0} {z0}')
def explosion_creaking_heart_experience():
"""#11330: `CreakingHeartBlock.onExplosionHit` awards a natural heart's experience (`tryAwardExperience`)
when the blast's indirect source is a player (a primed TNT's `owner`); an ownerless blast awards none. The
amount is random (20 to 24), so only whether orbs came out is compared. A creative owner gets none
(`preventsBlockDrops`), so the player is in survival for the owned blasts, then back in creative. A TNT
whose `owner` is a zombie's UUID is no player's (none), and with `block_drops` off `popExperience` drops
nothing."""
# Entities tick once the forced chunk's 2-column neighbourhood is loaded (ChunkMap.prepareEntityTickingChunk).
x0, y0, z0 = 4096 - 416, 260, 3584
ops = [cmd(f'/forceload add {x0} {z0}', False), {'op': 'wait_chunks',
'chunks': [[(x0 >> 4) + i, (z0 >> 4) + k] for i in range(-2, 3) for k in range(-2, 3)]},
cmd('/tick freeze', False)]
near = f'x={x0},y={y0 - 4},z={z0},dx=16,dy=16,dz=16'
for owner, mode, drops in ((None, 'survival', True), ('@p', 'creative', True), ('@p', 'survival', True),
('zombie', 'survival', True), ('@p', 'survival', False)):
ops += [cmd(f'/fill {x0} {y0 - 1} {z0} {x0 + 15} {y0 + 6} {z0 + 15} air', False),
cmd(f'/fill {x0} {y0 - 1} {z0} {x0 + 15} {y0 - 1} {z0 + 15} bedrock', False),
cmd(f'/setblock {x0 + 8} {y0} {z0 + 8} creaking_heart[natural=true]', expect_result=1),
cmd(f'/summon tnt {x0 + 8}.5 {y0 + 1} {z0 + 8}.5 {{fuse:1,Tags:["heart_tnt"]}}', expect_result=1)]
if owner == 'zombie':
ops.append(cmd(f'/summon zombie {x0 + 1}.5 {y0} {z0 + 1}.5 '
f'{{NoAI:1b,Silent:1b,PersistenceRequired:1b,Tags:["heart_owner"]}}',
expect_result=1))
owner = f'@e[type=zombie,tag=heart_owner,{near},limit=1]'
if owner:
ops.append(cmd(f'/data modify entity @e[type=tnt,tag=heart_tnt,limit=1] owner set from entity {owner} UUID'))
ops.append(cmd(f'/gamemode {mode} @p', False))
ops.append(cmd(f'/gamerule block_drops {str(drops).lower()}', False))
ops += [cmd('/tick unfreeze', False), {'op': 'tick', 'n': 2}, cmd('/tick freeze', False),
cmd(f'/execute if block {x0 + 8} {y0} {z0 + 8} air', expect_result=1),
cmd(f'/execute if entity @e[type=experience_orb,{near}]', comparison='positive_result'),
cmd(f'/kill @e[type=!player,{near}]', False), cmd(f'/kill @e[type=item,{near}]', False)]
return ops + setup('/gamemode creative @p', '/gamerule block_drops true', '/tick unfreeze',
f'/fill {x0} {y0 - 1} {z0} {x0 + 15} {y0 + 6} {z0 + 15} air',
f'/forceload remove {x0} {z0}')
def groups():
try:
from .spectral_duration_corpus import cases as spectral_saved_duration
@@ -8064,6 +8101,7 @@ def groups():
('dispensed item escape', dispensed_item_escape()),
('command block last output', command_block_last_output()),
('explosion player experience', explosion_player_experience()),
('explosion creaking heart experience', explosion_creaking_heart_experience()),
('comparator setblock output', comparator_setblock_output()),
('glide fall accumulation', glide_fall_accumulation()),
('living short timers', living_short_timers()),
+1 -1
View File
@@ -85,7 +85,7 @@ LevelExtractor.extractBlockDestroyAnimation port crates/horizon-client/src/game/
LevelExtractor.extractGizmos port crates/horizon-client/src/game/debug_keys.rs::Game::debug_gizmos the frame's debug gizmos
LevelExtractor.extractPlayerState inline crates/horizon-client/src/game.rs::Game::render player and hands read from the game state at draw time
LevelExtractor.applyFrustum port crates/horizon-client-render/src/gfx/resident.rs::Resident::visible the frustum decides which sections are visible
LevelExtractor.setLevel inline crates/horizon-client/src/game.rs::Game::all_sections_changed a new level rebuilds every loaded section (allChanged); the null-level branch is not separate
LevelExtractor.setLevel inline crates/horizon-client/src/game/render_settings.rs::Game::all_sections_changed a new level rebuilds every loaded section (allChanged); the null-level branch is not separate
LevelExtractor.blockChanged port crates/horizon-client-render/src/world.rs::World::set a block edit marks its 3x3x3 neighbourhood; gap: minor: the player-changed flag (update bit 8) is not kept
LevelExtractor.setBlockDirty port crates/horizon-client-render/src/world.rs::World::set marks the 3x3x3 section neighbourhood of an edited block
LevelExtractor.setBlocksDirty inline crates/horizon-client-render/src/world.rs::World::set each block of the range goes through the same 3x3x3 marking
1 SectionCopy.getBlockState inline crates/horizon-client-render/src/mesher.rs::Padded::new the copy is Padded's block array; no debug-world barrier or DebugLevelSource blocks
85 LevelExtractor.extractGizmos port crates/horizon-client/src/game/debug_keys.rs::Game::debug_gizmos the frame's debug gizmos
86 LevelExtractor.extractPlayerState inline crates/horizon-client/src/game.rs::Game::render player and hands read from the game state at draw time
87 LevelExtractor.applyFrustum port crates/horizon-client-render/src/gfx/resident.rs::Resident::visible the frustum decides which sections are visible
88 LevelExtractor.setLevel inline crates/horizon-client/src/game.rs::Game::all_sections_changed crates/horizon-client/src/game/render_settings.rs::Game::all_sections_changed a new level rebuilds every loaded section (allChanged); the null-level branch is not separate
89 LevelExtractor.blockChanged port crates/horizon-client-render/src/world.rs::World::set a block edit marks its 3x3x3 neighbourhood; gap: minor: the player-changed flag (update bit 8) is not kept
90 LevelExtractor.setBlockDirty port crates/horizon-client-render/src/world.rs::World::set marks the 3x3x3 section neighbourhood of an edited block
91 LevelExtractor.setBlocksDirty inline crates/horizon-client-render/src/world.rs::World::set each block of the range goes through the same 3x3x3 marking
+1 -1
View File
@@ -82,7 +82,7 @@ Goat.getHurtSound port crates/horizon-client/src/mobs/ai.rs::voice_sounds scream
Goat.getDeathSound port crates/horizon-client/src/mobs/ai.rs::voice_sounds screaming death event selected by IsScreamingGoat
Goat.finalizeSpawn port crates/horizon-client/src/mobs/finalize.rs::goat_spawn called for a live spawn by spawning.rs::finalize; Ctx::finalize has its own arm with the same draws for a structure mob: the ram and long jump cooldowns, screaming when nextDouble is below 0.02, ageBoundaryReached, and for an adult a lost horn one time in ten (nextFloat below 0.1, the side by nextBoolean)
Goat.getDefaultDimensions port crates/horizon-client/src/mobs/living.rs::Mob::dims baby box (0.45 x 0.65, eye 0.59375) from the species table; no long jump pose scale since there is no long jump
Goat.addAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::mob_to_nbt IsScreamingGoat, HasLeftHorn, HasRightHorn
Goat.addAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::mob_to_nbt IsScreamingGoat, HasLeftHorn, HasRightHorn; the GoatAi LONG_JUMP_COOLDOWN_TICKS brain memory is saved too
Goat.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::mob_from_nbt IsScreamingGoat and both horn flags (default true)
Goat.defineSynchedData port crates/horizon-client/src/game/lan.rs::mob_meta gap: minor: the two horn flags are synced; the screaming flag is not
Goat.checkGoatSpawnRules port crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules goats_spawnable_on below and bright enough
1 Fox.defineSynchedData port crates/horizon-client/src/game/lan.rs::mob_meta gap: minor: the type and the flags byte are synced; the two trusted-entity slots are not
82 Goat.getDeathSound port crates/horizon-client/src/mobs/ai.rs::voice_sounds screaming death event selected by IsScreamingGoat
83 Goat.finalizeSpawn port crates/horizon-client/src/mobs/finalize.rs::goat_spawn called for a live spawn by spawning.rs::finalize; Ctx::finalize has its own arm with the same draws for a structure mob: the ram and long jump cooldowns, screaming when nextDouble is below 0.02, ageBoundaryReached, and for an adult a lost horn one time in ten (nextFloat below 0.1, the side by nextBoolean)
84 Goat.getDefaultDimensions port crates/horizon-client/src/mobs/living.rs::Mob::dims baby box (0.45 x 0.65, eye 0.59375) from the species table; no long jump pose scale since there is no long jump
85 Goat.addAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::mob_to_nbt IsScreamingGoat, HasLeftHorn, HasRightHorn IsScreamingGoat, HasLeftHorn, HasRightHorn; the GoatAi LONG_JUMP_COOLDOWN_TICKS brain memory is saved too
86 Goat.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::mob_from_nbt IsScreamingGoat and both horn flags (default true)
87 Goat.defineSynchedData port crates/horizon-client/src/game/lan.rs::mob_meta gap: minor: the two horn flags are synced; the screaming flag is not
88 Goat.checkGoatSpawnRules port crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules goats_spawnable_on below and bright enough
+1 -1
View File
@@ -108,7 +108,7 @@ FarmlandBlock.getStateForPlacement port crates/horizon-world/src/level/place.rs:
FarmlandBlock.tick port crates/horizon-world/src/level/tick.rs::Sim::tick_block Farmland arm turns the block to its base when it cannot survive
FarmlandBlock.randomTick port crates/horizon-world/src/level/tick.rs::Sim::farmland_random_tick moisture falls or rises with water in reach and rain above, else turns to base
FarmlandBlock.turnToBaseBlock port crates/horizon-world/src/level/tick.rs::Sim::turn_to_base_block the base block is set and Block.pushEntitiesUp goes out as the push_entities_up block event
FarmlandBlock.isNearWater inline crates/horizon-world/src/level/tick.rs::Sim::farmland_random_tick gap: minor: the 9x2x9 water search is inline and tests the water fluid only, not canBeHydrated
FarmlandBlock.isNearWater inline crates/horizon-world/src/level/tick.rs::Sim::farmland_random_tick the 9x2x9 inline search matches vanilla's predicate, `state.getFluidState().is(FluidTags.WATER)` (sources and flowing water alike), not a block tag
FenceBlock.isSameFence inline crates/horizon-world/src/level/shapes.rs::Sim::fence_connects_to the same-fence test (fence tag and wooden match) is inline in fence_connects_to
FenceBlock.getStateForPlacement port crates/horizon-world/src/level/place.rs::Sim::placement_class Fence arm: each horizontal side connects per neighbour and waterlogged from the spot
FenceBlock.updateShape port crates/horizon-world/src/level/shapes.rs::Sim::update_shape_class Fence arm: waterlogged tick and the horizontal side connection update
1 ConcretePowderBlock.onLand port crates/horizon-world/src/level/falling.rs::Behaviours::falling_block_landed landing branch turns the powder to concrete when the spot is water or touches liquid
108 FarmlandBlock.tick port crates/horizon-world/src/level/tick.rs::Sim::tick_block Farmland arm turns the block to its base when it cannot survive
109 FarmlandBlock.randomTick port crates/horizon-world/src/level/tick.rs::Sim::farmland_random_tick moisture falls or rises with water in reach and rain above, else turns to base
110 FarmlandBlock.turnToBaseBlock port crates/horizon-world/src/level/tick.rs::Sim::turn_to_base_block the base block is set and Block.pushEntitiesUp goes out as the push_entities_up block event
111 FarmlandBlock.isNearWater inline crates/horizon-world/src/level/tick.rs::Sim::farmland_random_tick gap: minor: the 9x2x9 water search is inline and tests the water fluid only, not canBeHydrated the 9x2x9 inline search matches vanilla's predicate, `state.getFluidState().is(FluidTags.WATER)` (sources and flowing water alike), not a block tag
112 FenceBlock.isSameFence inline crates/horizon-world/src/level/shapes.rs::Sim::fence_connects_to the same-fence test (fence tag and wooden match) is inline in fence_connects_to
113 FenceBlock.getStateForPlacement port crates/horizon-world/src/level/place.rs::Sim::placement_class Fence arm: each horizontal side connects per neighbour and waterlogged from the spot
114 FenceBlock.updateShape port crates/horizon-world/src/level/shapes.rs::Sim::update_shape_class Fence arm: waterlogged tick and the horizontal side connection update
+1 -1
View File
@@ -32,7 +32,7 @@ ChestBlockEntity.recheckOpen port crates/horizon-world/src/level/tick.rs::Sim::t
ChestLidController.tickLid port crates/horizon-client-render/src/block_entity_anim.rs::ChestLid::tick lid openness step of 0.1 per tick
ChiseledBookShelfBlockEntity.removeItem port crates/horizon-client/src/game/machines.rs::Game::take_one removes one item from the slot
ChiseledBookShelfBlockEntity.setItem port crates/horizon-client/src/game/chiseled_bookshelf.rs::Game::use_chiseled_bookshelf player insert path; slot update via chiseled_bookshelf_update_state
ChiseledBookShelfBlockEntity.canTakeItem port crates/horizon-client/src/game/machines.rs::Containers::can_take gap: minor: the target-has-room check is left to take_one and put_back
ChiseledBookShelfBlockEntity.canTakeItem port crates/horizon-client/src/game/machines.rs::Containers::can_take the target's room: a free slot or a matching stack with room (`hasAnyMatching`); the hopper asks `can_take_into`, which applies it before `can_take`
ChiseledBookShelfBlockEntity.applyImplicitComponents port crates/horizon-client/src/block_entity_item.rs::place Items as container component
ChiseledBookShelfBlockEntity.collectImplicitComponents port crates/horizon-client/src/block_entity_item.rs::collect Items as container component
CommandBlockEntity.onModeSwitch port crates/horizon-client/src/game/command_blocks.rs::Game::apply_command_block mode switch; the AUTO schedule follows the setAutomatic step
1 BellBlockEntity.serverTick port crates/horizon-client/src/game/block_entity_anims.rs::Game::tick_bell_servers world-owned ringing bells tick without a renderer; clients animate their separate copy
32 ChestLidController.tickLid port crates/horizon-client-render/src/block_entity_anim.rs::ChestLid::tick lid openness step of 0.1 per tick
33 ChiseledBookShelfBlockEntity.removeItem port crates/horizon-client/src/game/machines.rs::Game::take_one removes one item from the slot
34 ChiseledBookShelfBlockEntity.setItem port crates/horizon-client/src/game/chiseled_bookshelf.rs::Game::use_chiseled_bookshelf player insert path; slot update via chiseled_bookshelf_update_state
35 ChiseledBookShelfBlockEntity.canTakeItem port crates/horizon-client/src/game/machines.rs::Containers::can_take gap: minor: the target-has-room check is left to take_one and put_back the target's room: a free slot or a matching stack with room (`hasAnyMatching`); the hopper asks `can_take_into`, which applies it before `can_take`
36 ChiseledBookShelfBlockEntity.applyImplicitComponents port crates/horizon-client/src/block_entity_item.rs::place Items as container component
37 ChiseledBookShelfBlockEntity.collectImplicitComponents port crates/horizon-client/src/block_entity_item.rs::collect Items as container component
38 CommandBlockEntity.onModeSwitch port crates/horizon-client/src/game/command_blocks.rs::Game::apply_command_block mode switch; the AUTO schedule follows the setAutomatic step