diff --git a/init.lua b/init.lua index 18a2427..e60370b 100644 --- a/init.lua +++ b/init.lua @@ -1,21 +1,1131 @@ +-- DEVELOPMENT ONLY: Timer module for debugging performance local modpath = minetest.get_modpath(minetest.get_current_modname()) -local function load(name) - dofile(modpath.."/lua/"..name..".lua") -end +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 = {} +noordstar_caves = + { a = nil --- Load features to influence cave shapes -load("shape") + -- 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 --- Load features to influence cave biomes -load("biome") + -- A public list of all registered biomes + , registered_biomes = {} --- Start engine to generate caves -load("engine") + -- 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 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 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 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) + local biome = internal.default_biome() + local d = internal.biome_def_distance(biome, heat, humidity) + + for _, def in pairs(noordstar_caves.registered_biomes) do + local new_d = internal.biome_def_distance(def, heat, humidity) + + if new_d <= d then + biome = def + d = new_d + end + end + + return biome +end + +-- Get the most nearby cave shape +function internal.closest_cave_shape(cnct, vrtcl) + local shape = internal.default_shape() + local d = internal.shape_def_distance(shape, cnct, vrtcl) + + for key, def in pairs(noordstar_caves.registered_shapes) do + local new_d = internal.shape_def_distance(def, cnct, vrtcl) + + if new_d <= d then + shape = def + d = new_d + 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) + + 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 + + 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) + + -- 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 + ) + + local air = minetest.get_content_id("air") + + -- Place blocks where necessary + internal.iter_3d_area(bminp, bmaxp, function (i, pos) + + local function place(name) + local vi = vmanip:pos_to_index(pos) + + 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") + elseif nt == internal.node_types.floor_deco then + -- TODO: Place registered decoration + 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) + + 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) + else + error( + table.concat( + { "Found unknown node type " + , nt + , " (type " + , type(nt) + , ")" + } + , "" + ) + ) + end + end + end) + + return vmanip.arr +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 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 + +-- 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) + local vm = minetest.get_mapgen_object("voxelmanip") + local data = vm:get_data() + + data = internal.generate_caves(data, minp, maxp) + + vm:set_data(data) + vm:write_to_map() +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 = "test", + node_floor = "mcl_core:crying_obsidian", + node_wall = "mcl_core:sand", + node_roof = "mcl_ocean:sea_lantern", + heat_point = 0, + humidity_point = 0, +}) +noordstar_caves.register_biome({ + name = "test2", + node_floor = "mcl_amethyst:amethyst_block", + node_wall = "mcl_crimson:shroomlight", + node_roof = "mcl_colorblocks:glazed_terracotta_silver", + heat_point = 100, + humidity_point = 100, +}) diff --git a/lua/biome.lua b/lua/biome.lua deleted file mode 100644 index 202879d..0000000 --- a/lua/biome.lua +++ /dev/null @@ -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 diff --git a/lua/engine.lua b/lua/engine.lua deleted file mode 100644 index 367c44b..0000000 --- a/lua/engine.lua +++ /dev/null @@ -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) diff --git a/lua/shape.lua b/lua/shape.lua deleted file mode 100644 index 960a19c..0000000 --- a/lua/shape.lua +++ /dev/null @@ -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 diff --git a/lua/timer.lua b/lua/timer.lua new file mode 100644 index 0000000..a6a3773 --- /dev/null +++ b/lua/timer.lua @@ -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()