From aeb0ad348f38ade864cbede289d6749e51675b7a Mon Sep 17 00:00:00 2001 From: p2r3 Date: Wed, 20 Aug 2025 22:35:26 +0300 Subject: [PATCH] add biomes --- build_registries.js | 79 ++++++++++++- include/tools.h | 2 + include/worldgen.h | 8 +- src/worldgen.c | 276 ++++++++++++++++++++++++++++++++++---------- 4 files changed, 294 insertions(+), 71 deletions(-) diff --git a/build_registries.js b/build_registries.js index 578c562..6ae45b4 100644 --- a/build_registries.js +++ b/build_registries.js @@ -4,16 +4,54 @@ const path = require("path"); // Overrides for block-to-item conversion const blockToItemOverrides = { "grass_block": "dirt", + "snowy_grass_block": "dirt", "stone": "cobblestone", "diamond_ore": "diamond", "gold_ore": "raw_gold", "redstone_ore": "redstone", "iron_ore": "raw_iron", - "coal_ore": "coal" + "coal_ore": "coal", + "snow": "snowball", + "dead_bush": "stick" }; +// Blacklisted block name strings +const blockBlacklist = [ + "spruce_", + "birch_", + "jungle_", + "acacia_", + "dark_oak_", + "mangrove_", + "cherry_", + "pale_oak_", + "crimson_", + "warped_", + "bamboo_", + "deepslate", + "infested_", + "stained_", + "_terracotta", + "_head" +]; + +// Whitelisted blocks, i.e. guaranteed to be included +const blockWhitelist = [ + "air", + "water", + "lava", + "snowy_grass_block", + "mud", + "moss_carpet" +]; + +// Currently, only 4 biome types are supported, excluding "beach" const biomes = [ - "plains" + "plains", + "mangrove_swamp", + "desert", + "snowy_plains", + "beach" ]; // Extract item and block data from registry dump @@ -46,7 +84,22 @@ async function extractItemsAndBlocks () { for (const entry of sortedBlocks) { const defaultState = entry[1].states.find(c => c.default); if (!defaultState) continue; + // Check if a part of this block's name is in the blacklist + let found = false; + for (const str of blockBlacklist) { + if (entry[0].includes(str)) { + found = true; + break; + } + } + if (found) continue; + // Register the block ID blocks[entry[0].replace("minecraft:", "")] = defaultState.id; + // Include "snowy" variants of blocks as well + if ("properties" in defaultState && "snowy" in defaultState.properties) { + const snowyState = entry[1].states.find(c => c.properties.snowy); + blocks["snowy_" + entry[0].replace("minecraft:", "")] = snowyState.id; + } } for (const item in itemSource) { @@ -60,13 +113,22 @@ async function extractItemsAndBlocks () { * items, outside of some exceptions. */ const palette = {}; - const exceptions = [ "air", "water", "lava" ]; // While we're at it, map block IDs to item IDs const mapping = [], mappingWithOverrides = []; + // Handle explicitly whitelisted blocks first + for (const block of blockWhitelist) { + palette[block] = blocks[block]; + mapping.push(items[block] || 0); + mappingWithOverrides.push(items[blockToItemOverrides[block]] || items[block] || 0); + if (mapping.length === 256) break; + } + + // Continue adding blocks with matching items for (const block in blocks) { - if (!(block in items) && !exceptions.includes(block)) continue; + if (!(block in items)) continue; + if (blockWhitelist.includes(block)) continue; palette[block] = blocks[block]; mapping.push(items[block] || 0); mappingWithOverrides.push(items[blockToItemOverrides[block]] || items[block] || 0); @@ -234,7 +296,7 @@ async function convert () { const inputPath = __dirname + "/notchian/generated/data/minecraft"; const outputPath = __dirname + "/src/registries.c"; - const headerPath = __dirname + "/src/registries.h"; + const headerPath = __dirname + "/include/registries.h"; const registries = await scanDirectory(inputPath); const registryBuffers = []; @@ -267,6 +329,8 @@ async function convert () { "mineable/pickaxe": [ itemsAndBlocks.blockRegistry["stone"], itemsAndBlocks.blockRegistry["cobblestone"], + itemsAndBlocks.blockRegistry["sandstone"], + itemsAndBlocks.blockRegistry["ice"], itemsAndBlocks.blockRegistry["diamond_ore"], itemsAndBlocks.blockRegistry["gold_ore"], itemsAndBlocks.blockRegistry["redstone_ore"], @@ -281,7 +345,10 @@ async function convert () { ], "mineable/shovel": [ itemsAndBlocks.blockRegistry["grass_block"], - itemsAndBlocks.blockRegistry["dirt"] + itemsAndBlocks.blockRegistry["dirt"], + itemsAndBlocks.blockRegistry["sand"], + itemsAndBlocks.blockRegistry["snow"], + itemsAndBlocks.blockRegistry["mud"] ], }, "item": { diff --git a/include/tools.h b/include/tools.h index b5a6ee6..c4fc428 100644 --- a/include/tools.h +++ b/include/tools.h @@ -6,6 +6,8 @@ #include "globals.h" +#define mod_abs(a, b) ((a % b + b) % b) + ssize_t recv_all (int client_fd, void *buf, size_t n, uint8_t require_first); ssize_t send_all (int fd, const void *buf, size_t len); diff --git a/include/worldgen.h b/include/worldgen.h index a684441..b062d47 100644 --- a/include/worldgen.h +++ b/include/worldgen.h @@ -9,15 +9,21 @@ #define TERRAIN_BASE_HEIGHT 60 // Center point of cave generation #define CAVE_BASE_DEPTH 24 +// Size of every major biome in multiples of CHUNK_SIZE +// For best performance, should also be a power of 2 +#define BIOME_SIZE 64 +#define BIOME_RADIUS (BIOME_SIZE / 2) typedef struct { short x; short z; uint32_t hash; + uint8_t biome; } ChunkAnchor; uint32_t getChunkHash (short x, short z); -int getHeightAt (int rx, int rz, int _x, int _z, uint32_t chunk_hash); +uint8_t getChunkBiome (short x, short z); +int getHeightAt (int rx, int rz, int _x, int _z, uint32_t chunk_hash, uint8_t biome); uint8_t getTerrainAt (int x, int y, int z, ChunkAnchor anchor); uint8_t getBlockAt (int x, int y, int z); diff --git a/src/worldgen.c b/src/worldgen.c index 73ca521..5cf1295 100644 --- a/src/worldgen.c +++ b/src/worldgen.c @@ -20,21 +20,97 @@ uint32_t getChunkHash (short x, short z) { } -int getCornerHeight (uint32_t hash) { +uint8_t getChunkBiome (short x, short z) { - // Use parts of the hash as random values for the height variation. - // We stack multiple different numbers to stabilize the distribution - // while allowing for occasional variances. - int height = TERRAIN_BASE_HEIGHT + ( - (hash & 3) + - (hash >> 4 & 3) + - (hash >> 8 & 3) + - (hash >> 12 & 3) - ); + // Center biomes on 0;0 + x += BIOME_RADIUS; + z += BIOME_RADIUS; - // If height dips below sea level, push it down further - // This selectively makes bodies of water larger and deeper - if (height < 64) height -= (hash >> 24) & 7; + // Calculate "biome coordinates" (one step above chunk coordinates) + // The pattern repeats every 4 biomes, so the coordinate range is [0;3] + uint8_t _x = mod_abs(x / BIOME_SIZE, 16) & 3; + uint8_t _z = mod_abs(z / BIOME_SIZE, 16) & 3; + // To prevent obvious mirroring, invert values on negative axes + if (x < 0) _x = 3 - _x; + if (z < 0) _z = 3 - _z; + + // Calculate distance from biome center + int8_t dx = BIOME_RADIUS - mod_abs(x, BIOME_SIZE); + int8_t dz = BIOME_RADIUS - mod_abs(z, BIOME_SIZE); + // Each biome is a circular island, with beaches in-between + // Determine whether the given chunk is within the island + if (dx * dx + dz * dz > BIOME_RADIUS * BIOME_RADIUS) return W_beach; + + // Finally, the biome itself is plucked from the world seed. + // The 32-bit seed is treated as a 4x4 biome matrix, with each biome + // taking up 2 bytes. This is why there are only 4 biomes, excluding + // beaches. Using the world seed as a repeating pattern avoids + // having to generate and layer yet another hash. + return (world_seed >> (_x + _z * 4)) & 3; + +} + +int getCornerHeight (uint32_t hash, uint8_t biome) { + + // When calculating the height, parts of the hash are used as random values. + // Often, multiple values are stacked to stabilize the distribution while + // allowing for occasionally larger variances. + int height; + + switch (biome) { + + case W_mangrove_swamp: { + height = TERRAIN_BASE_HEIGHT + ( + (hash % 3) + + ((hash >> 4) % 3) + + ((hash >> 8) % 3) + + ((hash >> 12) % 3) + ); + // If height dips below sea level, push it down further + // This ends up creating many large ponds of water + if (height < 64) height -= (hash >> 24) & 3; + break; + } + + case W_plains: { + height = TERRAIN_BASE_HEIGHT + ( + (hash & 3) + + (hash >> 4 & 3) + + (hash >> 8 & 3) + + (hash >> 12 & 3) + ); + break; + } + + case W_desert: { + height = TERRAIN_BASE_HEIGHT + 4 + ( + (hash & 3) + + (hash >> 4 & 3) + ); + break; + } + + case W_beach: { + // Start slightly below sea level to ensure it's all water + height = 62 - ( + (hash & 3) + + (hash >> 4 & 3) + + (hash >> 8 & 3) + ); + break; + } + + case W_snowy_plains: { + // Use fewer components with larger ranges to create hills + height = TERRAIN_BASE_HEIGHT + ( + (hash & 7) + + (hash >> 4 & 7) + ); + break; + } + + default: break; + } return height; @@ -46,17 +122,17 @@ int interpolate (int a, int b, int c, int d, int x, int z) { return (top * (CHUNK_SIZE - z) + bottom * z) / (CHUNK_SIZE * CHUNK_SIZE); } -int getHeightAt (int rx, int rz, int _x, int _z, uint32_t chunk_hash) { +int getHeightAt (int rx, int rz, int _x, int _z, uint32_t chunk_hash, uint8_t biome) { if (rx == 0 && rz == 0) { - int height = getCornerHeight(chunk_hash); + int height = getCornerHeight(chunk_hash, biome); if (height > 67) return height - 1; } return interpolate( - getCornerHeight(chunk_hash), - getCornerHeight(getChunkHash(_x + 1, _z)), - getCornerHeight(getChunkHash(_x, _z + 1)), - getCornerHeight(getChunkHash(_x + 1, _z + 1)), + getCornerHeight(chunk_hash, biome), + getCornerHeight(getChunkHash(_x + 1, _z), getChunkBiome(_x + 1, _z)), + getCornerHeight(getChunkHash(_x, _z + 1), getChunkBiome(_x, _z + 1)), + getCornerHeight(getChunkHash(_x + 1, _z + 1), getChunkBiome(_x + 1, _z + 1)), rx, rz ); @@ -71,54 +147,118 @@ uint8_t getTerrainAt (int x, int y, int z, ChunkAnchor anchor) { if (rx < 0) rx += CHUNK_SIZE; if (rz < 0) rz += CHUNK_SIZE; - int height = getHeightAt(rx, rz, anchor.x, anchor.z, anchor.hash); + int height = getHeightAt(rx, rz, anchor.x, anchor.z, anchor.hash, anchor.biome); - if (y >= 64 && y >= height) { + if (y < 64 || y < height) goto skip_feature; - uint8_t tree_position = anchor.hash % (CHUNK_SIZE * CHUNK_SIZE); + uint8_t feature_position = anchor.hash % (CHUNK_SIZE * CHUNK_SIZE); - short tree_x = tree_position % CHUNK_SIZE; - if (tree_x < 3 || tree_x > CHUNK_SIZE - 3) goto skip_tree; - short tree_z = tree_position / CHUNK_SIZE; - if (tree_z < 3 || tree_z > CHUNK_SIZE - 3) goto skip_tree; - - uint8_t tree_short = (anchor.hash >> (tree_x + tree_z)) & 1; - - tree_x += anchor.x * CHUNK_SIZE; - tree_z += anchor.z * CHUNK_SIZE; - - uint8_t tree_y = getHeightAt( - tree_x < 0 ? tree_x % CHUNK_SIZE + CHUNK_SIZE : tree_x % CHUNK_SIZE, - tree_z < 0 ? tree_z % CHUNK_SIZE + CHUNK_SIZE : tree_z % CHUNK_SIZE, - anchor.x, anchor.z, anchor.hash - ) + 1; - if (tree_y < 64) goto skip_tree; - - if (x == tree_x && z == tree_z) { - if (y == tree_y - 1) return B_dirt; - if (y >= tree_y && y < tree_y - tree_short + 6) return B_oak_log; - } - - uint8_t dx = x > tree_x ? x - tree_x : tree_x - x; - uint8_t dz = z > tree_z ? z - tree_z : tree_z - z; - - if (dx < 3 && dz < 3 && y > tree_y - tree_short + 2 && y < tree_y - tree_short + 5) { - if (y == tree_y - tree_short + 4 && dx == 2 && dz == 2) return B_air; - return B_oak_leaves; - } - if (dx < 2 && dz < 2 && y >= tree_y - tree_short + 5 && y <= tree_y - tree_short + 6) { - if (y == tree_y - tree_short + 6 && dx == 1 && dz == 1) return B_air; - return B_oak_leaves; - } - - if (y == height) return B_grass_block; - return B_air; + short feature_x = feature_position % CHUNK_SIZE; + short feature_z = feature_position / CHUNK_SIZE; + // The following check does two things: + // firstly, it ensures that trees don't cross chunk boundaries; + // secondly, it reduces overall feature count. This is favorable + // everywhere except for swamps, which are otherwise very boring. + if (anchor.biome != W_mangrove_swamp) { + if (feature_x < 3 || feature_x > CHUNK_SIZE - 3) goto skip_feature; + if (feature_z < 3 || feature_z > CHUNK_SIZE - 3) goto skip_feature; } -skip_tree: - // For surface-level terrain, generate grass blocks - if (y == height && height >= 63) return B_grass_block; + uint8_t feature_variant = (anchor.hash >> (feature_x + feature_z)) & 1; + + feature_x += anchor.x * CHUNK_SIZE; + feature_z += anchor.z * CHUNK_SIZE; + + switch (anchor.biome) { + case W_plains: { // Generate trees in the plains biome + + uint8_t feature_y = getHeightAt( + feature_x < 0 ? feature_x % CHUNK_SIZE + CHUNK_SIZE : feature_x % CHUNK_SIZE, + feature_z < 0 ? feature_z % CHUNK_SIZE + CHUNK_SIZE : feature_z % CHUNK_SIZE, + anchor.x, anchor.z, anchor.hash, anchor.biome + ) + 1; + if (feature_y < 64) break; + + if (x == feature_x && z == feature_z) { + if (y == feature_y - 1) return B_dirt; + if (y >= feature_y && y < feature_y - feature_variant + 6) return B_oak_log; + } + + uint8_t dx = x > feature_x ? x - feature_x : feature_x - x; + uint8_t dz = z > feature_z ? z - feature_z : feature_z - z; + + if (dx < 3 && dz < 3 && y > feature_y - feature_variant + 2 && y < feature_y - feature_variant + 5) { + if (y == feature_y - feature_variant + 4 && dx == 2 && dz == 2) break; + return B_oak_leaves; + } + if (dx < 2 && dz < 2 && y >= feature_y - feature_variant + 5 && y <= feature_y - feature_variant + 6) { + if (y == feature_y - feature_variant + 6 && dx == 1 && dz == 1) break; + return B_oak_leaves; + } + + if (y == height) return B_grass_block; + return B_air; + } + + case W_desert: { // Generate dead bushes and cacti in deserts + + if (x != feature_x || z != feature_z) break; + + if (feature_variant == 0) { + if (y == height + 1) return B_dead_bush; + } else if (y > height) { + // The size of the cactus is determined based on whether the terrain + // height is even or odd at the target location + if (height & 1 && y <= height + 3) return B_cactus; + if (y <= height + 2) return B_cactus; + } + + break; + + } + + case W_mangrove_swamp: { // Generate lilypads and moss carpets in swamps + + if (x == feature_x && z == feature_z && y == 64 && height < 63) { + return B_lily_pad; + } + + if (y == height + 1) { + uint8_t dx = x > feature_x ? x - feature_x : feature_x - x; + uint8_t dz = z > feature_z ? z - feature_z : feature_z - z; + if (dx + dz < 4) return B_moss_carpet; + } + + break; + } + + case W_snowy_plains: { // Generate grass stubs in snowy plains + + if (x == feature_x && z == feature_z && y == height + 1 && height >= 64) { + return B_short_grass; + } + + break; + } + + default: break; + } + +skip_feature: + // Handle surface-level terrain (the very topmost blocks) + if (height >= 63) { + if (y == height) { + if (anchor.biome == W_mangrove_swamp) return B_mud; + if (anchor.biome == W_snowy_plains) return B_snowy_grass_block; + if (anchor.biome == W_desert) return B_sand; + if (anchor.biome == W_beach) return B_sand; + return B_grass_block; + } + if (anchor.biome == W_snowy_plains && y == height + 1) { + return B_snow; + } + } // Starting at 4 blocks below terrain level, generate minerals and caves if (y <= height - 4) { // Caves use the same shape as surface terrain, just mirrored @@ -149,9 +289,15 @@ skip_tree: // For everything else, fall back to stone return B_stone; } - // Under water and in the space between stone and grass, generate dirt - if (y <= height) return B_dirt; - // If all else failed, but we're below sea level, generate water + // Handle the space between stone and grass + if (y <= height) { + if (anchor.biome == W_desert) return B_sandstone; + if (anchor.biome == W_mangrove_swamp) return B_mud; + if (anchor.biome == W_beach && height > 64) return B_sandstone; + return B_dirt; + } + // If all else failed, but we're below sea level, generate water (or ice) + if (y == 63 && anchor.biome == W_snowy_plains) return B_ice; if (y < 64) return B_water; // For everything else, fall back to air @@ -171,6 +317,7 @@ uint8_t getBlockAt (int x, int y, int z) { if (x % CHUNK_SIZE < 0) anchor.x --; if (z % CHUNK_SIZE < 0) anchor.z --; anchor.hash = getChunkHash(anchor.x, anchor.z); + anchor.biome = getChunkBiome(anchor.x, anchor.z); return getTerrainAt(x, y, z, anchor); @@ -191,6 +338,7 @@ void buildChunkSection (int cx, int cy, int cz) { anchor->x = j / CHUNK_SIZE; anchor->z = i / CHUNK_SIZE; anchor->hash = getChunkHash(anchor->x, anchor->z); + anchor->biome = getChunkBiome(anchor->x, anchor->z); anchor_index ++; }