Skip to content
This repository has been archived by the owner on Jul 1, 2024. It is now read-only.

Commit

Permalink
Implement HeightMap
Browse files Browse the repository at this point in the history
  • Loading branch information
DimitriTimoz committed Dec 24, 2023
1 parent 9e0bfce commit bb0b93d
Showing 1 changed file with 263 additions and 8 deletions.
271 changes: 263 additions & 8 deletions minecraft-server/src/world/map.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::collections::HashMap;
use minecraft_protocol::components::chunk::PalettedData;
use std::{collections::HashMap, cmp::Ordering};
use minecraft_protocol::{components::chunk::PalettedData, ids::blocks::Block};
use tokio::sync::RwLock;
use crate::prelude::*;

Expand Down Expand Up @@ -169,11 +169,193 @@ impl Chunk {
}
}

struct HeightMap {
base: u8,
data: Vec<u64>,
max_height: Option<u32>,
}

impl HeightMap {
pub fn new(base: u8) -> Self {
assert!(base <= 9, "base must be <= 9 because the max height is 320 + 64");
Self {
base,
data: vec![0; ((16 * 16 * 9usize).div_euclid(base as usize) + 1) * base as usize ],
max_height: None
}
}

pub fn to_tag(&self) -> NbtTag {
NbtTag::Compound(
HashMap::from_iter(
vec![
(String::from("MOTION_BLOCKING"), NbtTag::LongArray(unsafe {
std::mem::transmute::<Vec<u64>, Vec<i64>>(self.data.clone())
})),
]
)
)
}

/// Update the current base of the heightmap.
fn new_base(&mut self, new_base: u8) {
assert!(new_base <= 9, "base must be <= 9 because the max height is 320 + 64");

let old_base = self.base as usize;

unimplemented!();

self.base = new_base as u8;
}


fn get_need_base(&self, height: u32) -> u8 {
32 - ((height + 1).leading_zeros() as u8)
}

/// Set the height of the highest block at the given position.
pub fn set(&mut self, position: &BlockPositionInChunkColumn, height: u32) {
let (x, z) = (position.bx, position.bz);
// Check if the height is higher than the current max height.
if let Some(max_height) = self.max_height {
if height < max_height { // Calculate the new base for the data.
let new_base = self.get_need_base(height);
// Update the base & max height.
self.max_height = Some(height);
}
} else {
// Set the max height.
self.max_height = Some(height);
}

let index = (x * 16 + z) as usize; // assuming a 16x16 chunk column
let bits_per_entry = self.base as usize;
let bit_pos = index * bits_per_entry;
let data_index = bit_pos / 64;
let bit_offset = bit_pos % 64;

// Ensure we don't shift beyond the limits of the data type.
if bits_per_entry >= 64 {
panic!("base too large for u64 storage");
}

// Cast the height to u64
let height = height as u64;

// Prepare the mask to clear the bits at the position.
let mask = ((1 << bits_per_entry) - 1) << bit_offset;
// Clear the bits at the target position.
self.data[data_index] &= !mask;
// Set the new height with the sign.
self.data[data_index] |= height << bit_offset;
// Check if the entry spills over to the next u64.
if bit_offset + bits_per_entry > 64 {
// Calculate how many bits spill over.
let spill_over_bits = bit_offset + bits_per_entry - 64;
// Prepare the mask to clear the spill over bits.
let spill_over_mask = (1 << spill_over_bits) - 1;
// Clear the spill over bits in the next entry.
self.data[data_index + 1] &= !spill_over_mask;
// Set the spill over bits.
self.data[data_index + 1] |= height >> (64 - bit_offset);
}
}

/// Get the height of the highest block at the given position.
pub fn get(&self, position: &BlockPositionInChunkColumn) -> u16 {
let (x, z) = (position.bx, position.bz);

let index = (x * 16 + z) as usize; // assuming a 16x16 chunk column
let bits_per_entry = self.base as usize;
let bit_pos = index * bits_per_entry;
let data_index = bit_pos / 64;
let bit_offset = bit_pos % 64;

// Prepare the mask to get the bits at the position.
let mask = ((1u64 << bits_per_entry) - 1) << bit_offset;
// Retrieve the bits.
let mut value = (self.data[data_index] & mask) >> bit_offset;

// Check if the entry spills over to the next u64 and retrieve the remaining bits.
if bit_offset + bits_per_entry > 64 {
// Calculate how many bits spill over.
let spill_over_bits = bit_offset + bits_per_entry - 64;
// Prepare the mask to get the spill over bits.
let spill_over_mask = (1u64 << spill_over_bits) - 1;
// Retrieve the spill over bits from the next entry.
value |= (self.data[data_index + 1] & spill_over_mask) << (64 - bit_offset);
}

// Perform sign extension if the value is negative.
let sign_bit = 1u64 << (bits_per_entry - 1);
if value & sign_bit != 0 {
// If the sign bit is set, extend the sign to the rest of the i64.
value |= !((1u64 << bits_per_entry) - 1);
}

// Cast to i32 with sign extension.
value as u16
}
}

pub(super) struct ChunkColumn {
heightmap: HeightMap,
chunks: Vec<Chunk>,
}

impl ChunkColumn {
pub const MAX_HEIGHT: u16 = 320 + 64; // TODO: adapt to the world height
pub const MIN_Y: i32 = -64;

fn init_chunk_heightmap(&mut self){
self.heightmap = HeightMap::new(9);
if self.chunks.len() != 24 {
panic!("Chunk column must have 24 chunks (change it for other world heights)");
}

// Start from the higher chunk
for bx in 0..16 {
for bz in 0..16 {
let height = self.get_higher_skylight_filter_block(&BlockPositionInChunkColumn { bx, y: 0, bz }, Self::MAX_HEIGHT).into();
self.heightmap.set(&BlockPositionInChunkColumn { bx, y: 0, bz }, height);
}
}
}

fn get_higher_skylight_filter_block(&self, position: &BlockPositionInChunkColumn, current_height: u16) -> u16 {
let n_chunk_to_skip = self.chunks.len() - current_height.div_euclid(16) as usize - (current_height.rem_euclid(16) > 0) as usize;
let mut current_height = current_height - 1;
// Downward propagation
for chunk in self.chunks.iter().rev().skip(n_chunk_to_skip) {
for by in (0..((((current_height) % 16) + 1) as u8)).rev() {
let block: BlockWithState = chunk.get_block(BlockPositionInChunk { bx: position.bx, by, bz: position.bz });
// SAFETY: fom_id will get a valid block necessarily
if !Block::from(block).is_transparent() {
return current_height + 1;
}
current_height = current_height.saturating_sub(1);
}
}
current_height
}

pub(super) fn get_highest_block(&self) -> u32 {
self.heightmap.max_height.unwrap_or(0)
}

pub(super) fn get_highest_block_at(&self, position: &BlockPositionInChunkColumn) -> u16 {
self.heightmap.get(position)
}

pub fn from(chunks: Vec<Chunk>) -> Self {
let mut column = Self {
chunks,
heightmap: HeightMap::new(9),
};
column.init_chunk_heightmap();
column
}

pub fn flat() -> Self {
let empty_chunk = Chunk {
data: NetworkChunk {
Expand All @@ -196,10 +378,10 @@ impl ChunkColumn {
for _ in 0..23 {
chunks.push(empty_chunk.clone());
}
ChunkColumn { chunks }
Self::from(chunks)
}

fn get_block(&self, position: BlockPositionInChunkColumn) -> BlockWithState {
pub(super) fn get_block(&self, position: BlockPositionInChunkColumn) -> BlockWithState {
fn get_block_inner(s: &ChunkColumn, position: BlockPositionInChunkColumn) -> Option<BlockWithState> {
let cy = position.cy();
let cy_in_vec: usize = cy.saturating_add(4).try_into().ok()?;
Expand All @@ -210,18 +392,39 @@ impl ChunkColumn {
get_block_inner(self, position).unwrap_or(BlockWithState::Air)
}

#[cfg(test)]
pub fn set_block_for_test(&mut self, position: BlockPositionInChunkColumn, block: BlockWithState) {
self.set_block(position, block);
}

fn set_block(&mut self, position: BlockPositionInChunkColumn, block: BlockWithState) {
fn set_block_innter(s: &mut ChunkColumn, position: BlockPositionInChunkColumn, block: BlockWithState) -> Option<()> {
let cy = position.cy();
let cy_in_vec: usize = cy.saturating_add(4).try_into().ok()?;
let position = position.in_chunk();
let chunk = s.chunks.get_mut(cy_in_vec)?;
chunk.set_block(position, block);
chunk.set_block(position, block.clone());
Some(())
}
set_block_innter(self, position, block);
}
}
set_block_innter(self, position.clone(), block.clone());

let last_height = self.heightmap.get(&position);
let not_filter_sunlight = Block::from(block.clone()).is_transparent(); // TODO: check if the block is transparent

// Get the height of the placed block
let block_height = (position.y - Self::MIN_Y + 1).max(0) as u16;
match block_height.cmp(&last_height) {
Ordering::Greater if !not_filter_sunlight => {
self.heightmap.set(&position, block_height.into());
},
Ordering::Equal if not_filter_sunlight => {
// Downward propagation
let new_height = self.get_higher_skylight_filter_block(&position, last_height).into();
self.heightmap.set(&position, new_height);
},
_ => {}
}
}}

impl WorldMap {
pub fn new(shard_count: usize) -> WorldMap {
Expand Down Expand Up @@ -455,6 +658,58 @@ mod tests {
}
}

#[test]
fn test_heightmap_get_and_set() {
let mut heightmap = HeightMap::new(5);
heightmap.set(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 0 }, 0);
heightmap.set(&BlockPositionInChunkColumn { bx: 0, y: -2, bz: 1 }, 2);
heightmap.set(&BlockPositionInChunkColumn { bx: 0, y: 3, bz: 2 }, 3);
heightmap.set(&BlockPositionInChunkColumn { bx: 0, y: -4, bz: 3 }, 4);
heightmap.set(&BlockPositionInChunkColumn { bx: 0, y: -4, bz: 7 }, 5);

// Test get
assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 0 }), 0);
assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 1 }), 2);
assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 2 }), 3);
assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 3 }), 4);
assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 7 }), 5);

// Test erase
heightmap.set(&BlockPositionInChunkColumn { bx: 0, y: 12, bz: 0 }, 12);
assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 12, bz: 0 }), 12);

// Test new base
//heightmap.new_base(8);
assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 0 }), 12);
assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 1 }), 2);
assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 2 }), 3);
assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 3 }), 4);
assert_eq!(heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 7 }), 5);
}

#[test]
fn test_heightmap_auto_updates() {
let mut flat_column = ChunkColumn::flat();

// Check that the heightmap is correct
flat_column.set_block(BlockPositionInChunkColumn { bx: 0, y: 2, bz: 0 }, BlockWithState::GrassBlock { snowy: true });
flat_column.init_chunk_heightmap();
assert_eq!(flat_column.heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 0 }), 67);
assert_eq!(flat_column.heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 1 }), 16);

// Now check that the heightmap is correct after setting a block
flat_column.set_block(BlockPositionInChunkColumn { bx: 0, y: 10, bz: 0 }, BlockWithState::GrassBlock { snowy: false });
assert_eq!(flat_column.heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 0 }), 75);

// Check that the heightmap is correct after setting a block to air under the highest block
flat_column.set_block(BlockPositionInChunkColumn { bx: 0, y: 8, bz: 0 }, BlockWithState::Air);
assert_eq!(flat_column.heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 0 }), 75);

// Check that the heightmap is correct after setting the highest block to air
flat_column.set_block(BlockPositionInChunkColumn { bx: 0, y: 10, bz: 0 }, BlockWithState::Air);
assert_eq!(flat_column.heightmap.get(&BlockPositionInChunkColumn { bx: 0, y: 0, bz: 0 }), 67);
}

#[tokio::test]
async fn test_try_move() {
let map = WorldMap::new(1);
Expand Down

0 comments on commit bb0b93d

Please sign in to comment.