Finish cave generation refactor #1

Merged
Bram merged 6 commits from refactor-end into main 2024-04-30 15:15:05 +00:00
5 changed files with 1259 additions and 735 deletions

1130
init.lua

File diff suppressed because it is too large Load Diff

View File

@ -1,91 +0,0 @@
-- This file takes care of cave biomes
noordstar_caves.registered_biomes = {}
-- Clean the input and return a valid shape def
-- If the input is invalid, return nil
local function clean_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 -1e5,
y = def.min_pos.y or -1e5,
z = def.min_pos.z or -1e5,
}
elseif type(def.y_min) == "number" then
d.minp = { x = -1e5, y = def.y_min, z = -1e5 }
else
d.minp = { x = -1e5, y = -1e5, z = -1e5 }
end
if type(def.max_pos) == "table" then
d.maxp = {
x = def.max_pos.x or 1e5,
y = def.max_pos.y or 1e5,
z = def.max_pos.z or 1e5,
}
elseif type(def.y_max) == "number" then
d.maxp = { x = 1e5, y = def.y_max, z = 1e5 }
else
d.maxp = { x = 1e5, y = 1e5, z = 1e5 }
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
function noordstar_caves.register_biome(def)
local d = clean_def(def)
if d ~= nil then
noordstar_caves.registered_biomes[d.name] = d
end
end
function noordstar_caves.unregister_biome(name)
noordstar_caves.registered_biomes[name] = nil
end
function noordstar_caves.clear_registered_biomes()
noordstar_caves.registered_biomes = {}
end

View File

@ -1,578 +0,0 @@
-- Constants and magic numbers
local mapgen_buffer = 16
-- Cave biome range
local biome_spread_range = 50
-- Noise params for heat_point
local heat_noise_params =
{ offset = 50
, scale = 50
, spread = { x = biome_spread_range, y = biome_spread_range, z = biome_spread_range }
, seed = 320523
, octaves = 2
, persistence = 0.1
, lacunarity = 2.0
, flags = ""
}
-- Noise params for humidity_point
local humidity_noise_params =
{ offset = 50
, scale = 50
, spread = { x = biome_spread_range, y = biome_spread_range, z = biome_spread_range }
, seed = 9923473
, octaves = 2
, persistence = 0.1
, lacunarity = 2.0
, flags = ""
}
local default_biome =
{ name = "noordstar_caves:none"
, heat_point = -1e6
, humidity_point = -1e6
, minp = { x = -1e5, y = -1e5, z = -1e5 }
, maxp = { x = 1e5, y = 1e5, z = 1e5 }
}
local cave_shape_chunk_size = 8
local cluster_shape_chunks_x = 4
local cluster_shape_chunks_y = 6
local cluster_shape_chunks_z = 4
local function reduced_pos(pos)
return
{ x = math.floor(pos.x / (cave_shape_chunk_size * cluster_shape_chunks_x))
, y = math.floor(pos.y / (cave_shape_chunk_size * cluster_shape_chunks_y))
, z = math.floor(pos.z / (cave_shape_chunk_size * cluster_shape_chunks_z))
}
end
-- Convert 3d relative coordinates to an index on a flat array
local function from_3d_to_flat(dx, dy, dz, nx, ny)
return (nx * ny * dz) + (nx * dy) + dx + 1
end
-- Convert an index on a flat array to 3d relative coordinates
local function from_flat_to_3d(i, nx, ny)
return {
dx = (i - 1) % nx,
dy = math.floor((i - 1) / nx) % ny,
dz = math.floor((i - 1) / (nx * ny))
}
end
-- Iterate over a 3d area, both indexing by the index and the ansolute position
local function 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
for i = 1, nx * ny * nz do
local dpos = from_flat_to_3d(i, nx, ny)
local pos = {
x = minp.x + dpos.dx,
y = minp.y + dpos.dy,
z = minp.z + dpos.dz,
}
callback(i, pos)
end
end
-- Helper function to convert a set of coordinates to a readable string
local function pos_to_str(pos)
return "(" .. pos.x .. ", " .. pos.y .. ", " .. pos.z .. " )"
end
local Flat3dArray = {}
Flat3dArray.__index = Flat3dArray
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
instance.nx = nx
instance.ny = ny
instance.nz = nz
instance.minp = minp
instance.maxp = maxp
instance.arr = arr
return instance
end
function Flat3dArray:from_func(minp, maxp, callback)
local arr = {}
iter_3d_area(minp, maxp, function (i, pos)
arr[i] = callback(i, pos)
end)
return self:new(minp, maxp, arr)
end
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
function Flat3dArray:get_pos(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 self:get_index(from_3d_to_flat(dx, dy, dz, self.nx, self.ny))
end
local function is_valid_pos(pos, minp, maxp)
if pos.x < minp.x then
return false
elseif pos.x > maxp.x then
return false
elseif pos.y < minp.y then
return false
elseif pos.y > maxp.y then
return false
elseif pos.z < minp.z then
return false
elseif pos.z > maxp.z then
return false
else
return true
end
end
function Flat3dArray:valid_pos(pos)
return is_valid_pos(pos, self.minp, self.maxp)
end
function Flat3dArray:validate_pos(pos)
if not self:valid_pos(pos) then
error(
table.concat(
{ "Position "
, pos_to_str(pos)
, " out of bounds from minp = "
, pos_to_str(self.minp)
, ", maxp = "
, pos_to_str(self.maxp)
},
""
)
)
end
end
-- Get an enhanced function from the def function that warns us when the
-- function does not behave properly
local function enhanced_func(def)
if type(def.name) ~= "string" then
error("Invalid nameless shape definition")
elseif type(def.func) ~= "function" then
error(
"Invalid shape definition misses an adjustment function"
)
else
return function(pos, n)
local out = def.func(pos, n)
if type(out) == "number" then
return out
elseif n == nil then
error(
"Shape " .. def.name .. " function must return a number. The input `n` was nil. Perhaps your `noise_params` field is invalid?"
)
else
error("Shape " .. def.name .. " function must return a number.")
end
end
end
end
-- Get a flat array of cave shape noise values from a given cave shape def
local function get_flat_from_shape_def(def, minp, maxp)
local f = enhanced_func(def)
local nx = maxp.x - minp.x + 1
local ny = maxp.y - minp.y + 1
local nz = maxp.z - minp.z + 1
local noise_flat_map = {}
-- If noise parameters have been defined, fill the table with noise
-- If not, all values remain nil
if def.noise_params ~= nil 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
iter_3d_area(minp, maxp, function(i, pos)
noise_flat_map[i] = f(pos, noise_flat_map[i])
end)
return Flat3dArray:new(minp, maxp, noise_flat_map)
end
local function get_flat_from_noise_params(minp, maxp, noise_params)
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
-- Based on the number of cave shapes, calculate how quickly connectivity is
-- meant to change
local function get_connectivity_noise_params(shape_size)
local factor = math.max(math.abs(shape_size) ^ 0.5, 1)
return {
offset = 50,
scale = 50,
spread =
reduced_pos(
{ x = factor * 250
, y = factor * 100
, z = factor * 250
}
),
seed = 297948,
octaves = 2,
persistence = 0.2,
lacunarity = 2.0,
flags = "eased"
}
end
-- Based on the number of cave shapes, calculate how quickly verticality is
-- meant to change
local function get_verticality_noise_params(shape_size)
local factor = math.max(math.abs(shape_size) ^ 0.5, 1)
return {
offset = 50,
scale = 50,
spread =
reduced_pos(
{ x = factor * 100
, y = factor * 250
, z = factor * 100
}
),
seed = 35644,
octaves = 2,
persistence = 0.2,
lacunarity = 2.0,
flags = "eased"
}
end
-- Get whether a cave shape is within distance.
local function shape_within_distance(shape_size, dx, dy)
local factor = math.max(math.abs(shape_size) ^ 0.5, 1)
local max_distance = 30 / factor
return dx^2 + dy^2 <= max_distance^2
end
local function shape_def_distance(def, x, y)
local dx = math.abs(x - def.connectivity_point)
local dy = math.abs(y - def.verticality_point)
return dx^2 + dy^2
end
-- Get a flat map containing all flat threshold values
local function get_threshold_flat(minp, maxp)
local connectivity = get_flat_from_noise_params(
reduced_pos(minp), reduced_pos(maxp),
get_connectivity_noise_params(#noordstar_caves.registered_shapes)
)
local verticality = get_flat_from_noise_params(
reduced_pos(minp), reduced_pos(maxp),
get_verticality_noise_params(#noordstar_caves.registered_shapes)
)
local noise = {}
-- Get noise for all cave shapes
for key, def in pairs(noordstar_caves.registered_shapes) do
noise[key] = get_flat_from_shape_def(def, reduced_pos(minp), reduced_pos(maxp))
end
-- Create a (reduced) flat array
local reduced = Flat3dArray:from_func(
reduced_pos(minp), reduced_pos(maxp),
function(i, pos)
local x = connectivity:get_pos(pos)
local y = verticality:get_pos(pos)
local value = 0
for key, def in pairs(noordstar_caves.registered_shapes) do
local dx = math.abs(x - def.connectivity_point)
local dy = math.abs(y - def.verticality_point)
if shape_within_distance(#noordstar_caves.registered_shapes, dx, dy) then
value = math.max(value, noise[key]:get_pos(pos))
end
end
return value
end
)
-- Create the flat array
local x = Flat3dArray:from_func(minp, maxp, function(i, pos)
return reduced:get_pos(reduced_pos(pos))
end)
return x
end
-- Old minimum height for the overworld
local old_overworld_min = mcl_vars.mg_overworld_min
-- If another mod doesn't override the maximum world depth, we will assume that
-- the world depth is the following value.
local world_depth = -60
-- Otherwise, this variable can be changed using the following function,
-- which will also update all the other necessary variables
function noordstar_caves.set_world_depth(h)
-- Set world depth variable
world_depth = h
end
noordstar_caves.set_world_depth(world_depth)
-- 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
-- - This function will be run a LOT so it is very performance sensitive
noordstar_caves.cave_vastness = function(pos)
return pos.y / world_depth
end
-- Secretly, we're using an internal function that also adds a safe layer for
-- when we're approaching bedrock levels
local function cave_vastness(pos)
if world_depth + 20 < pos.y then
return noordstar_caves.cave_vastness(pos)
elseif world_depth + 5 < pos.y then
return noordstar_caves.cave_vastness(pos) * math.abs(pos.y - world_depth - 5) / 15
else
return 0
end
end
-- Get a flat array of nodes that are either in caves or not in caves
local function get_flat_cave_bools(minp, maxp)
local thresholds = get_threshold_flat(minp, maxp)
return Flat3dArray:from_func(minp, maxp, function(i, pos)
return thresholds:get_pos(pos) >= 1 - cave_vastness(pos)
end)
end
local node_type =
{ unknown = 1 -- Edge of a chunk
, floor = 2 -- Floor of a cave
, wall = 3 -- Side wall of a cave
, roof = 4 -- Roof of a cave
, content = 5 -- Air in the cave not adjacent to a wall, floor or roof
, stone = 6 -- Underground node not adjacent to a cave
}
local function get_flat_cave_node_types(minp, maxp)
local bools = get_flat_cave_bools(minp, maxp)
return Flat3dArray:from_func(minp, maxp, function (i, pos)
if bools:get_pos(pos) then
return node_type.content
-- Floor takes precedence
elseif pos.y == maxp.y then -- Could be floor
return node_type.unknown
elseif bools:get_pos({ x = pos.x, y = pos.y + 1, z = pos.z }) then
return node_type.floor
-- Then roof takes precedence
elseif pos.y == minp.y then -- Could be roof
return node_type.unknown
elseif bools:get_pos({ x = pos.x, y = pos.y - 1, z = pos.z }) then
return node_type.roof
else
-- Check for walls
local left = { x = pos.x - 1, y = pos.y, z = pos.z }
local right = { x = pos.x + 1, y = pos.y, z = pos.z }
local front = { x = pos.x, y = pos.y, z = pos.z - 1 }
local back = { x = pos.x, y = pos.y, z = pos.z + 1 }
-- Check if the value is near the edge
local on_edge = false
if left.x < minp.x then
on_edge = true
elseif bools:get_pos(left) then
return node_type.wall
end
if right.x > maxp.x then
on_edge = true
elseif bools:get_pos(right) then
return node_type.wall
end
if front.z < minp.z then
on_edge = true
elseif bools:get_pos(front) then
return node_type.wall
end
if back.z > maxp.z then
on_edge = true
elseif bools:get_pos(back) then
return node_type.wall
end
if on_edge then
return node_type.unknown
else
return node_type.stone
end
end
end)
end
-- Transform categorized blocks into
local function biome_def_to_content_id(def, nt, original_block)
local function get_node(name, alt)
return function()
if name == nil then
return alt
else
return minetest.get_content_id(name)
end
end
end
local node_air = get_node(def.node_air, minetest.get_content_id("air"))
local node_floor = get_node(def.node_floor, original_block)
local node_stone = get_node(nil, original_block)
local node_roof = get_node(def.node_roof, original_block)
local node_unknown = get_node(nil, original_block)
local node_wall = get_node(def.node_wall, original_block)
if nt == node_type.unknown then
return node_unknown()
elseif nt == node_type.floor then
return node_floor()
elseif nt == node_type.wall then
return node_wall()
elseif nt == node_type.roof then
return node_roof()
elseif nt == node_type.content then
return node_air()
elseif nt == node_type.stone then
return node_stone()
else
return original_block
end
end
local function biome_distance(def, heat, humidity)
return (def.heat_point - heat)^2 + (def.humidity_point - humidity)^2
end
minetest.register_on_generated(function(minp, maxp, blockseed)
local vminp =
{ x = minp.x - mapgen_buffer
, y = minp.y - mapgen_buffer
, z = minp.z - mapgen_buffer
}
local vmaxp =
{ x = maxp.x + mapgen_buffer
, y = maxp.y + mapgen_buffer
, z = maxp.z + mapgen_buffer
}
-- Get voxelmanip
local vm = minetest.get_mapgen_object("voxelmanip")
local flat_data = Flat3dArray:new(vminp, vmaxp, vm:get_data())
-- Get threshold values
local node_types = get_flat_cave_node_types(vminp, vmaxp)
local heat_points = get_flat_from_noise_params(minp, maxp, heat_noise_params)
local humidity_points = get_flat_from_noise_params(minp, maxp, humidity_noise_params)
-- Map block values
local nids = Flat3dArray:from_func(vminp, vmaxp, function(i, pos)
local nt = node_types:get_pos(pos)
if not is_valid_pos(pos, minp, maxp) then
return flat_data:get_pos(pos)
else
local biome = default_biome
local heat = heat_points:get_pos(pos)
local humidity = humidity_points:get_pos(pos)
for _, def in pairs(noordstar_caves.registered_biomes) do
if pos.x < def.minp.x then
elseif pos.y < def.minp.y then
elseif pos.z < def.minp.z then
elseif pos.x > def.maxp.x then
elseif pos.y > def.maxp.y then
elseif pos.z > def.maxp.z then
elseif biome_distance(def, heat, humidity) > biome_distance(biome, heat, humidity) then
else
biome = def
end
end
return biome_def_to_content_id(biome, nt, flat_data:get_pos(pos))
end
end)
-- Write all changes to the Minetest world
vm:set_data(nids.arr)
vm:write_to_map()
end)

View File

@ -1,56 +0,0 @@
-- This file takes care of the cave shapes.
noordstar_caves.registered_shapes = {}
-- Clean the input and return a valid shape def
-- If the input is invalid, return nil
local function clean_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
}
if type(def.noise_params) == "table" then
d.noise_params = def.noise_params
end
if type(def.func) == "function" then
d.func = def.func
else
d.func = function(pos, v) return v end
end
if type(def.y_min) == "number" and type(def.y_max) == "number" then
d.y_min = def.y_min
d.y_max = def.y_max
end
return d
end
function noordstar_caves.register_shape(def)
local d = clean_def(def)
if d ~= nil then
noordstar_caves.registered_shapes[d.name] = d
end
end
function noordstar_caves.unregister_shape(name)
noordstar_caves.registered_shapes[name] = nil
end
function noordstar_caves.clear_registered_shapes()
noordstar_caves.registered_shapes = {}
end

139
lua/timer.lua Normal file
View File

@ -0,0 +1,139 @@
local internal = {
waypoints = {},
stats = {},
sessions = 0,
last = os.clock()
}
timer = {}
function timer.start()
internal.waypoints = {}
internal.now()
internal.sessions = internal.sessions + 1
end
function timer.checkpoint(name)
local now = os.clock()
table.insert(internal.waypoints, { name, internal.last, now })
internal.now()
end
function timer.stop()
local name_len = 0
for _, t in ipairs(internal.waypoints) do
local name = t[1]
local start = t[2]
local stop = t[3]
local stat = internal.stats[name]
if stat then
internal.stats[name] = stat + (stop - start)
else
internal.stats[name] = stop - start
end
name_len = math.max(name_len, string.len(name))
end
local h1 = "Task"
local h2 = "Time"
local h3 = "Time (avg)"
internal.log(
table.concat(
{ h1
, string.rep(" ", name_len - string.len(h1))
, " | "
, h2
, string.rep(" ", 8 - string.len(h2))
, " | "
, h3
}
, ""
)
)
internal.log_hr(name_len)
for _, t in ipairs(internal.waypoints) do
local name = t[1]
internal.log_time(
name,
internal.to_ms(t[3] - t[2]),
internal.to_ms(internal.stats[name] / internal.sessions),
name_len
)
end
internal.log_hr(name_len)
internal.log_time("Total", internal.sum(), internal.sum_avg(), name_len)
end
-- Log text to the Minetest chat
function internal.log(text)
minetest.chat_send_all(os.time() .. " - " .. text)
end
function internal.log_hr(name_len)
internal.log(string.rep("-", name_len + 3 + 8 + 3 + 8))
end
function internal.log_time(header, time, time_avg, header_max_len)
local duration = tostring(time)
local duration_avg = tostring(time_avg)
internal.log(
table.concat(
{ header
, string.rep(" ", header_max_len - string.len(header))
, " | "
, string.rep(" ", 5 - string.len(duration))
, duration
, " ms | "
, string.rep(" ", 5 - string.len(duration_avg))
, duration_avg
, " ms"
}
, ""
)
)
end
function internal.now()
internal.last = os.clock()
end
function internal.sum()
local s = 0
for _, t in ipairs(internal.waypoints) do
s = s + (t[3] - t[2])
end
return internal.to_ms(s)
end
function internal.sum_avg()
local s = 0
for _, t in pairs(internal.stats) do
s = s + t
end
return internal.to_ms(s / internal.sessions)
end
function internal.to_ms(s)
return math.round(s * 1e3)
end
internal.now()