-- DEVELOPMENT ONLY: Timer module for debugging performance local modpath = minetest.get_modpath(minetest.get_current_modname()) dofile(modpath.."/lua/timer.lua") -- DEVELOPMENT ONLY: End of timer module -- Local variables used for programming local internal = { a = nil -- Average size of a cave biome , biome_size = { x = 50, y = 50, z = 50 } -- Average blob size of connectivity noise params , cnct_blob = { x = 250, y = 100, z = 250 } -- The margin that a voxelmanip gives around the chunk being generated , mapgen_buffer = 16 -- Enum type used during cave generation for classifying different types -- of cave nodes. Do not touch unless you're changing the code , node_types = { stick_edge = 0 , air = 1 , stone = 2 , floor = 3 , wall = 4 , ceiling = 5 , floor_deco = 6 } -- Point that can be used for generating Voronoi graphs -- The number can be anything, as long as it reasonably far away from -- the range 0 - 100 , outlandish_point = -1e3 -- Random seeds used to influence noise , seeds = { connectivity = 297948 , heat = 320523 , humidity = 9923473 , verticality = 35644 } -- Size of blocks being combined to simplify cave shape generation -- Higher values means better performance, but lower detail and variations , shape_cluster = { x = 32, y = 32, z = 32 } -- Average size of a shape biome , shape_size = { x = 50, y = 50, z = 50 } -- Average blob size of verticality noise params , vrtcl_blob = { x = 100, y = 250, z = 100 } -- If another mod doesn't override this value, we will assume that this is -- the world's depth. , world_depth = -60 -- Lower bound for the entire world , world_minp = { x = -1e6, y = -1e6, z = -1e6 } -- Upper bound for cave generation , world_maxp = { x = 1e6, y = 1e6, z = 1e6 } } ------------------------------------------------------------------------------- -- Flat3dArray ------------------------------------------------------------------------------- -- The Flat3dArray helps translate, transform and read data from different -- 1-dimensional flat tables that all represent some 3-dimensional set. local Flat3dArray = {} Flat3dArray.__index = Flat3dArray -- Global variable that can be used by other mods -- Please see API.md for reference noordstar_caves = { a = nil -- The `cave_vastness` is a variable that determines the size of caves. -- The function takes in an x, y and z variable, and returns a number -- between 0 and 1. -- Low numbers mean very rare and tiny caves, while high numbers mean -- massive caves, with massive open spaces. -- If you wish to overwrite this function, it is good to keep in mind to: -- - Make sure that the output changes VERY SLOWLY over time -- - Keep the function performant as it will be executed many, many times , cave_vastness = function(pos) return pos.y / internal.world_depth end -- A public list of all registered biomes , registered_biomes = {} -- A public list of all registered decorations , registered_decorations = {} -- A public list of all registered shapes , registered_shapes = {} } -- Remove all registered biomes and start with a clean slate function noordstar_caves.clear_registered_biomes() noordstar_caves.registered_biomes = {} end -- Remove all registered decorations and start with a clean slate function noordstar_caves.clear_registered_decoration() noordstar_caves.registered_decorations = {} end -- Remove all registered shapes and start with a clean slate function noordstar_caves.clear_registered_shapes() noordstar_caves.registered_shapes = {} end -- Register a new cave biome function noordstar_caves.register_biome(def) local d = internal.clean_biome_def(def) if d then noordstar_caves.registered_biomes[d.name] = d end end -- Register a new cave decoration function noordstar_caves.register_decoration(def) local d = internal.clean_deco_def(def) if d then table.insert(noordstar_caves.registered_decorations, d) end end -- Register a new cave shape function noordstar_caves.register_shape(def) local d = internal.clean_shape_def(def) if d then noordstar_caves.registered_shapes[d.name] = d end end -- Override the world's depth function noordstar_caves.set_world_depth(h) internal.world_depth = h end noordstar_caves.set_world_depth(internal.world_depth) -- Remove a specific registered cave biome function noordstar_caves.unregister_biome(name) noordstar_caves.registered_biomes[name] = nil end -- Remove a specific registered cave shape function noordstar_caves.unregister_shape(name) noordstar_caves.registered_shapes[name] = nil end -- Calculate the distance of a given biome to a given point function internal.biome_def_distance(def, heat, humidity) local dx = math.abs(humidity - def.humidity_point) local dy = math.abs(heat - def.heat_point) return dx^2 + dy^2 end -- While a public `noordstar_caves.cave_vastness` function is exposed, -- this mod uses an improved internal function to keep the edges pleasant -- and to avoid potential errors. function internal.cave_vastness(pos) -- Calculate value and put it in bounds local v = noordstar_caves.cave_vastness(pos) v = math.min(v, 1) v = math.max(v, 0) local d = internal.world_depth if pos.y > d + 20 then return v elseif pos.y > d + 5 then return v * (pos.y - (d + 5)) / 15 else return 0 end end -- Clean the user input on a biome definition before inserting it. function internal.clean_biome_def(def) if type(def.name) ~= "string" then return nil end if type(def.heat_point) ~= "number" then return nil end if type(def.humidity_point) ~= "number" then return nil end local d = { name = def.name, heat_point = def.heat_point, humidity_point = def.humidity_point, } -- Position if type(def.min_pos) == "table" then d.minp = { x = def.min_pos.x or internal.world_minp.x, y = def.min_pos.y or internal.world_minp.y, z = def.min_pos.z or internal.world_minp.z, } elseif type(def.y_min) == "number" then d.minp = { x = internal.world_minp.x, y = def.y_min, z = internal.world_minp.z, } else d.minp = { x = internal.world_minp.x, y = internal.world_minp.y, z = internal.world_minp.z, } end if type(def.max_pos) == "table" then d.maxp = { x = def.max_pos.x or internal.world_maxp.x, y = def.max_pos.y or internal.world_maxp.y, z = def.max_pos.z or internal.world_maxp.z, } elseif type(def.y_max) == "number" then d.maxp = { x = internal.world_maxp.x, y = def.y_max, z = internal.world_maxp.z, } else d.maxp = { x = internal.world_maxp.x, y = internal.world_maxp.y, z = internal.world_maxp.z, } end -- Optional nodes if type(def.node_dust) == "string" then d.node_dust = def.node_dust end if type(def.node_floor) == "string" then d.node_floor = def.node_floor end if type(def.node_wall) == "string" then d.node_wall = def.node_wall end if type(def.node_roof) == "string" then d.node_roof = def.node_roof end if type(def.node_air) == "string" then d.node_air = def.node_air end if type(def.node_shell) == "string" then d.node_shell = def.node_shell end if type(def.depth_shell) == "number" then d.depth_shell = def.depth_shell else d.depth_shell = 0 end return d end -- Clean the user input on a decoration definition before inserting it. function internal.clean_deco_def(def) if def.deco_type ~= "simple" and def.deco_type ~= "schematic" then return nil end if def.deco_type == "simple" and type(def.decoration) ~= "string" then return nil end if def.deco_type == "schematic" then if type(def.schematic) == "string" then elseif type(def.schematic) == "table" then else return nil end end if type(def.fill_ratio) ~= "number" then return nil end local d = { deco_type = def.deco_type, decoration = def.decoration, schematic = def.schematic, fill_ratio = def.fill_ratio, biomes = def.biomes, } if def.place_on == "floor" or def.place_on == "ceiling" then d.place_on = def.place_on else d.place_on = "floor" end local function place(key, t, default) if type(def[key]) == t then d[key] = def[key] else d[key] = default end end place("y_min", "number", internal.world_minp.y) place("y_max", "number", internal.world_maxp.y) place("height_max", "number", 0) place("height", "number", 1) place("place_offset_y", "number", 0) place("replacements", "table", {}) place("flags", "string", "place_center_x,place_center_z") place("rotation", "string", "0") d.height = math.max(d.height , 0) d.height = math.min(d.height , 16) d.height_max = math.max(d.height_max, 0) d.height_max = math.min(d.height_max, 16) return d end -- Clean the user input on a shape definition before inserting it. function internal.clean_shape_def(def) if type(def.name) ~= "string" then return nil end if type(def.connectivity_point) ~= "number" then return nil end if type(def.verticality_point) ~= "number" then return nil end local d = { name = def.name, connectivity_point = def.connectivity_point, verticality_point = def.verticality_point, y_min = def.y_min or internal.world_minp.y, y_max = def.y_max or internal.world_maxp.y, } if type(def.noise_params) == "table" then d.noise_params = def.noise_params end local func = function(pos, v) return v end if type(def.func) == "function" then func = def.func end d.func = internal.enhanced_shape_func(def.name, func) return d end -- Get the most nearby cave biome function internal.closest_cave_biome(heat, humidity, pos) local biome = internal.default_biome() local d = internal.biome_def_distance(biome, heat, humidity) for _, def in pairs(noordstar_caves.registered_biomes) do if internal.is_valid_pos(pos, def.minp, def.maxp) then local new_d = internal.biome_def_distance(def, heat, humidity) if new_d <= d then biome = def d = new_d end end end return biome end -- Get the most nearby cave shape function internal.closest_cave_shape(cnct, vrtcl, y) local shape = internal.default_shape() local d = internal.shape_def_distance(shape, cnct, vrtcl) for key, def in pairs(noordstar_caves.registered_shapes) do if def.y_min <= y and y <= def.y_max then local new_d = internal.shape_def_distance(def, cnct, vrtcl) if new_d <= d then shape = def d = new_d end end end return shape end -- Get connectivity noise params function internal.connectivity_noise_params() local factor = math.max( math.abs(#noordstar_caves.registered_shapes) ^ 0.5 , 1 ) return { offset = 50, scale = 50, spread = internal.reduced_shape_pos( { x = factor * internal.cnct_blob.x , y = factor * internal.cnct_blob.y , z = factor * internal.cnct_blob.z } ), seed = internal.seeds.connectivity, octaves = 2, persistence = 0.2, lacunarity = 2.0, flags = "eased" } end -- Get a default cave biome in case no biomes are registered function internal.default_biome() return internal.clean_biome_def( { name = "noordstar_caves:default_biome" , heat_point = internal.outlandish_point , humidity_point = internal.outlandish_point } ) end -- Get a default cave shape in case no shapes are registered function internal.default_shape() return internal.clean_shape_def( { name = "noordstar_caves:none" , connectivity_point = internal.outlandish_point , verticality_point = internal.outlandish_point , func = function (pos, v) return 0 end } ) end -- This library decides not to trust that the user is giving a proper input -- on its function field. To help the developer insert a proper function, a few -- error messages are inserted to give the developer more insight in how their -- function behaves. function internal.enhanced_shape_func(name, func) if type(func) ~= "function" then error("Expected function, found type `" .. type(func) .. "`") end return function (pos, n) local out = func(pos, n) if type(out) == "number" then return out elseif n == nil then error( table.concat( { "Shape" , name , "function must return a number." , "Your function's input `n` was nil." , "Perhaps your `noise_params` field is invalid?" } , " " ) ) else error( table.concat( { "Shape" , name , "function must return a number." } , " " ) ) end end end -- Get a Flat3dArray showing whether nodes are cave bools function internal.flat_from_cave_bools(minp, maxp) -- Calculate connectivity and verticality for each point local connectivity = internal.flat_from_noise_params( internal.connectivity_noise_params(), internal.reduced_shape_pos(minp), internal.reduced_shape_pos(maxp) ) local verticality = internal.flat_from_noise_params( internal.verticality_noise_params(), internal.reduced_shape_pos(minp), internal.reduced_shape_pos(maxp) ) -- Using the calculated noise, determine the cave shape per shape chunk local reduced = Flat3dArray:from_func( internal.reduced_shape_pos(minp), internal.reduced_shape_pos(maxp), function (i, pos) -- DEVELOPMENT ONLY: Unittest if connectivity:pos_to_index(pos) ~= i then error("Connectivity index doesn't match local index") end if verticality:pos_to_index(pos) ~= i then error("Verticality index doesn't match local index") end -- DEVELOPMENT ONLY: End unittest local cnct = connectivity:get_index(i) local vrtcl = verticality:get_index(i) local def = internal.closest_cave_shape(cnct, vrtcl, pos.y) return def.name end) -- Calculate noise for each shape local noise = {} for key, shape in pairs(noordstar_caves.registered_shapes) do noise[key] = internal.flat_from_shape_def(shape, minp, maxp) end local default = internal.default_shape() noise[default.name] = internal.flat_from_shape_def(default, minp, maxp) -- Create a flat array of bools local bools = Flat3dArray:from_func(minp, maxp, function (i, pos) local key = reduced:get_pos(internal.reduced_shape_pos(pos)) if noise[key] == nil then error("Key " .. key .. " gave no value on noise") end local n = noise[key]:get_index(i) local v = internal.cave_vastness(pos) return internal.is_cave_node(n, v) end) return bools end function internal.flat_from_node_types(bools, minp, maxp) local nt = internal.node_types -- local bools = internal.flat_from_cave_bools(minp, maxp) local node_types = Flat3dArray:from_func(minp, maxp, function (i, pos) -- Simplify calculation by ignoring edges if pos.x == minp.x or pos.x == maxp.x then return nt.stick_edge elseif pos.y == minp.y or pos.y == maxp.y then return nt.stick_edge elseif pos.z == maxp.z or pos.z == minp.z then return nt.stick_edge end if bools:get_index(i) then -- PART OF CAVE if bools:get_index(i + bools.down) == false then -- Block is right on the floor return nt.floor_deco end if bools:get_index(i + bools.up) == false then -- Block is near the ceiling return nt.ceiling_deco end return nt.air else -- NOT PART OF CAVE -- Floor if bools:get_index(i + bools.up) then return nt.floor end -- Ceiling if bools:get_index(i + bools.down) then return nt.ceiling end -- Walls if bools:get_index(i + bools.north) then return nt.wall end if bools:get_index(i + bools.east) then return nt.wall end if bools:get_index(i + bools.south) then return nt.wall end if bools:get_index(i + bools.west) then return nt.wall end return nt.stone end end) return node_types end -- Get a Flat3dArray using noise_params function internal.flat_from_noise_params(noise_params, minp, maxp) local nx = maxp.x - minp.x + 1 local ny = maxp.y - minp.y + 1 local nz = maxp.z - minp.z + 1 local buffer = {} local p = PerlinNoiseMap(noise_params, { x = nx, y = ny, z = nz }) if nz == 1 then p:get_2d_map_flat(minp, buffer) else p:get_3d_map_flat(minp, buffer) end return Flat3dArray:new(minp, maxp, buffer) end -- Get a Flat3dArray about shape noise -- This function is defined differently from other definitions because the -- noise_params are optional in the shape definition function internal.flat_from_shape_def(def, minp, maxp) local noise_flat_map = {} local nx = maxp.x - minp.x + 1 local ny = maxp.y - minp.y + 1 local nz = maxp.z - minp.z + 1 if def.noise_params then local p = PerlinNoiseMap(def.noise_params, { x = nx, y = ny, z = nz }) if nz == 1 then p:get_2d_map_flat(minp, noise_flat_map) else p:get_3d_map_flat(minp, noise_flat_map) end end internal.iter_3d_area(minp, maxp, function (i, pos) noise_flat_map[i] = def.func(pos, noise_flat_map[i]) end) return Flat3dArray:new(minp, maxp, noise_flat_map) end -- Convert 3d relative coordinates to an index on a flat array function internal.from_3d_to_flat(dx, dy, dz, nx, ny) return (nx * ny * dz) + (nx * dy) + dx + 1 end -- Generate caves given a voxelmanip array function internal.generate_caves(data, minp, maxp) -- Increased voxelmanip size local vminp = { x = minp.x - internal.mapgen_buffer , y = minp.y - internal.mapgen_buffer , z = minp.z - internal.mapgen_buffer } local vmaxp = { x = maxp.x + internal.mapgen_buffer , y = maxp.y + internal.mapgen_buffer , z = maxp.z + internal.mapgen_buffer } -- Increased node type size to account for unknown edges local bminp = { x = minp.x - 1 , y = minp.y - 1 , z = minp.z - 1 } local bmaxp = { x = maxp.x + 1 , y = maxp.y + 1 , z = maxp.z + 1 } -- Load data local vmanip = Flat3dArray:new(vminp, vmaxp, data) -- Get cave bools local bools = internal.flat_from_cave_bools(bminp, bmaxp) timer.checkpoint("Calculate cave shape") -- Get node types local nts = internal.flat_from_node_types(bools, bminp, bmaxp) -- Calculate biome heat & humidity local heat_points = internal.flat_from_noise_params( internal.heat_noise_params(), bminp, bmaxp ) local humidity_points = internal.flat_from_noise_params( internal.humidity_noise_params(), bminp, bmaxp ) timer.checkpoint("Calculate block types & biomes") local air = minetest.get_content_id("air") local schems = {} -- Place blocks where necessary internal.iter_3d_area(bminp, bmaxp, function (i, pos) local vi = vmanip:pos_to_index(pos) local function place(name) if vmanip:get_index(vi) == air then elseif type(name) == "string" then vmanip:set_index(vi, minetest.get_content_id(name)) elseif type(name) == "nil" then else error("Inserted invalid type " .. type(name) .. " into voxelmanip array") end end local nt = nts:get_index(i) if nt == internal.node_types.stone then elseif nt == internal.node_types.stick_edge then elseif nt == internal.node_types.air then place("air") else -- Find appropriate biome local heat = heat_points:get_index(i) local humidity = humidity_points:get_index(i) local def = internal.closest_cave_biome(heat, humidity, pos) if nt == internal.node_types.floor then place(def.node_floor) elseif nt == internal.node_types.wall then place(def.node_wall) elseif nt == internal.node_types.ceiling then place(def.node_roof) elseif nt == internal.node_types.floor_deco or nt == internal.node_types.ceiling_deco then local schem_placed = false local appropriate_place = "floor" if nt == internal.node_types.ceiling_deco then appropriate_place = "ceiling" end if not schem_placed then -- Prevent decorations from spawning in the air if vmanip:get_index(vi) == air then schem_placed = true end end if not schem_placed then for _, deco in ipairs(noordstar_caves.registered_decorations) do if deco.place_on ~= appropriate_place then elseif math.random() > deco.fill_ratio then elseif not internal.is_deco_biome(deco, def.name) then else table.insert(schems, { pos = pos, deco = deco }) schem_placed = true break end end end if not schem_placed then local n = "air" if nt == internal.node_types.floor_deco then n = def.node_dust or n end place(n) schem_placed = true end else error( table.concat( { "Found unknown node type " , nt , " (type " , type(nt) , ")" } , "" ) ) end end end) return vmanip, schems end -- Get the noise params for the cave biome temperature. function internal.heat_noise_params() return { offset = 50, scale = 50, spread = internal.biome_size, seed = internal.seeds.heat, octaves = 2, persistence = 0.1, lacunarity = 2.0, flags = "" } end -- Get the noise params for the cave biome humidity. function internal.humidity_noise_params() return { offset = 50, scale = 50, spread = internal.biome_size, seed = internal.seeds.humidity, octaves = 2, persistence = 0.1, lacunarity = 2.0, flags = "" } end -- Return whether a node is part of a cave function internal.is_cave_node(noise, vastness) return noise > 1 - vastness end -- Return whether a given decoration definition can spawn in a given biome name function internal.is_deco_biome(def, biome_name) if type(def.biomes) == "nil" then return true elseif type(def.biomes) == "string" then return def.biomes == biome_name elseif type(def.biomes) == "table" then for _, name in pairs(def.biomes) do if name == biome_name then return true end end end return false end -- Return a bool whether a position is within a given volume function internal.is_valid_pos(pos, minp, maxp) if minp.x <= pos.x and pos.x <= maxp.x then if minp.y <= pos.y and pos.y <= maxp.y then if minp.z <= pos.z and pos.z <= maxp.z then return true end end end return false end -- Iterate over a 3d area, both indexing by the index and the ansolute position function internal.iter_3d_area(minp, maxp, callback) local nx = maxp.x - minp.x + 1 local ny = maxp.y - minp.y + 1 -- local nz = maxp.z - minp.z + 1 local i = 0 for z = minp.z, maxp.z, 1 do for y = minp.y, maxp.y, 1 do for x = minp.x, maxp.x, 1 do local pos = { x = x, y = y, z = z } i = i + 1 -- DEVELOPMENT ONLY: This function only serves as a unittest -- DEVELOPMENT ONLY: to verify that the iteration across a 3D -- DEVELOPMENT ONLY: table works appropriately. -- DEVELOPMENT ONLY: This calculation is highly inefficient and -- DEVELOPMENT ONLY: unnecessary so it is highly recommended -- DEVELOPMENT ONLY: to remove this code in production. local dx = x - minp.x local dy = y - minp.y local dz = z - minp.z local si = internal.from_3d_to_flat(dx, dy, dz, nx, ny) if i ~= si then error( table.concat( { "Expected position" , internal.pos_to_str(pos) , "to correspond to index" , si , "but our iteration arrived at index" , i } , " " ) ) end -- DEVELOPMENT ONLY: -- DEVELOPMENT ONLY: This is the bottom of the unittest. -- DEVELOPMENT ONLY: Please remove this code in a future commit -- DEVELOPMENT ONLY: before the mod is assumed to be -- DEVELOPMENT ONLY: production-ready. -- DEVELOPMENT ONLY: callback(i, pos) end end end end -- -- Log text to the Minetest chat -- function internal.log(text) -- minetest.chat_send_all(os.time() .. " - " .. text) -- end -- Place all schematic decorations into the world function internal.place_schematic_decorations(vmanip, schems) for _, schem in ipairs(schems) do local pos = { x = schem.pos.x, y = schem.pos.y + schem.deco.place_offset_y, z = schem.pos.z, } local deco = schem.deco if deco.deco_type ~= "schematic" then else if deco.place_on == "floor" then minetest.place_schematic_on_vmanip( vmanip, pos, deco.schematic, deco.rotation, deco.replacement, true, deco.flags ) elseif deco.place_on == "ceiling" then local h = deco.schematic if type(deco.schematic) == "string" then h = minetest.read_schematic(h, {}) if type(h) == "nil" then error("Could not find schematic! Perhaps it the filename is incorrect?") end end h = h.size.y minetest.place_schematic_on_vmanip( vmanip, { x = pos.x, y = pos.y - h + 1, z = pos.z }, deco.schematic, deco.rotation, deco.replacement, true, deco.flags ) else error("Unknown place_on value `" .. deco.place_on .. "`") end end end end -- Place all simple decorations into the world function internal.place_simple_decorations(flat, schems) for _, schem in ipairs(schems) do if schem.deco.deco_type == "simple" then local pos = { x = schem.pos.x, y = schem.pos.y + schem.deco.place_offset_y, z = schem.pos.z, } local deco = schem.deco local i = flat:pos_to_index(pos) local dir = nil if deco.place_on == "floor" then dir = flat.up elseif deco.place_on == "ceiling" then dir = flat.down else error("Invalid place_on value `" .. deco.place_on .. "`") end local h_min = deco.height local h_max = math.max(deco.height, deco.height_max) for dy = 0, math.random(h_min, h_max) - 1, 1 do local li = i + dir * dy flat:set_index(li, minetest.get_content_id(deco.decoration)) end end end end -- Helper function to convert a set of coordinates to a readable string function internal.pos_to_str(pos) return "(" .. pos.x .. ", " .. pos.y .. ", " .. pos.z .. " )" end -- Get a reduced position based on the chunks function internal.reduced_shape_pos(pos) return { x = math.floor(pos.x / internal.shape_cluster.x) , y = math.floor(pos.y / internal.shape_cluster.y) , z = math.floor(pos.z / internal.shape_cluster.z) } end -- Calculate the relative index change to move downwards one position function internal.relative_get_down(nx, ny) return internal.relative_index_diff(0, -1, 0, nx, ny) end -- Calculate the relative index change to move eastwards one position function internal.relative_get_east(nx, ny) return internal.relative_index_diff(1, 0, 0, nx, ny) end -- Calculate the relative index change to move northwards one position function internal.relative_get_north(nx, ny) return internal.relative_index_diff(0, 0, 1, nx, ny) end -- Calculate the relative index change to move southwards one position function internal.relative_get_south(nx, ny) return internal.relative_index_diff(0, 0, -1, nx, ny) end -- Calculate the relative index change to move upwards one position function internal.relative_get_up(nx, ny) return internal.relative_index_diff(0, 1, 0, nx, ny) end -- Calculate the relative index change to move westwards one position function internal.relative_get_west(nx, ny) return internal.relative_index_diff(-1, 0, 0, nx, ny) end -- Calculate the difference in index to relative coordinates. -- Used to speed up index lookup on flat 3d arrays. function internal.relative_index_diff(dx, dy, dz, nx, ny) return ( internal.from_3d_to_flat(dx, dy, dz, nx, ny) - internal.from_3d_to_flat( 0, 0, 0, nx, ny) ) end -- Calculate the distance of a given shape to a given point function internal.shape_def_distance(def, cnct, vrtcl) local dx = math.abs(cnct - def.connectivity_point) local dy = math.abs(vrtcl - def.verticality_point) return dx^2 + dy^2 end -- Get verticality noise params function internal.verticality_noise_params() local factor = math.max( math.abs(#noordstar_caves.registered_shapes) ^ 0.5 , 1 ) return { offset = 50, scale = 50, spread = internal.reduced_shape_pos( { x = factor * internal.vrtcl_blob.x , y = factor * internal.vrtcl_blob.y , z = factor * internal.vrtcl_blob.z } ), seed = internal.seeds.verticality, octaves = 2, persistence = 0.2, lacunarity = 2.0, flags = "eased" } end -- Determine whether a position is within given bounds function internal.within_bounds(pos, minp, maxp) if minp.x <= pos.x and pos.x <= maxp.x then if minp.y <= pos.y and pos.y <= maxp.y then if minp.z <= pos.z and pos.z <= maxp.z then return true end end end return false end -- Create a new flat 3d array based on a function callback that gets the index -- and position as inputs function Flat3dArray:from_func(minp, maxp, callback) local arr = {} internal.iter_3d_area(minp, maxp, function (i, pos) arr[i] = callback(i, pos) end) return self:new(minp, maxp, arr) end -- Get the Flat3dArray's value on the i'th index function Flat3dArray:get_index(i) local out = self.arr[i] if out == nil then error( "Index " .. i .. " not found in array of length " .. #self.arr ) end return out end -- Get a value at a given position function Flat3dArray:get_pos(pos) -- pos_to_index already validates so there's no need to validate here return self:get_index(self:pos_to_index(pos)) end -- Create a new Flat3dArray instance function Flat3dArray:new(minp, maxp, arr) local instance = {} setmetatable(instance, Flat3dArray) local nx = maxp.x - minp.x + 1 local ny = maxp.y - minp.y + 1 local nz = maxp.z - minp.z + 1 if #arr ~= nx * ny * nz then error( "Input array doesn't match dimension lengths: " .. nx .. " x " .. ny .. " x " .. nz .. " = " .. (nx*ny*nz) .. ", but found " .. #arr ) end -- Variables instance.nx = nx instance.ny = ny instance.nz = nz instance.minp = minp instance.maxp = maxp instance.arr = arr -- Constants instance.up = internal.relative_get_up(nx, ny) instance.down = internal.relative_get_down(nx, ny) instance.north = internal.relative_get_north(nx, ny) instance.east = internal.relative_get_east(nx, ny) instance.south = internal.relative_get_south(nx, ny) instance.west = internal.relative_get_west(nx, ny) return instance end -- Convert a position to an index function Flat3dArray:pos_to_index(pos) self:validate_pos(pos) local dx = pos.x - self.minp.x local dy = pos.y - self.minp.y local dz = pos.z - self.minp.z return internal.from_3d_to_flat(dx, dy, dz, self.nx, self.ny) end -- Override a value at a given index function Flat3dArray:set_index(i, val) self:validate_index(i) self.arr[i] = val end -- Validate an index to be in the Flat3dArray function Flat3dArray:validate_index(i) if i < 1 or i > #self.arr then error( table.concat( { "Index" , i , "is not in range 1 -" , #self.arr } , " " ) ) end end -- Validate a position to be in the Flat3dArray function Flat3dArray:validate_pos(pos) if not internal.is_valid_pos(pos, self.minp, self.maxp) then error( table.concat( { "Position" , internal.pos_to_str(pos) , "is not within minp =" , internal.pos_to_str(self.minp) , "and maxp =" , internal.pos_to_str(self.maxp) } , " " ) ) end end ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- minetest.register_on_generated(function(minp, maxp, blockseed) timer.start() math.randomseed(blockseed) local vm = minetest.get_mapgen_object("voxelmanip") local data = vm:get_data() local flat, schems = internal.generate_caves(data, minp, maxp) timer.checkpoint("Place caves") internal.place_simple_decorations(flat, schems) vm:set_data(flat.arr) vm:write_to_map() internal.place_schematic_decorations(vm, schems) timer.checkpoint("Place decorations") timer.stop() end) ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- -- For show, the following code COULD in theory be run elsewhere ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- noordstar_caves.register_shape({ name = "noordstar_caves:bubbles", noise_params = { offset = 0.5, scale = 0.5, spread = { x = 20, y = 20, z = 20 }, seed = 248039, octaves = 2, persistence = 0.6, lacunarity = 2.0, flags = "eased" }, func = function(pos, n) return n end, -- TODO: Implement y limits -- y_min = -31000, -- y_max = 31000, connectivity_point = 10, verticality_point = 40, }) noordstar_caves.register_shape({ name = "noordstar_caves:cliffs", noise_params = { offset = 0.4, scale = 0.5, spread = { x = 20, y = 100, z = 20 }, seed = 97354, octaves = 4, persistence = 0.6, lacunarity = 2.0, flags = "" }, func = function(pos, n) return n end, connectivity_point = 30, verticality_point = 80, }) noordstar_caves.register_shape({ name = "noordstar_caves:donuts", noise_params = { offset = 0.0, scale = 1.0, spread = { x = 30, y = 30, z = 30 }, seed = 3934, octaves = 1, persistence = 0.6, lacunarity = 2.0, flags = "eased" }, func = function(pos, n) return 1 - 2 * math.abs(n)^0.5 end, connectivity_point = 50, verticality_point = 40, }) noordstar_caves.register_shape({ name = "noordstar_caves:wall", func = function(pos, n) return -0.5 end, connectivity_point = 0, verticality_point = 0, }) -- noordstar_caves.set_world_depth(-60) -- noordstar_caves.cave_vastness = function(pos) return math.abs(pos.y - 60) / 120 end noordstar_caves.register_biome({ name = "nc:glowing_floor", node_floor = "mcl_core:crying_obsidian", node_wall = "mcl_core:sand", node_roof = "mcl_ocean:sea_lantern", node_dust = "mcl_core:snow", heat_point = 20, humidity_point = 0, }) noordstar_caves.register_biome({ name = "nc:crazy_glowin_walls", node_floor = "mcl_amethyst:amethyst_block", node_wall = "mcl_crimson:shroomlight", node_roof = "mcl_colorblocks:glazed_terracotta_silver", heat_point = 100, humidity_point = 0, }) noordstar_caves.register_biome({ name = "nc:regular", heat_point = 50, humidity_point = 50, }) noordstar_caves.register_biome({ name = "nc:cold", node_dust = "mcl_core:snow", node_roof = "mcl_core:ice", heat_point = 10, humidity_point = 100, }) noordstar_caves.register_biome({ name = "nc:grass", node_floor = "mcl_core:dirt_with_grass", heat_point = 50, humidity_point = 70, }) noordstar_caves.register_biome({ name = "nc:dangerous_lava", node_floor = "mcl_core:lava_source", y_max = -40, heat_point = 100, humidity_point = 100, }) noordstar_caves.register_decoration({ deco_type = "simple", place_on = "floor", fill_ratio = 0.01, decoration = "mcl_core:cactus", height = 3, height_max = 8, biomes = { "nc:glowing_floor" }, }) noordstar_caves.register_decoration({ deco_type = "simple", place_on = "ceiling", fill_ratio = 0.1, decoration = "mcl_ocean:sea_lantern", height = 1, height_max = 4, biomes = { "nc:crazy_glowin_walls" }, place_offset_y = -3, }) noordstar_caves.register_decoration({ deco_type = "schematic", place_on = "floor", fill_ratio = 0.001, schematic = squeak, rotation = "random", -- place_offset_y = 5, })