1use std::{
3 fmt::Debug,
4 hash::Hash,
5 io::{Result, Write},
6 mem, slice,
7};
8
9use steel_registry::blocks::block_state_ext::BlockStateExt;
10use steel_utils::{BlockStateId, codec::VarInt, serial::WriteTo};
11
12pub trait ToGlobalId {
14 fn to_global_id(&self) -> u32;
16}
17
18impl ToGlobalId for BlockStateId {
19 fn to_global_id(&self) -> u32 {
20 u32::from(self.0)
21 }
22}
23
24impl ToGlobalId for u16 {
25 fn to_global_id(&self) -> u32 {
26 u32::from(*self)
27 }
28}
29
30type Cube<T, const DIM: usize> = [[[T; DIM]; DIM]; DIM];
32
33#[derive(Debug, Clone)]
35pub struct HeterogeneousPalette<V: Hash + Eq + Copy, const DIM: usize> {
36 pub(crate) cube: Box<Cube<V, DIM>>,
37 pub(crate) palette: Vec<(V, u16)>,
39}
40
41impl<V: Hash + Eq + Copy, const DIM: usize> HeterogeneousPalette<V, DIM> {
42 fn get(&self, x: usize, y: usize, z: usize) -> V {
43 debug_assert!(x < DIM);
44 debug_assert!(y < DIM);
45 debug_assert!(z < DIM);
46
47 self.cube[y][z][x]
48 }
49
50 fn get_at_index(&self, index: usize) -> V {
51 debug_assert!(index < DIM * DIM * DIM);
52
53 let y = index / (DIM * DIM);
54 let z = (index / DIM) % DIM;
55 let x = index % DIM;
56 self.cube[y][z][x]
57 }
58
59 pub fn iter_values(&self) -> impl Iterator<Item = &V> {
61 self.cube.iter().flatten().flatten()
62 }
63
64 fn set(&mut self, x: usize, y: usize, z: usize, value: V) -> V {
65 debug_assert!(x < DIM);
66 debug_assert!(y < DIM);
67 debug_assert!(z < DIM);
68
69 let old_value = self.cube[y][z][x];
70
71 if let Some((_, count)) = self.palette.iter_mut().find(|(v, _)| *v == value) {
72 *count += 1;
73 } else {
74 self.palette.push((value, 1));
75 }
76
77 if let Some((index, (_, count))) = self
78 .palette
79 .iter_mut()
80 .enumerate()
81 .find(|(_, (v, _))| *v == old_value)
82 {
83 *count -= 1;
84 if *count == 0 {
85 self.palette.swap_remove(index);
86 }
87 }
88
89 self.cube[y][z][x] = value;
90
91 old_value
92 }
93}
94
95#[derive(Debug, Clone)]
103pub enum PalettedContainer<V: Hash + Eq + Copy + Default, const DIM: usize> {
104 Homogeneous(V),
106 Heterogeneous(HeterogeneousPalette<V, DIM>),
108 Building(Box<Cube<V, DIM>>),
112}
113
114enum PaletteMode {
115 Linear,
116 Hash,
117 Global,
118}
119
120impl<V: Hash + Eq + Copy + Default + Debug, const DIM: usize> PalettedContainer<V, DIM> {
121 pub const SIZE: usize = DIM;
123 pub const VOLUME: usize = DIM * DIM * DIM;
125
126 #[must_use]
135 pub fn from_cube(cube: Box<Cube<V, DIM>>) -> Self {
136 let mut palette: Vec<(V, u16)> = Vec::new();
137 let total = DIM * DIM * DIM;
138 let flat: &[V] = unsafe { slice::from_raw_parts(cube.as_ptr().cast::<V>(), total) };
143
144 let mut i = 0;
145 while i < total {
146 let v = flat[i];
147 let mut j = i + 1;
148 while j < total && flat[j] == v {
149 j += 1;
150 }
151 let run_len = (j - i) as u16;
152 if let Some(pos) = palette.iter().position(|(value, _)| *value == v) {
153 palette[pos].1 += run_len;
154 } else {
155 palette.push((v, run_len));
156 }
157 i = j;
158 }
159
160 if palette.len() == 1 {
161 Self::Homogeneous(palette[0].0)
162 } else {
163 Self::Heterogeneous(HeterogeneousPalette { cube, palette })
164 }
165 }
166
167 pub fn get(&self, x: usize, y: usize, z: usize) -> V {
169 match self {
170 Self::Homogeneous(value) => *value,
171 Self::Heterogeneous(data) => data.get(x, y, z),
172 Self::Building(cube) => {
173 debug_assert!(x < DIM);
174 debug_assert!(y < DIM);
175 debug_assert!(z < DIM);
176 cube[y][z][x]
177 }
178 }
179 }
180
181 pub fn get_at_index(&self, index: usize) -> V {
186 debug_assert!(index < Self::VOLUME);
187
188 match self {
189 Self::Homogeneous(value) => *value,
190 Self::Heterogeneous(data) => data.get_at_index(index),
191 Self::Building(cube) => {
192 let y = index / (DIM * DIM);
193 let z = (index / DIM) % DIM;
194 let x = index % DIM;
195 cube[y][z][x]
196 }
197 }
198 }
199
200 pub(crate) fn copy_column_into(&self, x: usize, z: usize, out: &mut [V]) {
202 debug_assert!(x < DIM);
203 debug_assert!(z < DIM);
204 debug_assert!(out.len() >= DIM);
205
206 match self {
207 Self::Homogeneous(value) => {
208 for slot in &mut out[..DIM] {
209 *slot = *value;
210 }
211 }
212 Self::Heterogeneous(data) => {
213 for (y, slot) in out[..DIM].iter_mut().enumerate() {
214 *slot = data.cube[y][z][x];
215 }
216 }
217 Self::Building(cube) => {
218 for (y, slot) in out[..DIM].iter_mut().enumerate() {
219 *slot = cube[y][z][x];
220 }
221 }
222 }
223 }
224
225 #[must_use]
230 pub fn maybe_has(&self, mut predicate: impl FnMut(V) -> bool) -> bool {
231 match self {
232 Self::Homogeneous(value) => predicate(*value),
233 Self::Heterogeneous(data) => data.palette.iter().any(|(value, _)| predicate(*value)),
234 Self::Building(cube) => cube
235 .iter()
236 .flatten()
237 .flatten()
238 .any(|value| predicate(*value)),
239 }
240 }
241
242 #[must_use]
244 pub fn collect_values(&self) -> Vec<V> {
245 match self {
246 Self::Homogeneous(value) => vec![*value; Self::VOLUME],
247 Self::Heterogeneous(data) => data.iter_values().copied().collect(),
248 Self::Building(cube) => cube.iter().flatten().flatten().copied().collect(),
249 }
250 }
251
252 pub fn enter_building_mode(&mut self) {
258 match self {
259 Self::Building(_) => {}
260 Self::Homogeneous(value) => {
261 let cube: Box<Cube<V, DIM>> = Box::new([[[*value; DIM]; DIM]; DIM]);
262 *self = Self::Building(cube);
263 }
264 Self::Heterogeneous(_) => {
265 let taken = mem::replace(self, Self::Homogeneous(V::default()));
266 let Self::Heterogeneous(data) = taken else {
267 unreachable!()
268 };
269 *self = Self::Building(data.cube);
270 }
271 }
272 }
273
274 #[inline]
281 pub fn as_building_slice_mut(&mut self) -> Option<&mut [V]> {
282 if let Self::Building(cube) = self {
283 Some(unsafe {
287 slice::from_raw_parts_mut(cube.as_mut_ptr().cast::<V>(), DIM * DIM * DIM)
288 })
289 } else {
290 None
291 }
292 }
293
294 pub fn finalize_building(&mut self) {
298 if !matches!(self, Self::Building(_)) {
299 return;
300 }
301 let taken = mem::replace(self, Self::Homogeneous(V::default()));
302 let Self::Building(cube) = taken else {
303 unreachable!()
304 };
305 *self = Self::from_cube(cube);
306 }
307
308 pub fn set(&mut self, x: usize, y: usize, z: usize, value: V) -> V {
310 debug_assert!(x < Self::SIZE);
311 debug_assert!(y < Self::SIZE);
312 debug_assert!(z < Self::SIZE);
313
314 match self {
315 Self::Homogeneous(original) => {
316 let original = *original;
317 if value != original {
318 let mut cube = Box::new([[[original; DIM]; DIM]; DIM]);
319 cube[y][z][x] = value;
320 *self = Self::from_cube(cube);
321 }
322 original
323 }
324 Self::Heterogeneous(data) => {
325 let original = data.set(x, y, z, value);
326 if data.palette.len() == 1 {
327 *self = Self::Homogeneous(data.palette[0].0);
328 }
329 original
330 }
331 Self::Building(cube) => {
332 let old = cube[y][z][x];
333 cube[y][z][x] = value;
334 old
335 }
336 }
337 }
338
339 #[expect(
344 clippy::missing_panics_doc,
345 clippy::unwrap_used,
346 reason = "position() is guaranteed to exist: palette was built from the cube's own values"
347 )]
348 pub fn write(&self, writer: &mut impl Write) -> Result<()>
349 where
350 V: ToGlobalId,
351 {
352 match self {
353 Self::Homogeneous(value) => {
354 0u8.write(writer)?;
356 VarInt(value.to_global_id() as i32).write(writer)?;
358 }
360 Self::Heterogeneous(data) => {
361 let (bits, mode) = Self::calculate_strategy(data.palette.len());
362
363 bits.write(writer)?;
365
366 match mode {
368 PaletteMode::Linear | PaletteMode::Hash => {
369 VarInt(data.palette.len() as i32).write(writer)?;
370 for (val, _) in &data.palette {
371 VarInt(val.to_global_id() as i32).write(writer)?;
372 }
373 }
374 PaletteMode::Global => {}
375 }
376
377 let indices: Vec<u32> = data
379 .cube
380 .iter()
381 .flatten()
382 .flatten()
383 .map(|val| {
384 if matches!(mode, PaletteMode::Global) {
385 val.to_global_id()
386 } else {
387 data.palette.iter().position(|(v, _)| v == val).unwrap() as u32
388 }
389 })
390 .collect();
391
392 let packed = pack_bits(&indices, bits as usize);
393
394 for long in packed {
396 long.write(writer)?;
397 }
398 }
399 Self::Building(_) => {
400 panic!(
401 "PalettedContainer in Building mode cannot be serialized; \
402 call finalize_building() first"
403 );
404 }
405 }
406 Ok(())
407 }
408
409 fn calculate_strategy(count: usize) -> (u8, PaletteMode) {
410 if DIM == 16 {
411 match count {
413 0..=1 => unreachable!("Homogeneous handled separately"),
414 2..=16 => (4, PaletteMode::Linear),
415 17..=32 => (5, PaletteMode::Hash),
416 33..=64 => (6, PaletteMode::Hash),
417 65..=128 => (7, PaletteMode::Hash),
418 129..=256 => (8, PaletteMode::Hash),
419 _ => (15, PaletteMode::Global), }
421 } else {
422 match count {
424 0..=1 => unreachable!("Homogeneous handled separately"),
425 2 => (1, PaletteMode::Linear),
426 3..=4 => (2, PaletteMode::Linear),
427 5..=8 => (3, PaletteMode::Hash),
428 _ => (6, PaletteMode::Global), }
430 }
431 }
432}
433
434fn pack_bits(indices: &[u32], bits: usize) -> Vec<u64> {
435 let values_per_long = 64 / bits;
436 let len = indices.len().div_ceil(values_per_long);
437 let mut data = vec![0u64; len];
438
439 for (i, &index) in indices.iter().enumerate() {
440 let array_index = i / values_per_long;
441 let offset = (i % values_per_long) * bits;
442 data[array_index] |= u64::from(index) << offset;
443 }
444
445 data
446}
447
448pub type BlockPalette = PalettedContainer<BlockStateId, 16>;
450pub type BiomePalette = PalettedContainer<u16, 4>;
452
453impl BlockPalette {
454 #[must_use]
456 pub fn non_empty_block_count(&self) -> u16 {
457 match self {
458 Self::Homogeneous(v) => {
459 if v.0 == 0 {
460 0
461 } else {
462 #[expect(
463 clippy::cast_possible_truncation,
464 reason = "VOLUME = 16^3 = 4096, fits in u16"
465 )]
466 {
467 Self::VOLUME as u16
468 }
469 }
470 }
471 Self::Heterogeneous(data) => {
472 let mut count = 0;
473 for (v, c) in &data.palette {
474 if v.0 != 0 {
475 count += c;
476 }
477 }
478 count
479 }
480 Self::Building(cube) => {
481 let mut count: u16 = 0;
482 for slab in cube {
483 for row in slab {
484 for v in row {
485 if v.0 != 0 {
486 count += 1;
487 }
488 }
489 }
490 }
491 count
492 }
493 }
494 }
495
496 #[must_use]
498 pub fn has_only_air(&self) -> bool {
499 match self {
500 Self::Homogeneous(v) => v.is_air(),
501 Self::Heterogeneous(_data) => false,
503 Self::Building(cube) => cube
504 .iter()
505 .flatten()
506 .flatten()
507 .all(steel_utils::BlockStateId::is_air),
508 }
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::BlockPalette;
515 use steel_utils::BlockStateId;
516
517 fn assert_column_matches_get(container: &BlockPalette, x: usize, z: usize) {
518 let mut column = [BlockStateId::default(); 16];
519 container.copy_column_into(x, z, &mut column);
520 for (y, state) in column.into_iter().enumerate() {
521 assert_eq!(state, container.get(x, y, z));
522 }
523 }
524
525 #[test]
526 fn get_at_index_reads_homogeneous_palette_values() {
527 let container = BlockPalette::Homogeneous(BlockStateId(42));
528
529 assert_eq!(container.get_at_index(0), BlockStateId(42));
530 assert_eq!(
531 container.get_at_index(BlockPalette::VOLUME - 1),
532 BlockStateId(42)
533 );
534 }
535
536 #[test]
537 fn get_at_index_uses_y_z_x_linear_order_for_blocks() {
538 let mut container = BlockPalette::Homogeneous(BlockStateId(0));
539
540 container.set(3, 4, 5, BlockStateId(99));
541
542 assert_eq!(
543 container.get_at_index(3 + 5 * 16 + 4 * 16 * 16),
544 BlockStateId(99)
545 );
546 }
547
548 #[test]
549 fn maybe_has_checks_palette_values_without_scanning_cells() {
550 let mut container = BlockPalette::Homogeneous(BlockStateId(0));
551 assert!(!container.maybe_has(|state| state == BlockStateId(7)));
552
553 container.set(1, 2, 3, BlockStateId(7));
554 assert!(container.maybe_has(|state| state == BlockStateId(7)));
555 assert!(!container.maybe_has(|state| state == BlockStateId(9)));
556 }
557
558 #[test]
559 fn copy_column_into_matches_get_for_homogeneous_container() {
560 let container = BlockPalette::Homogeneous(BlockStateId(7));
561 assert_column_matches_get(&container, 3, 12);
562 }
563
564 #[test]
565 fn copy_column_into_matches_get_for_heterogeneous_container() {
566 let x = 5;
567 let z = 9;
568 let mut cube = Box::new([[[BlockStateId::default(); 16]; 16]; 16]);
569 for y in 0..16 {
570 cube[y][z][x] = BlockStateId((y + 1) as u16);
571 }
572
573 let container = BlockPalette::from_cube(cube);
574 assert_column_matches_get(&container, x, z);
575 }
576
577 #[test]
578 fn copy_column_into_matches_get_for_building_container() {
579 let x = 11;
580 let z = 2;
581 let mut container = BlockPalette::Homogeneous(BlockStateId(3));
582 container.enter_building_mode();
583 for y in 0..16 {
584 container.set(x, y, z, BlockStateId((31 + y) as u16));
585 }
586
587 assert_column_matches_get(&container, x, z);
588 }
589}