1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
//! `chunk` contains the [`ChunkData`] struct and it's core impls.
//! [`ChunkData`] is a wrapper for the actual chunk nbt and some attached data
//! to keep track of pending blocks and biomes and what blocks/biomes we've seen before.
use crate::{
BiomeCell, Block, BlockWithCoordinate, Coords, NbtString, Result, biome::BiomeCellWithId,
};
use ahash::AHashMap;
use fixedbitset::FixedBitSet;
use simdnbt::owned::NbtCompound;
use std::{fmt::Debug, ops::Range};
/// A chunk within a region and it's attached data to track pending blocks.
///
/// Provides some lower level set functions that [`Region`](crate::Region) uses.
#[derive(Debug, Clone, PartialEq)]
pub struct ChunkData {
/// The chunks actual NBT data
pub nbt: NbtCompound,
/// The world height, we keep a range copy here since we need it
/// for Bitset and index calculations
pub(crate) world_height: Range<isize>,
// note: we changed pending blocks to use a palette system, similar to Minecraft
// to use less memory and using references and lazily converting nbt > rust data
// we could do something similar to this but i have no clue if its worth the rewrite
// here do we store each block and its coordinate, soo putting in like a whole region is slow.
// but on the other side, the user can just fix that themself by set_block'ing per chunk
// and or flushing the blocks faster, which isnt possible with get_blocks and how that works.
/// The blocks that have been written but not pushed to the NBT
pub(crate) pending_blocks: AHashMap<i8, Vec<BlockWithCoordinate>>,
/// A check of what blocks has been seen before in `pending_blocks`
/// This is to avoid duplicate coordinate and makes stuff faster.
pub(crate) seen_blocks: FixedBitSet,
/// The biomes that have been written but not pushed to the NBT
pub(crate) pending_biomes: AHashMap<i8, Vec<BiomeCellWithId>>,
/// A check of what blocks has been seen before in `pending_biomes`
pub(crate) seen_biomes: FixedBitSet,
/// If this is unmarked, the block write logic will skip this one.
pub(crate) dirty_blocks: bool,
/// If this is unmarked, the biome write logic will skip this one.
pub(crate) dirty_biomes: bool,
}
impl ChunkData {
/// How many blocks wide a chunk is.
///
/// Also how wide/tall a single section is.
pub(crate) const WIDTH: usize = 16;
/// Set a block at the specified coordinates *(local to within the chunk)*.
///
/// In most cases you'd want to use [`Region::set_block`](crate::Region::set_block) instead since that picks
/// the right chunk and handles it for you.
pub fn set_block<C, B: Into<Block>>(&mut self, coords: C, block: B) -> Result<Option<()>>
where
C: Into<Coords>,
{
let coords: Coords = coords.into();
assert!(coords.x < ChunkData::WIDTH as u32 && coords.z < ChunkData::WIDTH as u32);
let index = self.get_block_index(&coords);
if !self.seen_blocks.contains(index) {
self.seen_blocks.insert(index);
let section_y = (coords.y as f64 / ChunkData::WIDTH as f64).floor() as i8;
self.pending_blocks
.entry(section_y)
.or_insert_with(|| Vec::with_capacity(ChunkData::WIDTH.pow(3)))
.push(BlockWithCoordinate {
coordinates: coords,
block: block.into(),
});
self.dirty_blocks = true;
return Ok(Some(()));
}
Ok(None)
}
/// Set a biome at the specified cell.
///
/// In most cases you'd want to use [`Region::set_biome`](crate::Region::set_biome) instead since that picks
/// the right chunk and handles it for you.
///
/// But if you have a [`ChunkData`] and know that these coordinates are within
/// this specific chunk then go ahead and use this.
///
/// But be careful.
pub fn set_biome<C: Into<BiomeCell>, B: Into<NbtString>>(
&mut self,
cell: C,
biome: B,
) -> Result<Option<()>> {
let cell: BiomeCell = cell.into();
let biome: NbtString = biome.into();
let index = self.get_biome_index(&cell);
if !self.seen_biomes.contains(index) {
self.seen_biomes.insert(index);
self.pending_biomes
.entry(cell.section)
.or_insert_with(|| Vec::with_capacity((BiomeCell::CELL_SIZE.pow(3)) as usize))
.push(BiomeCellWithId { cell, id: biome });
self.dirty_biomes = true;
return Ok(Some(()));
}
Ok(None)
}
/// Returns the [`FixedBitSet`] index for these coordinates.
pub(crate) fn get_block_index(&self, coords: &Coords) -> usize {
let y_offset = (coords.y as isize - self.world_height.start) as usize;
coords.x as usize
+ y_offset * ChunkData::WIDTH
+ coords.z as usize
* ChunkData::WIDTH
* (self.world_height.end - -self.world_height.start) as usize
}
/// Returns the index for a biome in the [`Self::seen_biomes`] bitset based of it's cell coordinates
pub(crate) fn get_biome_index(&self, cell: &BiomeCell) -> usize {
let cell_size = BiomeCell::CELL_SIZE as usize;
let (bx, by, bz) = (
cell.cell.0 as usize,
cell.cell.1 as usize,
cell.cell.2 as usize,
);
(cell.section - (self.world_height.start / ChunkData::WIDTH as isize) as i8) as usize
* cell_size
* cell_size
* cell_size
+ bx
+ bz * cell_size
+ by * cell_size * cell_size
}
/// Returns the [`FixedBitSet`] for seen_biomes
pub(crate) fn biome_bitset(world_height: usize) -> FixedBitSet {
let section_count = world_height / ChunkData::WIDTH;
let size = section_count * (BiomeCell::CELL_SIZE.pow(3)) as usize;
FixedBitSet::with_capacity(size)
}
/// Returns the [`FixedBitSet`] for seen_blocks
pub(crate) fn block_bitset(world_height: usize) -> FixedBitSet {
let size = ChunkData::WIDTH * world_height * ChunkData::WIDTH;
FixedBitSet::with_capacity(size)
}
/// Sets the internal block buffer.
///
/// Overwrites any and all data related to the buffer.
pub fn set_internal_block_buffer(&mut self, buffer: AHashMap<i8, Vec<BlockWithCoordinate>>) {
self.pending_blocks = buffer;
self.seen_blocks.clear();
}
/// Sets the internal biome buffer.
///
/// Overwrites any and all data related to the buffer.
pub fn set_internal_biome_buffer(&mut self, buffer: AHashMap<i8, Vec<BiomeCellWithId>>) {
self.pending_biomes = buffer;
self.seen_biomes.clear();
}
/// Creates a new [`ChunkData`] with empty and cleared buffers.
pub fn new(nbt: NbtCompound, world_height: Range<isize>) -> ChunkData {
let world_height_count = world_height.clone().count();
ChunkData {
nbt: nbt,
world_height: world_height.clone(),
pending_blocks: AHashMap::new(),
pending_biomes: AHashMap::new(),
seen_blocks: ChunkData::block_bitset(world_height_count),
seen_biomes: ChunkData::biome_bitset(world_height_count),
dirty_blocks: false,
dirty_biomes: false,
}
}
/// Checks if there exists a [`Block`] in the internal buffer at [`Coords`]
pub fn buffer_contains_block<C>(&self, coords: C) -> bool
where
C: Into<Coords>,
{
let coords: Coords = coords.into();
self.seen_blocks.contains(self.get_block_index(&coords))
}
/// Checks if there exists a [`NbtString`] (biome) in the internal buffer at [`BiomeCell`]
pub fn buffer_contains_biome<C>(&self, cell: C) -> bool
where
C: Into<BiomeCell>,
{
let cell: BiomeCell = cell.into();
self.seen_biomes.contains(self.get_biome_index(&cell))
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::Region;
#[test]
fn buffer_contains_block() -> Result<()> {
let region = Region::default();
let mut chunk = region.get_chunk_mut(5, 1)?;
let none = chunk.buffer_contains_block((0, 1, 8));
assert!(none == false);
chunk.set_block((6, 1, 8), "minecraft:furnace")?;
let some = chunk.buffer_contains_block((6, 1, 8));
assert!(some);
chunk.write_blocks((5, 1), region.get_config())?;
let none = chunk.buffer_contains_block((6, 1, 8));
assert!(none == false);
Ok(())
}
}