mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-11 17:19:31 -10:00
Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f787306fce | ||
|
|
d8bf54d1e6 | ||
|
|
f165e7fa29 | ||
|
|
9d73f6a26a | ||
|
|
4a60f12112 | ||
|
|
192be7194f | ||
|
|
5f3caf9be4 | ||
|
|
571ef142c5 | ||
|
|
21845f7949 | ||
|
|
27a4dc65c9 | ||
|
|
deaab60ba9 | ||
|
|
c2c3cea8e3 | ||
|
|
e72b69caca | ||
|
|
d9d89df9d8 | ||
|
|
f8e59c3e3b | ||
|
|
1dcb48e1ec | ||
|
|
80570541d0 | ||
|
|
44699b4c51 | ||
|
|
370854a657 | ||
|
|
83703c28b2 | ||
|
|
1b016ba22e | ||
|
|
023cef202f | ||
|
|
fdc97df507 | ||
|
|
fd7c423476 | ||
|
|
731699144c | ||
|
|
1fc67d4d10 | ||
|
|
54ea398de4 | ||
|
|
71d150b028 | ||
|
|
6c58b77fed | ||
|
|
9aae4a6671 | ||
|
|
195dceeb94 | ||
|
|
8dc176f4e6 | ||
|
|
13fe0905ba | ||
|
|
7d7fa98a44 | ||
|
|
fd2f881a32 | ||
|
|
b9abf293aa | ||
|
|
f5a3990fbf | ||
|
|
190aa00a63 | ||
|
|
1d2b14b01d | ||
|
|
1bcc5ec3f9 | ||
|
|
b61cff5625 | ||
|
|
37f6d169d7 | ||
|
|
ac4204f395 | ||
|
|
7714b1d37a | ||
|
|
5f61926401 | ||
|
|
98b40e8e5e | ||
|
|
8c893c55f0 | ||
|
|
30e4e488b2 | ||
|
|
e2ee8ad7d9 | ||
|
|
c59a4d6ffd | ||
|
|
fb05c1acfc | ||
|
|
306dca0a21 | ||
|
|
26d32147ea | ||
|
|
0b2ed50a68 | ||
|
|
386d0560a1 | ||
|
|
4d6923514e | ||
|
|
afb34764ec | ||
|
|
f23a03f6c7 | ||
|
|
10b318cd6b | ||
|
|
3877ae52d8 | ||
|
|
166073d4bc | ||
|
|
71e2401edc | ||
|
|
35fc116bd3 | ||
|
|
2a95475e8f | ||
|
|
47894e831f | ||
|
|
5e735bd030 | ||
|
|
da4591bc88 |
No files matched your search
@@ -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));
|
||||
}
|
||||
@@ -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)),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
Reference in new issue
Block a user