mirror of
https://github.com/neocturne/MinedMap.git
synced 2025-04-20 11:35:07 +02:00
194 lines
5.3 KiB
Rust
194 lines
5.3 KiB
Rust
//! Functions to search the "top" layer of a chunk
|
|
|
|
use std::num::NonZeroU16;
|
|
|
|
use anyhow::{Context, Result};
|
|
use indexmap::IndexSet;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use super::chunk::{Chunk, SectionIterItem};
|
|
use crate::{
|
|
resource::{Biome, BlockFlag, BlockType},
|
|
types::*,
|
|
};
|
|
|
|
/// Height (Y coordinate) of a block
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct BlockHeight(pub i32);
|
|
|
|
impl BlockHeight {
|
|
/// Constructs a new [BlockHeight] from section and block Y indices
|
|
///
|
|
/// Returns an error if the resulting coordindate does not fit into
|
|
/// an [i32].
|
|
pub fn new(section: SectionY, block: BlockY) -> Result<Self> {
|
|
let height = section
|
|
.0
|
|
.checked_mul(BLOCKS_PER_CHUNK as i32)
|
|
.and_then(|y| y.checked_add_unsigned(block.0.into()))
|
|
.context("Block height out of bounds")?;
|
|
Ok(BlockHeight(height))
|
|
}
|
|
}
|
|
|
|
/// Array optionally storing a [BlockType] for each coordinate of a chunk
|
|
pub type BlockArray = LayerBlockArray<Option<BlockType>>;
|
|
|
|
/// Array optionally storing a biome index for each coordinate of a chunk
|
|
///
|
|
/// The entries refer to a biome list generated with the top layer data.
|
|
/// Indices are stored incremented by 1 to allow using a [NonZeroU16].
|
|
pub type BiomeArray = LayerBlockArray<Option<NonZeroU16>>;
|
|
|
|
/// Array storing a block light value for each coordinate for a chunk
|
|
pub type BlockLightArray = LayerBlockArray<u8>;
|
|
|
|
/// Array optionally storing a depth value for each coordinate for a chunk
|
|
pub type DepthArray = LayerBlockArray<Option<BlockHeight>>;
|
|
|
|
/// References to LayerData entries for a single coordinate pair
|
|
struct LayerEntry<'a> {
|
|
/// The block type of the referenced entry
|
|
block: &'a mut Option<BlockType>,
|
|
/// The biome type of the referenced entry
|
|
biome: &'a mut Option<NonZeroU16>,
|
|
/// The block light of the referenced entry
|
|
block_light: &'a mut u8,
|
|
/// The depth value of the referenced entry
|
|
depth: &'a mut Option<BlockHeight>,
|
|
}
|
|
|
|
impl<'a> LayerEntry<'a> {
|
|
/// Returns true if the entry has not been filled yet (no opaque block has been encountered)
|
|
///
|
|
/// The depth value is filled separately when a non-water block is encountered after the block type
|
|
/// has already been filled.
|
|
fn is_empty(&self) -> bool {
|
|
self.block.is_none()
|
|
}
|
|
|
|
/// Returns true if the entry has been filled including its depth (an opaque non-water block has been
|
|
/// encountered)
|
|
fn done(&self) -> bool {
|
|
self.depth.is_some()
|
|
}
|
|
|
|
/// Fills in the LayerEntry
|
|
///
|
|
/// Checks whether the passed coordinates point at an opaque or non-water block and
|
|
/// fills in the entry accordingly. Returns true when the block has been filled including its depth.
|
|
fn fill(
|
|
&mut self,
|
|
biome_list: &mut IndexSet<Biome>,
|
|
section: SectionIterItem,
|
|
coords: SectionBlockCoords,
|
|
) -> Result<bool> {
|
|
let Some(block_type) = section
|
|
.section
|
|
.block_at(coords)?
|
|
.filter(|block_type| block_type.is(BlockFlag::Opaque))
|
|
else {
|
|
if self.is_empty() {
|
|
*self.block_light = section.block_light.block_light_at(coords);
|
|
}
|
|
|
|
return Ok(false);
|
|
};
|
|
|
|
if self.is_empty() {
|
|
*self.block = Some(block_type);
|
|
if let Some(biome) = section.biomes.biome_at(section.y, coords)? {
|
|
let (biome_index, _) = biome_list.insert_full(*biome);
|
|
*self.biome = NonZeroU16::new(
|
|
(biome_index + 1)
|
|
.try_into()
|
|
.expect("biome index not in range"),
|
|
);
|
|
}
|
|
}
|
|
|
|
if block_type.is(BlockFlag::Water) {
|
|
return Ok(false);
|
|
}
|
|
|
|
let height = BlockHeight::new(section.y, coords.y)?;
|
|
*self.depth = Some(height);
|
|
|
|
Ok(true)
|
|
}
|
|
}
|
|
|
|
/// Top layer data
|
|
///
|
|
/// A LayerData stores block type, biome, block light and depth data for
|
|
/// each coordinate of a chunk.
|
|
#[derive(Debug, Default)]
|
|
pub struct LayerData {
|
|
/// Block type data
|
|
pub blocks: Box<BlockArray>,
|
|
/// Biome data
|
|
pub biomes: Box<BiomeArray>,
|
|
/// Block light data
|
|
pub block_light: Box<BlockLightArray>,
|
|
/// Depth data
|
|
pub depths: Box<DepthArray>,
|
|
}
|
|
|
|
impl LayerData {
|
|
/// Builds a [LayerEntry] referencing the LayerData at a given coordinate pair
|
|
fn entry(&mut self, coords: LayerBlockCoords) -> LayerEntry {
|
|
LayerEntry {
|
|
block: &mut self.blocks[coords],
|
|
biome: &mut self.biomes[coords],
|
|
block_light: &mut self.block_light[coords],
|
|
depth: &mut self.depths[coords],
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Fills in a [LayerData] with the information of the chunk's top
|
|
/// block layer
|
|
///
|
|
/// For each (X, Z) coordinate pair, the topmost opaque block is
|
|
/// determined as the block that should be visible on the rendered
|
|
/// map. For water blocks, the height of the first non-water block
|
|
/// is additionally filled in as the water depth (the block height is
|
|
/// used as depth otherwise).
|
|
pub fn top_layer(biome_list: &mut IndexSet<Biome>, chunk: &Chunk) -> Result<Option<LayerData>> {
|
|
use BLOCKS_PER_CHUNK as N;
|
|
|
|
if chunk.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
|
|
let mut done = 0;
|
|
let mut ret = LayerData::default();
|
|
|
|
for section in chunk.sections().rev() {
|
|
for y in BlockY::iter().rev() {
|
|
for z in BlockZ::iter() {
|
|
for x in BlockX::iter() {
|
|
let xz = LayerBlockCoords { x, z };
|
|
|
|
let mut entry = ret.entry(xz);
|
|
if entry.done() {
|
|
continue;
|
|
}
|
|
|
|
let coords = SectionBlockCoords { xz, y };
|
|
if !entry.fill(biome_list, section, coords)? {
|
|
continue;
|
|
}
|
|
|
|
assert!(entry.done());
|
|
done += 1;
|
|
if done == N * N {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(Some(ret))
|
|
}
|