Test feature generation

This commit is contained in:
2025-04-27 19:22:16 +03:00
parent efe1d87d76
commit 3d8e7e23a6
13 changed files with 746 additions and 28 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"model": {
"simple": "iron_ore"
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"model": {
"simple": "leaves"
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"model": {
"simple": "log_side"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

+1 -1
View File
@@ -1,5 +1,5 @@
#![feature(duration_constants)]
#![allow(clippy::new_without_default)]
#![allow(clippy::new_without_default, clippy::let_unit_value)]
use std::{process::ExitCode, sync::Arc, time::Instant};
+13 -7
View File
@@ -46,12 +46,16 @@ impl WorldMeshState {
&mut self,
allocators: &Arc<Allocators>,
coords: ChunkCoords,
world: &World,
world: &mut World,
) -> Result<Option<&ChunkMesh>, Error> {
match self.chunks.entry(coords) {
Entry::Occupied(entry) => Ok(Some(entry.into_mut())),
Entry::Vacant(entry) => {
if let Some(chunk) = world.get(coords) {
if let Some(chunk) = world.get_mut(coords) {
if chunk.clear_dirty() {
self.chunks.remove(&coords);
}
match self.chunks.entry(coords) {
Entry::Occupied(entry) => Ok(Some(entry.into_mut())),
Entry::Vacant(entry) => {
let mesh = ChunkMesh::from_chunk(
allocators,
&self.block_model_registry,
@@ -59,10 +63,12 @@ impl WorldMeshState {
chunk,
)?;
Ok(Some(entry.insert(mesh)))
} else {
Ok(None)
}
}
} else {
self.chunks.remove(&coords);
Ok(None)
}
}
}
+20
View File
@@ -3,6 +3,7 @@ use super::NeighborQuery;
pub struct Chunk {
blocks: [u8; Self::SIZE * Self::SIZE * Self::HEIGHT],
height_map: [u32; Self::SIZE * Self::SIZE],
dirty: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -12,6 +13,7 @@ pub struct ChunkCoords {
}
impl ChunkCoords {
#[inline]
pub const fn new(x: i32, z: i32) -> Self {
Self { x, z }
}
@@ -24,6 +26,12 @@ impl ChunkCoords {
let (cx, cz) = self.origin_block();
(cx + x as i32, cz + z as i32)
}
pub fn from_block_coords(x: i32, z: i32) -> Self {
let cx = if x < 0 { x - Chunk::SIZE as i32 + 1 } else { x } / Chunk::SIZE as i32;
let cz = if z < 0 { z - Chunk::SIZE as i32 + 1 } else { z } / Chunk::SIZE as i32;
Self::new(cx, cz)
}
}
impl Chunk {
@@ -34,9 +42,16 @@ impl Chunk {
Self {
blocks: [0; Self::SIZE * Self::SIZE * Self::HEIGHT],
height_map: [0; Self::SIZE * Self::SIZE],
dirty: false,
}
}
pub fn clear_dirty(&mut self) -> bool {
let dirty = self.dirty;
self.dirty = false;
dirty
}
pub fn fill_layer<F: Fn(u32, u32) -> u8>(&mut self, y: u32, f: F) {
for x in 0..Self::SIZE as u32 {
for z in 0..Self::SIZE as u32 {
@@ -50,7 +65,12 @@ impl Chunk {
}
pub fn set_id(&mut self, x: u32, y: u32, z: u32, id: u8) {
if x >= Self::SIZE as u32 || z >= Self::SIZE as u32 || y >= Self::HEIGHT as u32 {
panic!("Invalid coords: {x}, {y}, {z}");
}
self.blocks[Self::to_index(x, y, z)] = id;
self.dirty = true;
if id != 0 && y >= self.top_height_at(x, z) {
self.set_top_height_at(x, z, y + 1);
+467
View File
@@ -0,0 +1,467 @@
use std::{
mem,
ops::{Add, Deref, Range},
};
use super::{level::BoundingBox, Chunk, ChunkCoords};
pub enum FeaturePlacementFilter {
ReplaceAll,
ReplaceAir,
Replace(u8),
}
pub struct FeatureLayer {
x_range: Range<i32>,
z_range: Range<i32>,
content: Vec<u8>,
}
pub struct FeatureBuilder {
y_range: Range<i32>,
layers: Vec<FeatureLayer>,
}
pub struct Feature {
width: u32,
height: u32,
depth: u32,
filter: FeaturePlacementFilter,
content: Vec<u8>,
}
pub struct PositionedFeature {
inner: Feature,
x: i32,
y: i32,
z: i32,
}
impl Deref for PositionedFeature {
type Target = Feature;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl FeaturePlacementFilter {
pub fn should_place(&self, from: u8) -> bool {
match self {
Self::ReplaceAir => from == 0,
Self::ReplaceAll => true,
&Self::Replace(id) => from == id,
}
}
}
impl PositionedFeature {
pub fn aabb(&self) -> BoundingBox {
BoundingBox {
x: self.x,
y: self.y,
z: self.z,
w: self.width as i32,
h: self.height as i32,
d: self.depth as i32,
}
}
pub fn place(&self, coords: ChunkCoords, chunk: &mut Chunk) {
let in_chunk_aabb = self.aabb().in_chunk(coords);
let Some(in_chunk_aabb) = in_chunk_aabb else {
return;
};
let (sx, sz) = coords.origin_block();
for ix in 0..in_chunk_aabb.w {
// Coords in chunk
let cx = ix + in_chunk_aabb.x;
// Coords in feature
let fx = cx as i32 + sx - self.x;
assert!(fx >= 0);
assert!(fx < self.width as i32);
for iy in 0..in_chunk_aabb.h {
let cy = iy + in_chunk_aabb.y;
for iz in 0..in_chunk_aabb.d {
let cz = iz + in_chunk_aabb.z;
let fz = cz as i32 + sz - self.z;
assert!(fz >= 0);
assert!(fz < self.depth as i32);
let chunk_id = chunk.get_id(cx, cy, cz);
let feature_id = self.get(fx as u32, iy, fz as u32);
if self.filter.should_place(chunk_id) {
chunk.set_id(cx, cy, cz, feature_id);
}
}
}
}
}
}
impl Feature {
pub fn position(self, x: i32, y: i32, z: i32) -> PositionedFeature {
PositionedFeature {
inner: self,
x,
y,
z,
}
}
pub fn position_centered_horizontally(self, x: i32, y: i32, z: i32) -> PositionedFeature {
let x = x - self.width as i32 / 2;
let z = z - self.width as i32 / 2;
PositionedFeature {
inner: self,
x,
y,
z,
}
}
pub fn get(&self, x: u32, y: u32, z: u32) -> u8 {
let i = x + (z + y * self.depth) * self.width;
self.content[i as usize]
}
}
impl FeatureLayer {
pub fn new() -> Self {
Self {
x_range: 0..0,
z_range: 0..0,
content: vec![],
}
}
#[inline]
pub fn width(&self) -> u32 {
range_width(&self.x_range)
}
#[inline]
pub fn depth(&self) -> u32 {
range_width(&self.z_range)
}
#[inline]
pub fn size(&self) -> (u32, u32) {
(self.width(), self.depth())
}
pub fn set(&mut self, x: i32, z: i32, id: u8) {
self.adjust_for_placement(x, z);
let i = self.index(x, z);
self.content[i] = id;
}
pub fn replace(&mut self, x: i32, z: i32, from: u8, to: u8) {
self.adjust_for_placement(x, z);
if self.get(x, z) == from {
self.set(x, z, to);
}
}
pub fn get(&self, x: i32, z: i32) -> u8 {
self.content[self.index(x, z)]
}
pub fn try_get(&self, x: i32, z: i32) -> Option<u8> {
if !self.x_range.contains(&x) || !self.z_range.contains(&z) {
None
} else {
Some(self.get(x, z))
}
}
pub fn reserve(&mut self, x_range: Range<i32>, z_range: Range<i32>) {
let new_x_range = x_range.start.min(self.x_range.start)..x_range.end.max(self.x_range.end);
let new_z_range = z_range.start.min(self.z_range.start)..z_range.end.max(self.z_range.end);
if new_x_range == self.x_range && new_z_range == self.z_range {
return;
}
let new_width = range_width(&new_x_range);
let new_depth = range_width(&new_z_range);
let old_width = self.width();
let old_depth = self.depth();
let shift_x = (self.x_range.start - new_x_range.start) as u32;
let shift_z = (self.z_range.start - new_z_range.start) as u32;
let old = mem::replace(&mut self.content, vec![0; (new_width * new_depth) as usize]);
for abs_z in 0..old_depth {
let new_abs_z = shift_z + abs_z;
let abs_x = 0..old_width as usize;
let new_abs_x = advance_range(&abs_x, shift_x as usize);
let old_i = advance_range(&abs_x, (abs_z * old_width) as usize);
let new_i = advance_range(&new_abs_x, (new_abs_z * new_width) as usize);
self.content[new_i].copy_from_slice(&old[old_i]);
}
self.x_range = new_x_range;
self.z_range = new_z_range;
}
pub fn adjust_for_placement(&mut self, x: i32, z: i32) {
// Start x,z ranges: 0..0
let new_x_range = adjust_range(x, &self.x_range);
let new_z_range = adjust_range(z, &self.z_range);
self.reserve(new_x_range, new_z_range);
}
fn index(&self, x: i32, z: i32) -> usize {
assert!(self.x_range.contains(&x));
assert!(self.z_range.contains(&z));
let abs_x = (x - self.x_range.start) as usize;
let abs_z = (z - self.z_range.start) as usize;
abs_x + abs_z * self.width() as usize
}
}
impl FeatureBuilder {
pub fn new() -> Self {
Self {
y_range: 0..0,
layers: vec![],
}
}
pub fn height(&self) -> u32 {
(self.y_range.end - self.y_range.start) as u32
}
pub fn reserve_height(&mut self, y_range: Range<i32>) {
let new_y_range = y_range.start.min(self.y_range.start)..y_range.end.max(self.y_range.end);
if new_y_range == self.y_range {
return;
}
let new_height = range_width(&new_y_range);
let shift_y = (self.y_range.start - new_y_range.start) as usize;
// -1..3 -> -2..3, shift_y = 1
// [??, -1, 0, 1, 2]
// [-2, -1, 0, 1, 2]
self.layers
.resize_with(new_height as usize, FeatureLayer::new);
self.layers.rotate_right(shift_y);
self.y_range = new_y_range;
}
pub fn adjust_height_for_placement(&mut self, y: i32) {
let new_y_range = adjust_range(y, &self.y_range);
self.reserve_height(new_y_range);
}
pub fn set(&mut self, x: i32, y: i32, z: i32, id: u8) {
self.adjust_height_for_placement(y);
let i = (y - self.y_range.start) as usize;
self.layers[i].set(x, z, id);
}
pub fn replace(&mut self, x: i32, y: i32, z: i32, from: u8, to: u8) {
self.adjust_height_for_placement(y);
let i = (y - self.y_range.start) as usize;
self.layers[i].replace(x, z, from, to);
}
pub fn get(&self, x: i32, y: i32, z: i32) -> u8 {
assert!(self.y_range.contains(&y));
self.layers[(y - self.y_range.start) as usize].get(x, z)
}
pub fn build(self, filter: FeaturePlacementFilter) -> Feature {
assert!(!self.layers.is_empty());
let x_range = largest_range_bounds(self.layers.iter().map(|l| &l.x_range));
let z_range = largest_range_bounds(self.layers.iter().map(|l| &l.z_range));
let width = range_width(&x_range);
let depth = range_width(&z_range);
let height = self.layers.len() as u32;
let mut content = vec![0; (width * depth) as usize * self.layers.len()];
for (y, layer) in self.layers.into_iter().enumerate() {
for x in x_range.clone() {
for z in z_range.clone() {
let id = layer.try_get(x, z).unwrap_or(0);
let ix = (x - x_range.start) as usize;
let iz = (z - z_range.start) as usize;
let i = ix + (iz + y * depth as usize) * width as usize;
content[i] = id;
}
}
}
Feature {
width,
height,
depth,
content,
filter,
}
}
}
fn largest_range_bounds<'a, I: IntoIterator<Item = &'a Range<i32>>>(ranges: I) -> Range<i32> {
let mut result = 0..0;
for range in ranges {
if range.start < result.start {
result.start = range.start;
}
if range.end > result.end {
result.end = range.end;
}
}
result
}
fn advance_range<T: Copy + Add<Output = T>>(range: &Range<T>, x: T) -> Range<T> {
range.start + x..range.end + x
}
#[inline]
fn adjust_range(x: i32, axis: &Range<i32>) -> Range<i32> {
let mut new_x = axis.clone();
if x < axis.start {
new_x.start = x;
} else if x >= axis.end {
new_x.end = x + 1;
}
new_x
}
#[inline]
fn range_width(axis: &Range<i32>) -> u32 {
assert!(axis.end >= axis.start);
(axis.end - axis.start) as u32
}
#[cfg(test)]
mod tests {
use super::{FeatureBuilder, FeatureLayer};
#[test]
fn featuer_build() {
let mut builder = FeatureBuilder::new();
builder.set(0, 0, 0, 1);
builder.set(0, 1, 0, 1);
for y in 2..5 {
for x in -1..=1 {
for z in -1..=1 {
let id = if x == 0 && z == 0 { 3 } else { 2 };
builder.set(x, y, z, id);
}
}
}
// Should yield a 3x5x3 feature
let feature = builder.build();
assert_eq!(feature.width, 3);
assert_eq!(feature.height, 5);
assert_eq!(feature.depth, 3);
assert_eq!(
&feature.content[..],
&[
// y = 0
0, 0, 0, // z = -1
0, 1, 0, // z = 0
0, 0, 0, // z = 1
// y = 1
0, 0, 0, // z = -1
0, 1, 0, // z = 0
0, 0, 0, // z = 1
// y = 2
2, 2, 2, // z = -1
2, 3, 2, // z = 0
2, 2, 2, // z = 1
// y = 3
2, 2, 2, // z = -1
2, 3, 2, // z = 0
2, 2, 2, // z = 1
// y = 4
2, 2, 2, // z = -1
2, 3, 2, // z = 0
2, 2, 2, // z = 1
]
);
}
#[test]
fn feature_layer_resize() {
let mut layer = FeatureLayer::new();
assert_eq!(layer.size(), (0, 0));
// 0x0 -> 1x1
layer.set(0, 0, 1);
assert_eq!(layer.size(), (1, 1));
assert_eq!(layer.get(0, 0), 1);
// 1x1 -> 3x1
layer.set(-2, 0, 2);
assert_eq!(layer.size(), (3, 1));
assert_eq!(layer.get(0, 0), 1);
assert_eq!(layer.get(-1, 0), 0);
assert_eq!(layer.get(-2, 0), 2);
// 3x1 -> 3x2
layer.set(-1, -1, 3);
assert_eq!(layer.size(), (3, 2));
assert_eq!(
&layer.content[..],
&[
0, 3, 0, // z = -1
2, 0, 1, // z = 0
]
);
// 3x2 -> 4x4
layer.set(-3, -3, 4);
assert_eq!(layer.size(), (4, 4));
assert_eq!(
&layer.content[..],
&[
4, 0, 0, 0, // z = -3
0, 0, 0, 0, // z = -2
0, 0, 3, 0, // z = -1
0, 2, 0, 1, // z = 0
]
);
// 4x4 -> 5x6
layer.set(1, 2, 5);
assert_eq!(layer.size(), (5, 6));
assert_eq!(
&layer.content[..],
&[
4, 0, 0, 0, 0, // z = -3
0, 0, 0, 0, 0, // z = -2
0, 0, 3, 0, 0, // z = -1
0, 2, 0, 1, 0, // z = 0
0, 0, 0, 0, 0, // z = 1
0, 0, 0, 0, 5, // z = 2
]
);
}
}
+3 -2
View File
@@ -17,7 +17,7 @@ impl ChunkGenerator for NoiseChunkGenerator {
) {
let grass = block_registry.get("grass").unwrap_or(0);
let dirt = block_registry.get("dirt").unwrap_or(0);
let stone = block_registry.get("stone").unwrap_or(0);
// let stone = block_registry.get("stone").unwrap_or(0);
for ix in 0..Chunk::SIZE as u32 {
for iz in 0..Chunk::SIZE as u32 {
@@ -37,7 +37,8 @@ impl ChunkGenerator for NoiseChunkGenerator {
let id = if y + 1 == h {
grass
} else if h - y > stone_depth {
stone
0
// stone
} else {
dirt
};
+226 -18
View File
@@ -1,20 +1,95 @@
use std::collections::HashMap;
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use glam::Vec3;
use crate::render::asset::block::BlockRegistry;
use super::{Chunk, ChunkCoords, ChunkGenerator};
use super::{
feature::{FeatureBuilder, FeaturePlacementFilter, PositionedFeature},
Chunk, ChunkCoords, ChunkGenerator,
};
pub struct World {
chunks: HashMap<ChunkCoords, Chunk>,
block_registry: BlockRegistry,
// populated_chunk_features: HashSet<ChunkCoords>,
unpopulated_chunks: HashSet<ChunkCoords>,
pending_chunk_features: HashMap<ChunkCoords, Vec<Arc<PositionedFeature>>>,
}
#[derive(Clone, Copy, Debug)]
pub struct BoundingBox {
pub x: i32,
pub y: i32,
pub z: i32,
pub w: i32,
pub h: i32,
pub d: i32,
}
#[derive(Clone, Copy, Debug)]
pub struct InChunkBoundingBox {
pub x: u32,
pub y: u32,
pub z: u32,
pub w: u32,
pub h: u32,
pub d: u32,
}
impl BoundingBox {
pub fn map_chunks<F: FnMut(ChunkCoords)>(self, mut f: F) {
let start = ChunkCoords::from_block_coords(self.x, self.z);
let end = ChunkCoords::from_block_coords(
self.x + self.w + Chunk::SIZE as i32 - 1,
self.z + self.d + Chunk::SIZE as i32 - 1,
);
for cx in start.x..end.x {
for cz in start.z..end.z {
f(ChunkCoords::new(cx, cz));
}
}
}
pub fn in_chunk(self, chunk: ChunkCoords) -> Option<InChunkBoundingBox> {
let cx = chunk.x * Chunk::SIZE as i32;
let cz = chunk.z * Chunk::SIZE as i32;
let sx = (self.x - cx).clamp(0, Chunk::SIZE as i32) as u32;
let sz = (self.z - cz).clamp(0, Chunk::SIZE as i32) as u32;
let dx = (self.x + self.w - cx).clamp(0, Chunk::SIZE as i32) as u32;
let dz = (self.z + self.d - cz).clamp(0, Chunk::SIZE as i32) as u32;
let sy = self.y.clamp(0, Chunk::HEIGHT as i32) as u32;
let dy = (self.y + self.h).clamp(0, Chunk::HEIGHT as i32) as u32;
if sx < dx && sy < dy && sz < dz {
Some(InChunkBoundingBox {
x: sx,
y: sy,
z: sz,
w: dx - sx,
h: dy - sy,
d: dz - sz,
})
} else {
None
}
}
}
impl World {
pub fn new(block_registry: BlockRegistry) -> Self {
Self {
chunks: HashMap::new(),
pending_chunk_features: HashMap::new(),
unpopulated_chunks: HashSet::new(),
block_registry,
}
}
@@ -25,36 +100,169 @@ impl World {
let mut chunk = Chunk::empty();
g.generate_terrain(coords, &mut chunk, &self.block_registry);
// Populate chunk with pending features
if let Some(pending) = self.pending_chunk_features.remove(&coords) {
for pending in pending {
pending.place(coords, &mut chunk);
}
}
self.unpopulated_chunks.insert(coords);
self.chunks.insert(coords, chunk);
}
fn populate_terrain<G: ChunkGenerator + ?Sized>(&mut self, coords: ChunkCoords, g: &mut G) {
if !self.chunks.contains_key(&coords) {
self.generate(coords, g);
}
}
fn populate_terrain_at<G: ChunkGenerator + ?Sized>(
&mut self,
center: ChunkCoords,
radius: i32,
g: &mut G,
) {
for cx in -radius..=radius {
for cz in -radius..=radius {
let coords = ChunkCoords::new(center.x + cx, center.z + cz);
self.populate_terrain(coords, g);
}
}
}
fn place_feature(&mut self, feature: Arc<PositionedFeature>) {
let aabb = feature.aabb();
aabb.map_chunks(|coords| {
if let Some(chunk) = self.chunks.get_mut(&coords) {
feature.place(coords, chunk);
} else {
let pending = self.pending_chunk_features.entry(coords).or_default();
pending.push(feature.clone());
}
});
}
fn populate_features(&mut self) {
let mut features = vec![];
let log = self.block_registry.get("log").unwrap_or(0);
let leaves = self.block_registry.get("leaves").unwrap_or(0);
for coords in self.unpopulated_chunks.drain() {
let Some(chunk) = self.chunks.get_mut(&coords) else {
continue;
};
// For each unpopulated chunk, generate a set of per-chunk features
let trees = rand::random_range(3..8);
for _ in 0..trees {
// Generate trees
// TODO collision detection for these
let trunk_height = rand::random_range(5..8);
let leaves_radius = trunk_height / 2 + rand::random_range(0..2);
let fx = rand::random_range(0..Chunk::SIZE as u32);
let fz = rand::random_range(0..Chunk::SIZE as u32);
let fy = chunk.top_height_at(fx, fz);
let mut builder = FeatureBuilder::new();
for y in 0..trunk_height {
builder.set(0, y, 0, log);
}
for x in -leaves_radius..=leaves_radius {
for y in -leaves_radius..=leaves_radius {
for z in -leaves_radius..=leaves_radius {
let p = Vec3::new(x as f32, y as f32, z as f32);
if p.distance(Vec3::ZERO) > leaves_radius as f32 - 0.1 {
continue;
}
builder.replace(
x,
y + trunk_height / 2 + leaves_radius / 2 + 1,
z,
0,
leaves,
);
}
}
}
let (root_x, root_z) = coords.block_coords(fx, fz);
features.push(Arc::new(
builder
.build(FeaturePlacementFilter::ReplaceAir)
.position_centered_horizontally(root_x, fy as i32, root_z),
));
}
}
// Place features
for feature in features {
self.place_feature(feature);
}
}
pub fn get(&self, coords: ChunkCoords) -> Option<&Chunk> {
self.chunks.get(&coords)
}
pub fn get_mut(&mut self, coords: ChunkCoords) -> Option<&mut Chunk> {
self.chunks.get_mut(&coords)
}
pub fn update_with_camera<G: ChunkGenerator + ?Sized>(
&mut self,
camera_position: Vec3,
g: &mut G,
) {
const VIEW_DISTANCE: i32 = 5;
const VIEW_DISTANCE: i32 = 1;
fn cpos(x: f32) -> i32 {
(x as i32 + Chunk::SIZE as i32 / 2) / Chunk::SIZE as i32
}
let camera_chunk =
ChunkCoords::from_block_coords(camera_position.x as i32, camera_position.z as i32);
let camera_cx = cpos(camera_position.x);
let camera_cz = cpos(camera_position.z);
for cx in camera_cx - VIEW_DISTANCE..=camera_cx + VIEW_DISTANCE {
for cz in camera_cz - VIEW_DISTANCE..=camera_cz + VIEW_DISTANCE {
let coords = ChunkCoords::new(cx, cz);
if !self.chunks.contains_key(&coords) {
self.generate(coords, g);
}
}
}
self.populate_terrain_at(camera_chunk, VIEW_DISTANCE, g);
self.populate_features();
// self.populate_features_at(camera_chunk, VIEW_DISTANCE);
}
}
#[cfg(test)]
mod tests {
use crate::world::ChunkCoords;
use super::BoundingBox;
#[test]
fn test_bounding_box_chunks() {
let aabb = BoundingBox {
x: -1,
y: 31,
z: 15,
w: 3,
h: 10,
d: 3,
};
let mut v0 = false;
let mut v1 = false;
aabb.map_chunks(|coords| {
if coords == ChunkCoords::new(-1, 0) {
v0 = true;
} else if coords == ChunkCoords::new(0, 0) {
v1 = true;
} else {
panic!("Unexpected chunk coords: {coords:?}");
}
});
assert!(v0 && v1);
}
}
+1
View File
@@ -1,6 +1,7 @@
use bitflags::bitflags;
pub mod chunk;
pub mod feature;
pub mod generator;
pub mod level;