diff --git a/Cargo.toml b/Cargo.toml index 7a5c1d9..4c6d95c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" image = "0.25.6" rand = "0.9.2" rand_chacha = "0.9.0" +nbt = { git = "https://github.com/Cactus-minecraft-server/nbt.git" } [lib] name = "worldgen" path = "src/lib.rs" diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 3d3302e..0000000 --- a/TODO.md +++ /dev/null @@ -1,69 +0,0 @@ -# TODO List for a Minecraft-Like Terrain Generator in Rust - -## 1. Define Objectives and Requirements -- **Features:** - - Global terrain generation (overall elevation) - - Local detail (surface variations) - - Biome transitions and optionally cave systems -- **Technical Constraints:** - - Chunk dimensions (e.g., 16×16 blocks horizontally with a fixed vertical height) - - Memory management (chunk caching, on-demand generation) - - Server integration (protocol, networking, etc.) -- **Output Format:** - - How to represent the world (e.g., a 3D array of block types) - - Block types (Air, Grass, Dirt, Stone, Water, etc.) - -## 2. Research and Select Noise Algorithms -- Study noise algorithms such as Perlin and Simplex. -- Understand Fractal Brownian Motion (fBm) to combine multiple octaves. -- Define parameters like frequency, amplitude, persistence, number of octaves, and scaling factors. - -## 3. Set Up Your Rust Project -- Create a new project with Cargo. -- Add necessary dependencies in `Cargo.toml` (e.g., the `noise` crate). -- Set up version control (Git). - -## 4. Implement Basic Noise Generation -- Write a simple prototype to generate noise values. -- Use a scaling factor to avoid sampling only on integer coordinates. -- Test with a fixed seed for reproducibility. - -## 5. Implement Fractal Brownian Motion (fBm) -- Create a function to combine multiple noise octaves. -- Adjust parameters (octaves, persistence, etc.) and test the results. - -## 6. Map Noise to Terrain Height -- Convert normalized noise values (e.g., from -1 to 1) to block heights. -- Define a mapping strategy (for example, scaling to a maximum height). - -## 7. Design the Chunk Data Structure -- Decide on chunk dimensions (e.g., 16×16×128). -- Create a simple structure to represent blocks (using enums or similar). - -## 8. Generate Chunks Based on Noise -- For each (x, z) coordinate in a chunk: - - Calculate the noise value. - - Map it to a terrain height. - - Fill in blocks based on the height (e.g., surface, sub-surface, stone). -- Keep the code modular and avoid overcomplicating early on. - -## 9. Test and Visualize the Generated Terrain -- Write unit tests for noise functions and terrain mapping. -- Create a simple visualization (e.g., a 2D height map printed to the console or exporting data for external tools). -- Verify that parameter adjustments produce the expected variations. - -## 10. Integrate the Generator into Your Server Architecture -- Implement on-demand chunk generation as the player moves. -- Cache generated chunks (in memory or on disk) to avoid re-computation. -- Consider multithreading or asynchronous processing for parallel generation. - -## 11. Optimize and Refine -- Profile the terrain generation for performance bottlenecks. -- Fine-tune noise parameters and mapping logic. -- Plan future enhancements (biomes, caves, advanced block types). - -## 12. Document and Maintain the Codebase -- Document your functions, parameters, and overall architecture. -- Use version control to track changes and manage iterative improvements. -- Keep your code modular for easy future enhancements. - diff --git a/src/level.rs b/src/level.rs new file mode 100644 index 0000000..df490ed --- /dev/null +++ b/src/level.rs @@ -0,0 +1,80 @@ +use std::fs::File; + +use nbt::{Tag, write_nbt}; + +pub fn create_nbt( + name: String, + seed: i64, + is_hardcore: bool, + structures: bool, + raining: bool, + thundering: bool, + game_type: i32, + generator_version: i32, + raintime: i32, + spawnx: i32, + spawny: i32, + spawnz: i32, + thundertime: i32, + version: i32, + last_played: i64, + disk_size: i64, + time: i64, + generator_name: String, + level_name: String, + path: String, +) -> std::io::Result<()> { + let mut root = Tag::new_compound(name); + root.insert("RandomSeed".to_string(), Tag::new_long("RandomSeed", seed)); + root.insert( + "hardcore".to_string(), + Tag::new_byte("hardcore", i8::from(is_hardcore)), + ); + root.insert( + "MapFeatures".to_string(), + Tag::new_byte("MapFeatures", i8::from(structures)), + ); + root.insert( + "raining".to_string(), + Tag::new_byte("raining", i8::from(raining)), + ); + root.insert( + "thundering".to_string(), + Tag::new_byte("thundering", i8::from(thundering)), + ); + root.insert("GameType".to_string(), Tag::new_int("GameType", game_type)); + root.insert( + "GeneratorVersion".to_string(), + Tag::new_int("GeneratorVersion", generator_version), + ); + root.insert("RainTime".to_string(), Tag::new_int("RainTime", raintime)); + root.insert("SpawnX".to_string(), Tag::new_int("SpawnX", spawnx)); + root.insert("SpawnY".to_string(), Tag::new_int("SpawnY", spawny)); + root.insert("SpawnZ".to_string(), Tag::new_int("SpawnZ", spawnz)); + root.insert( + "ThunderTime".to_string(), + Tag::new_int("ThunderTime", thundertime), + ); + root.insert("Version".to_string(), Tag::new_int("Version", version)); + root.insert( + "LastPlayed".to_string(), + Tag::new_long("LastPlayed", last_played), + ); // Not sure if I should keep LastPlayed field because of the fact that this isn't for a + // client world but for a server world + root.insert( + "SizeOnDisk".to_string(), + Tag::new_long("SizeOnDisk", disk_size), + ); + root.insert("Time".to_string(), Tag::new_long("Time", time)); + root.insert( + "GeneratorName".to_string(), + Tag::new_string("GeneratorName", generator_name), + ); + root.insert( + "LevelName".to_string(), + Tag::new_string("LevelName", level_name), + ); + let file = File::create(path)?; + write_nbt(&root, file)?; + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 938a619..ea0ff4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,5 @@ +mod level; mod perlin; mod superflat; +#[cfg(test)] +mod test; diff --git a/src/perlin.rs b/src/perlin.rs index 7420328..215be6d 100644 --- a/src/perlin.rs +++ b/src/perlin.rs @@ -4,7 +4,7 @@ use rand::{ }; use rand_chacha::ChaCha8Rng; -const CHUNK_SIZE: usize = 16; +pub const CHUNK_SIZE: usize = 16; #[derive(Clone, Copy, Debug, PartialEq)] pub struct Noise { scale: f32, @@ -18,9 +18,9 @@ pub struct Vector { y: f32, } -const MIN_Y: i32 = -64; -const MAX_Y: i32 = 320; -const SEA_LEVEL: i32 = 63; +pub const MIN_Y: i32 = -64; +pub const MAX_Y: i32 = 320; +pub const SEA_LEVEL: i32 = 63; #[inline] fn fbm_seeded( @@ -211,13 +211,15 @@ fn linear_interpolation(a: f32, b: f32, t: f32) -> f32 { #[cfg(test)] mod tests { + use crate::perlin::{ + Noise, Vector, calculate_norm, dot_product, fade, linear_interpolation, normalize, + }; + fn deriv(f: fn(f32) -> f32, x: f32) -> f32 { let h = 1e-3; (f(x + h) - f(x - h)) / (2.0 * h) } - use super::*; - fn approx_eq(a: f32, b: f32, eps: f32) -> bool { (a - b).abs() <= eps } @@ -436,49 +438,3 @@ mod perlin_tests { assert!((n.get(x, y + h) - c0).abs() < 0.1); } } - -#[cfg(test)] -mod viz_chunk2 { - use super::*; - use image::{ImageBuffer, Luma}; - - #[test] - #[ignore] - fn dump_chunk_png() { - let seed: u64 = 42; - let chunks_x: usize = 32; - let chunks_z: usize = 32; - - let w: u32 = (chunks_x * CHUNK_SIZE) as u32; - let h: u32 = (chunks_z * CHUNK_SIZE) as u32; - - let mut field = vec![0i32; (w as usize) * (h as usize)]; - - for cz in 0..chunks_z { - for cx in 0..chunks_x { - let tile = generate_height_chunk(seed, cx as i32, cz as i32); - for lz in 0..CHUNK_SIZE { - for lx in 0..CHUNK_SIZE { - let x = cx * CHUNK_SIZE + lx; - let z = cz * CHUNK_SIZE + lz; - field[z * (w as usize) + x] = tile[lx][lz]; - } - } - } - } - - let mut img: ImageBuffer, Vec> = ImageBuffer::new(w, h); - let denom = (MAX_Y - MIN_Y) as f32; - for z in 0..h { - for x in 0..w { - let v = field[(z as usize) * (w as usize) + (x as usize)]; - let n01 = ((v - MIN_Y) as f32 / denom).clamp(0.0, 1.0); - let p = (n01 * u16::MAX as f32).round() as u16; - img.put_pixel(x, z, Luma([p])); - } - } - - std::fs::create_dir_all("target").ok(); - img.save("target/heightmap16.png").unwrap(); - } -} diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/region.rs b/src/region.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/test.rs b/src/test.rs new file mode 100644 index 0000000..96b1afa --- /dev/null +++ b/src/test.rs @@ -0,0 +1,78 @@ +/// Test for perlin.rs +#[cfg(test)] +mod perlin_test { + use crate::perlin::{CHUNK_SIZE, MAX_Y, MIN_Y, generate_height_chunk}; + + use image::{ImageBuffer, Luma}; + + #[test] + #[ignore] + fn dump_chunk_png() { + let seed: u64 = 42; + let chunks_x: usize = 32; + let chunks_z: usize = 32; + + let w: u32 = (chunks_x * CHUNK_SIZE) as u32; + let h: u32 = (chunks_z * CHUNK_SIZE) as u32; + + let mut field = vec![0i32; (w as usize) * (h as usize)]; + + for cz in 0..chunks_z { + for cx in 0..chunks_x { + let tile = generate_height_chunk(seed, cx as i32, cz as i32); + for lz in 0..CHUNK_SIZE { + for lx in 0..CHUNK_SIZE { + let x = cx * CHUNK_SIZE + lx; + let z = cz * CHUNK_SIZE + lz; + field[z * (w as usize) + x] = tile[lx][lz]; + } + } + } + } + + let mut img: ImageBuffer, Vec> = ImageBuffer::new(w, h); + let denom = (MAX_Y - MIN_Y) as f32; + for z in 0..h { + for x in 0..w { + let v = field[(z as usize) * (w as usize) + (x as usize)]; + let n01 = ((v - MIN_Y) as f32 / denom).clamp(0.0, 1.0); + let p = (n01 * u16::MAX as f32).round() as u16; + img.put_pixel(x, z, Luma([p])); + } + } + + std::fs::create_dir_all("target").ok(); + img.save("target/heightmap16.png").unwrap(); + } +} +/// Test for level.rs +#[cfg(test)] +mod level_file_test { + use crate::level::create_nbt; + #[test] + fn test_creation_of_file() -> () { + let result = create_nbt( + "test".to_string(), + 1234, + true, + true, + true, + true, + 1, + 1, + 1, + 0, + 0, + 0, + 213, + 1235, + 55555, + 90000, + 900, + "test".to_string(), + "test".to_string(), + "target/level.dat".to_string(), + ); + assert_eq!(result.is_ok(), true); + } +}