#include "server/pch.hh" #include "server/overworld.hh" #include "shared/coord.hh" #include "shared/game_voxels.hh" #include "shared/voxel_storage.hh" // FIXME: load these from a file static void compute_tree_feature(unsigned int height, Feature &feature, voxel_id log_voxel, voxel_id leaves_voxel) { // Ensure the tree height is too small height = cxpr::max(height, 4U); // Generate tree stem for(unsigned int i = 0; i < height; ++i) { feature.push_back({ voxel_pos(0, i, 0), log_voxel }); } auto leaves_start = height - 3U; auto leaves_thick_end = height - 2U; auto leaves_thin_end = height - 1U; // Generate the thin 3x3 layer of leaves that // starts from leaves_start and ends at leaves_thin_end for(unsigned int i = leaves_start; i <= leaves_thin_end; ++i) { feature.push_back({ local_pos(-1, i, -1), leaves_voxel }); feature.push_back({ local_pos(-1, i, +0), leaves_voxel }); feature.push_back({ local_pos(-1, i, +1), leaves_voxel }); feature.push_back({ local_pos(+0, i, -1), leaves_voxel }); feature.push_back({ local_pos(+0, i, +1), leaves_voxel }); feature.push_back({ local_pos(+1, i, -1), leaves_voxel }); feature.push_back({ local_pos(+1, i, +0), leaves_voxel }); feature.push_back({ local_pos(+1, i, +1), leaves_voxel }); } // Generate the tree cap; a 3x3 patch of leaves // that is slapped right on top of the thin 3x3 layer feature.push_back({ local_pos(-1, height, +0), leaves_voxel }); feature.push_back({ local_pos(+0, height, -1), leaves_voxel }); feature.push_back({ local_pos(+0, height, +0), leaves_voxel }); feature.push_back({ local_pos(+0, height, +1), leaves_voxel }); feature.push_back({ local_pos(+1, height, +0), leaves_voxel }); // Generate the thin 5x5 layer of leaves that // starts from leaves_start and ends at leaves_thin_end for(unsigned int i = leaves_start; i <= leaves_thick_end; ++i) { feature.push_back({ local_pos(-1, i, -2), leaves_voxel }); feature.push_back({ local_pos(-1, i, +2), leaves_voxel }); feature.push_back({ local_pos(-2, i, -1), leaves_voxel }); feature.push_back({ local_pos(-2, i, -2), leaves_voxel }); feature.push_back({ local_pos(-2, i, +0), leaves_voxel }); feature.push_back({ local_pos(-2, i, +1), leaves_voxel }); feature.push_back({ local_pos(-2, i, +2), leaves_voxel }); feature.push_back({ local_pos(+0, i, -2), leaves_voxel }); feature.push_back({ local_pos(+0, i, +2), leaves_voxel }); feature.push_back({ local_pos(+1, i, -2), leaves_voxel }); feature.push_back({ local_pos(+1, i, +2), leaves_voxel }); feature.push_back({ local_pos(+2, i, -1), leaves_voxel }); feature.push_back({ local_pos(+2, i, -2), leaves_voxel }); feature.push_back({ local_pos(+2, i, +0), leaves_voxel }); feature.push_back({ local_pos(+2, i, +1), leaves_voxel }); feature.push_back({ local_pos(+2, i, +2), leaves_voxel }); } } Overworld::Overworld(const char *name) : Dimension(name, -30.0f) { m_bottommost_chunk.set_limits(-64, -4); m_terrain_variation.set_limits(16, 256); compute_tree_feature(5U, m_feat_tree, game_voxels::oak_log, game_voxels::oak_leaves); } void Overworld::init(Config &config) { m_terrain_variation.set_value(64); m_bottommost_chunk.set_value(-4); config.add_value("overworld.terrain_variation", m_terrain_variation); config.add_value("overworld.bottommost_chunk", m_bottommost_chunk); } void Overworld::init_late(std::uint64_t global_seed) { std::mt19937 twister(global_seed); m_fnl_terrain = fnlCreateState(); m_fnl_terrain.seed = static_cast(twister()); m_fnl_terrain.noise_type = FNL_NOISE_OPENSIMPLEX2S; m_fnl_terrain.fractal_type = FNL_FRACTAL_FBM; m_fnl_terrain.frequency = 0.005f; m_fnl_terrain.octaves = 4; m_fnl_caves_a = fnlCreateState(); m_fnl_caves_a.seed = static_cast(twister()); m_fnl_caves_a.noise_type = FNL_NOISE_PERLIN; m_fnl_caves_a.frequency = 0.0075f; m_fnl_caves_b = fnlCreateState(); m_fnl_caves_b.seed = static_cast(twister()); m_fnl_caves_b.noise_type = FNL_NOISE_PERLIN; m_fnl_caves_b.frequency = 0.0075f; m_metamap.clear(); } bool Overworld::generate(const chunk_pos &cpos, VoxelStorage &voxels) { if(cpos.y <= m_bottommost_chunk.get_value()) { // If the player asks the generator // to generate a lot of stuff below // the surface, it will happily chew // through all the server threads return false; } voxels.fill(NULL_VOXEL_ID); m_mutex.lock(); generate_terrain(cpos, voxels); m_mutex.unlock(); m_mutex.lock(); generate_surface(cpos, voxels); m_mutex.unlock(); m_mutex.lock(); generate_caves(cpos, voxels); m_mutex.unlock(); m_mutex.lock(); generate_features(cpos, voxels); m_mutex.unlock(); return true; } bool Overworld::is_inside_cave(const voxel_pos &vpos) { auto noise_a = fnlGetNoise3D(&m_fnl_caves_a, vpos.x, 1.5f * vpos.y, vpos.z); auto noise_b = fnlGetNoise3D(&m_fnl_caves_b, vpos.x, 1.5f * vpos.y, vpos.z); auto noise_combined = noise_a * noise_a + noise_b * noise_b; return noise_combined < (1.0f / 1024.0f); } bool Overworld::is_inside_terrain(const voxel_pos &vpos) { auto variation = m_terrain_variation.get_value(); auto noise = variation * fnlGetNoise3D(&m_fnl_terrain, vpos.x, vpos.y, vpos.z) - vpos.y; return noise > 0.0f; } const Overworld_Metadata &Overworld::get_or_create_metadata(const chunk_pos_xz &cpos) { auto it = m_metamap.find(cpos); if(it != m_metamap.cend()) { // Metadata is present return it->second; } auto &metadata = m_metamap.insert_or_assign(cpos, Overworld_Metadata()).first->second; metadata.entropy.fill(std::numeric_limits::max()); metadata.heightmap.fill(std::numeric_limits::min()); auto twister = std::mt19937_64(std::hash()(cpos)); auto variation = m_terrain_variation.get_value(); // Generator might need some randomness // that depends on 2D coordinates, so we // generate this entropy ahead of time for(int i = 0; i < CHUNK_AREA; ++i) { metadata.entropy[i] = twister(); } // Generate speculative heightmap; // Cave generation might have issues with placing // surface features such as trees but I genuinely don't give a shit for(int lx = 0; lx < CHUNK_SIZE; lx += 1) { for(int lz = 0; lz < CHUNK_SIZE; lz += 1) { auto hdx = static_cast(lx + lz * CHUNK_SIZE); auto vpos = coord::to_voxel(chunk_pos(cpos.x, 0, cpos.y), local_pos(lx, 0, lz)); for(vpos.y = variation; vpos.y >= -variation; vpos.y -= 1) { if(is_inside_terrain(vpos)) { metadata.heightmap[hdx] = vpos.y; break; } } } } // FIXME: make this into a configuration value constexpr static unsigned int TREE_DENSITY = 4U; // Generate tree locations for this chunk while(metadata.trees.size() < TREE_DENSITY) { auto lpos = local_pos_xz((twister() % CHUNK_SIZE), (twister() % CHUNK_SIZE)); auto is_unique = true; for(const auto &check_lpos : metadata.trees) { if(check_lpos == lpos) { is_unique = false; break; } } if(is_unique) { metadata.trees.push_back(lpos); } } return metadata; } void Overworld::generate_terrain(const chunk_pos &cpos, VoxelStorage &voxels) { auto &metadata = get_or_create_metadata(chunk_pos_xz(cpos.x, cpos.z)); auto variation = m_terrain_variation.get_value(); for(unsigned long i = 0; i < CHUNK_VOLUME; ++i) { auto lpos = coord::to_local(i); auto vpos = coord::to_voxel(cpos, lpos); if(vpos.y > variation) { voxels[i] = NULL_VOXEL_ID; continue; } if(vpos.y < -variation) { voxels[i] = game_voxels::stone; continue; } if(is_inside_terrain(vpos)) { voxels[i] = game_voxels::stone; continue; } } } void Overworld::generate_surface(const chunk_pos &cpos, VoxelStorage &voxels) { auto &metadata = get_or_create_metadata(chunk_pos_xz(cpos.x, cpos.z)); auto variation = m_terrain_variation.get_value(); for(unsigned long i = 0; i < CHUNK_VOLUME; ++i) { auto lpos = coord::to_local(i); auto vpos = coord::to_voxel(cpos, lpos); auto hdx = static_cast(lpos.x + lpos.z * CHUNK_SIZE); if((vpos.y > variation) || (vpos.y < -variation)) { // Speculative optimization continue; } if(voxels[i] == NULL_VOXEL_ID) { // Surface voxel checks only apply for solid voxels; // it's kind of obvious you can't replace air with grass continue; } unsigned int depth = 0U; for(unsigned int dy = 0U; dy < 5U; dy += 1U) { auto d_lpos = local_pos(lpos.x, lpos.y + dy + 1, lpos.z); auto d_vpos = coord::to_voxel(cpos, d_lpos); auto d_index = coord::to_index(d_lpos); if(d_lpos.y >= CHUNK_SIZE) { if(!is_inside_terrain(d_vpos)) break; depth += 1U; } else { if(voxels[d_index] == NULL_VOXEL_ID) break; depth += 1U; } } if(depth < (2U + (metadata.entropy[hdx] % 5U))) { if(depth == 0U) voxels[i] = game_voxels::grass; else voxels[i] = game_voxels::dirt; } } } void Overworld::generate_caves(const chunk_pos &cpos, VoxelStorage &voxels) { auto &metadata = get_or_create_metadata(chunk_pos_xz(cpos.x, cpos.z)); auto variation = m_terrain_variation.get_value(); for(unsigned long i = 0U; i < CHUNK_VOLUME; ++i) { auto lpos = coord::to_local(i); auto vpos = coord::to_voxel(cpos, lpos); if(vpos.y > variation) { // Speculative optimization - there's no solid // terrain above variation to carve caves out from continue; } if(is_inside_cave(vpos)) { voxels[i] = NULL_VOXEL_ID; continue; } } } void Overworld::generate_features(const chunk_pos &cpos, VoxelStorage &voxels) { const chunk_pos_xz tree_chunks[] = { chunk_pos_xz(cpos.x - 0, cpos.z - 1), chunk_pos_xz(cpos.x - 1, cpos.z - 1), chunk_pos_xz(cpos.x - 1, cpos.z + 0), chunk_pos_xz(cpos.x - 1, cpos.z + 1), chunk_pos_xz(cpos.x + 0, cpos.z + 0), chunk_pos_xz(cpos.x + 0, cpos.z + 1), chunk_pos_xz(cpos.x + 1, cpos.z - 1), chunk_pos_xz(cpos.x + 1, cpos.z + 0), chunk_pos_xz(cpos.x + 1, cpos.z + 1), }; for(unsigned int i = 0U; i < cxpr::array_size(tree_chunks); ++i) { const auto &cpos_xz = tree_chunks[i]; const auto &metadata = get_or_create_metadata(cpos_xz); for(const auto &lpos_xz : metadata.trees) { auto hdx = static_cast(lpos_xz.x + lpos_xz.y * CHUNK_SIZE); auto height = metadata.heightmap[hdx]; if(height == std::numeric_limits::min()) { // What happened? Cave happened continue; } auto cpos_xyz = chunk_pos(cpos_xz.x, 0, cpos_xz.y); auto lpos_xyz = local_pos(lpos_xz.x, 0, lpos_xz.y); auto vpos = coord::to_voxel(cpos_xyz, lpos_xyz); if(is_inside_cave(vpos)) { // Cave is in the way continue; } m_feat_tree.place(vpos + DIR_UP, cpos, voxels); } } }