ns_cavegen/init.lua

623 lines
19 KiB
Lua
Raw Normal View History

2024-09-09 13:10:39 +00:00
-- Average size of each cave biome
local BIOME_SIZE = { x = 250, y = 250, z = 250 }
2024-04-29 15:31:03 +00:00
2024-09-09 13:10:39 +00:00
-- Average step size of connectivity params
-- Given that connectivity is mostly a horizontal feature, it mostly changes
-- on a vertical scale, and barely on a horizontal scale.
local CONNECTIVITY_BLOB = { x = 250, y = 100, z = 250 }
2024-04-29 15:31:03 +00:00
2024-09-09 13:10:39 +00:00
local ENUM_AIR = 1
local ENUM_CEILING = 2
local ENUM_CEILING_DECORATION = 3
local ENUM_FLOOR = 4
local ENUM_FLOOR_DECORATION = 5
local ENUM_STONE = 6
local ENUM_WALL = 7
2024-04-29 15:31:03 +00:00
2024-09-09 13:10:39 +00:00
-- Point that is so out of the normal connectivity/verticality scope (0 - 100)
-- that it is only selected when no other items are registered.
local OUTLANDISH_POINT = -1e3
2024-04-17 21:08:44 +00:00
2024-09-09 13:10:39 +00:00
-- Average size of each cave shape
local SHAPE_SIZE = { x = 128, y = 128, z = 128 }
2024-09-09 13:10:39 +00:00
-- Several seeds for mapgen
local SEED_CONNECTIVITY = 297948
local SEED_HEAT = 320523
local SEED_HUMIDITY = 9923473
local SEED_VERTICALITY = 35644
2024-09-09 13:10:39 +00:00
-- Average step size of verticality params
-- Given that verticality is mostly a vertical feature, it mostly changes on a
-- horizontal scale, and barely on a vertical scale.
local VERTICALITY_BLOB = { x = 100, y = 250, z = 100 }
2024-09-09 13:10:39 +00:00
-- Lower bound for cave generation
local WORLD_MINP = { x = -1e6, y = -1e6, z = -1e6 }
2024-09-09 13:10:39 +00:00
-- Upper bound for cave generation
local WORLD_MAXP = { x = 1e6, y = 1e6, z = 1e6 }
2024-05-01 09:26:36 +00:00
2024-09-09 13:10:39 +00:00
local WORLD_DEPTH = -60
2024-09-09 13:10:39 +00:00
local internal = {}
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
2024-09-09 13:10:39 +00:00
-------------------------------- INTERNAL API ---------------------------------
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
function internal.cave_vastness(pos)
2024-09-09 13:10:39 +00:00
local v = ns_caves.cave_vastness(pos)
2024-09-09 13:10:39 +00:00
if not v then
return 0
2024-09-09 13:10:39 +00:00
elseif v < 0 then
return 0
elseif v > 1 then
return 1
else
2024-09-09 13:10:39 +00:00
return v
end
2024-09-09 13:10:39 +00:00
end
2024-09-09 13:10:39 +00:00
-- Classify all nodes in the chunk into what they are
-- TODO: Perhaps instead of iterating over ALL nodes, we can sort all node types
-- TODO: into separate tables, effectively allowing us to process the nodes
-- TODO: independently, saving computation time.
function internal.classify_nodes(used_shapes, minp, maxp)
local sminp = vector.offset(minp, 1, 1, 1)
local smaxp = vector.offset(maxp, -1, -1, -1)
local sva = VoxelArea(sminp, smaxp)
local va = VoxelArea(minp, maxp)
2024-04-17 21:08:44 +00:00
2024-09-09 13:10:39 +00:00
local items = {}
for i in sva:iterp(sminp, smaxp) do
local pos = sva:position(i)
local bi = va:index(pos.x, pos.y, pos.z)
local function is_part_of_cave(v)
return used_shapes[va:index(v.x, v.y, v.z)]
end
2024-09-09 13:10:39 +00:00
if used_shapes[bi] == true then
-- Part of cave
if not used_shapes[va:index(pos.x, pos.y + 1, pos.z)] then
items[i] = ENUM_CEILING_DECORATION
elseif not used_shapes[va:index(pos.x, pos.y - 1, pos.z)] then
items[i] = ENUM_FLOOR_DECORATION
else
items[i] = ENUM_AIR
end
2024-04-30 10:13:40 +00:00
else
2024-09-09 13:10:39 +00:00
-- Not part of cave
if used_shapes[va:index(pos.x, pos.y + 1, pos.z)] then
items[i] = ENUM_FLOOR
elseif used_shapes[va:index(pos.x, pos.y - 1, pos.z)] then
items[i] = ENUM_CEILING
elseif used_shapes[va:index(pos.x - 1, pos.y, pos.z)] then
items[i] = ENUM_WALL
elseif used_shapes[va:index(pos.x + 1, pos.y, pos.z)] then
items[i] = ENUM_WALL
elseif used_shapes[va:index(pos.x, pos.y, pos.z - 1)] then
items[i] = ENUM_WALL
elseif used_shapes[va:index(pos.x, pos.y, pos.z + 1)] then
items[i] = ENUM_WALL
else
items[i] = ENUM_STONE
end
2024-04-30 10:13:40 +00:00
end
end
2024-09-09 13:10:39 +00:00
local custom_y = -13
if sminp.y < custom_y and custom_y < smaxp.y then
for i in sva:iterp({ x = sminp.x, y = custom_y, z = sminp.z }, { x = smaxp.x, y = custom_y, z = smaxp.z }) do
items[i] = ENUM_CEILING
2024-04-30 10:13:40 +00:00
end
end
2024-09-09 13:10:39 +00:00
return items
2024-04-30 10:13:40 +00:00
end
2024-09-09 13:10:39 +00:00
function internal.clean_biome_def(def)
assert(type(def) == "table")
assert(type(def.name) == "string")
assert(type(def.heat_point) == "number")
assert(type(def.humidity_point) == "number")
2024-09-09 13:10:39 +00:00
def.y_min = def.y_min or (def.min_pos and def.min_pos.y) or WORLD_MINP.y
def.y_max = def.y_max or (def.max_pos and def.max_pos.y) or WORLD_MAXP.y
2024-09-09 13:10:39 +00:00
def.min_pos = def.min_pos or { x = WORLD_MINP.x, y = def.y_min, z = WORLD_MINP.z }
def.max_pos = def.max_pos or { x = WORLD_MAXP.x, y = def.y_max, z = WORLD_MAXP.z }
2024-09-09 13:10:39 +00:00
assert(type(def.y_min) == "number")
assert(type(def.y_max) == "number")
assert(type(def.min_pos) == "table")
assert(type(def.max_pos) == "table")
assert(type(def.min_pos.x) == "number")
assert(type(def.max_pos.x) == "number")
assert(type(def.min_pos.y) == "number")
assert(type(def.max_pos.y) == "number")
assert(type(def.min_pos.z) == "number")
assert(type(def.max_pos.z) == "number")
2024-05-02 09:18:46 +00:00
2024-09-09 13:10:39 +00:00
def.node_air = def.node_air or "air"
2024-09-09 13:10:39 +00:00
assert(def.node_dust == nil or type(def.node_dust) == "string")
assert(def.node_floor == nil or type(def.node_floor) == "string")
assert(def.node_wall == nil or type(def.node_wall) == "string")
assert(def.node_roof == nil or type(def.node_roof) == "string")
assert(type(def.node_air) == "string")
2024-09-09 13:10:39 +00:00
return def
end
2024-09-09 13:10:39 +00:00
function internal.clean_shape_def(def)
assert(
type(def) == "table",
"Shape def is meant to be a table type"
)
assert(type(def.name) == "string", "Shape name is meant to be a string")
assert(type(def.connectivity_point) == "number")
assert(type(def.verticality_point) == "number")
2024-09-09 13:10:39 +00:00
def.y_min = def.y_min or WORLD_MINP.y
def.y_max = def.y_max or WORLD_MAXP.y
def.func = def.func or function(_, n) return n end
2024-09-09 13:10:39 +00:00
assert(type(def.y_min) == "number")
assert(type(def.y_max) == "number")
assert(type(def.func) == "function")
assert(def.noise_params == nil or type(def.noise_params) == "table")
2024-09-09 13:10:39 +00:00
return def
end
-- Get connectivity noise params
function internal.connectivity_noise_params()
2024-09-09 13:10:39 +00:00
local factor = math.max(1, math.abs(#ns_caves.registered_shapes) ^ 0.5)
return {
offset = 50,
scale = 50,
2024-09-09 13:10:39 +00:00
spread = {
x = factor * CONNECTIVITY_BLOB.x,
y = factor * CONNECTIVITY_BLOB.y,
z = factor * CONNECTIVITY_BLOB.z,
},
seed = SEED_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"
2024-09-09 13:10:39 +00:00
, heat_point = OUTLANDISH_POINT
, humidity_point = 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"
2024-09-09 13:10:39 +00:00
, connectivity_point = OUTLANDISH_POINT
, verticality_point = OUTLANDISH_POINT
, func = function (pos, v) return 0 end
}
)
end
2024-09-09 13:10:39 +00:00
-- Get a (sorta) Euclidian distance. The exact distance doesn't matter,
-- it just needs to correctly determine whichever value is closer in a
-- Euclidian space. Consequently, the square root is ignored as optimization.
function internal.euclidian(x1, x2, y1, y2)
return (x1 - x2) ^ 2 + (y1 - y2) ^ 2
end
2024-09-09 13:10:39 +00:00
-- For each node, determine which cave biome they're in.
function internal.find_biome_allocations(heat, humidity)
assert(#heat == #humidity)
2024-09-09 13:10:39 +00:00
local allocs = {}
local default_biome = internal.default_biome()
2024-04-29 16:19:23 +00:00
2024-09-09 13:10:39 +00:00
for i = 1, #heat, 1 do
local e, u = heat[i], humidity[i]
local d = internal.euclidian(
e, default_biome.heat_point,
u, default_biome.humidity_point
)
local biome_name
2024-09-09 13:10:39 +00:00
-- Find the appropriate biome
for name, def in pairs(ns_caves.registered_biomes) do
local def_d = internal.euclidian(
e, def.heat_point, u, def.humidity_point
)
2024-04-29 16:19:23 +00:00
2024-09-09 13:10:39 +00:00
if def_d < d then
d = def_d
biome_name = name
end
end
2024-09-09 13:10:39 +00:00
allocs[i] = biome_name
end
2024-09-09 13:10:39 +00:00
return allocs
end
2024-09-09 13:10:39 +00:00
-- For each node, determine which cave shape they follow.
function internal.find_shape_allocations(connectivity, verticality)
assert(#connectivity == #verticality)
2024-04-30 16:50:07 +00:00
2024-09-09 13:10:39 +00:00
local allocs = {}
local default_shape = internal.default_shape()
2024-04-29 15:33:46 +00:00
2024-09-09 13:10:39 +00:00
for i = 1, #connectivity, 1 do
local c, v = connectivity[i], verticality[i]
local d = internal.euclidian(
c, default_shape.connectivity_point,
v, default_shape.verticality_point
)
local shape_name
2024-04-29 15:33:46 +00:00
2024-09-09 13:10:39 +00:00
-- Find the appropriate shape
for name, def in pairs(ns_caves.registered_shapes) do
local def_d = internal.euclidian(
c, def.connectivity_point, v, def.verticality_point
)
2024-04-29 15:33:46 +00:00
2024-09-09 13:10:39 +00:00
if def_d < d then
d = def_d
shape_name = name
2024-04-29 15:33:46 +00:00
end
end
2024-09-09 13:10:39 +00:00
-- Assign the chosen name
allocs[i] = shape_name
end
2024-09-09 13:10:39 +00:00
return allocs
end
2024-09-09 13:10:39 +00:00
-- Once it has been figured out which node belongs to which shape,
-- get noise values from the respective shapes.
-- This function does its operations in-place.
function internal.find_shape_values(used_shapes, minp, maxp, va)
-- Cache shapes so they don't need to be recalculated.
local captured_shapes = {}
local default_shape = internal.default_shape()
2024-04-18 08:49:45 +00:00
2024-09-09 13:10:39 +00:00
for i in va:iterp(minp, maxp) do
-- Get shape values per item
local name = used_shapes[i]
local shape
2024-04-18 08:49:45 +00:00
2024-09-09 13:10:39 +00:00
if name == nil then
shape = default_shape
else
2024-09-09 13:10:39 +00:00
shape = ns_caves.registered_shapes[name]
end
2024-09-09 13:10:39 +00:00
if captured_shapes[shape] == nil then
-- minetest.debug("Generating new shape! " .. shape.name)
captured_shapes[shape] = internal.generate_shape(shape, minp, maxp, va)
end
2024-04-29 16:19:23 +00:00
2024-09-09 13:10:39 +00:00
used_shapes[i] = captured_shapes[shape][i]
if used_shapes[i] == nil then
error(
"Noise value ended up nil unexpectedly: i = " .. i .. ", shape = " .. shape.name
)
end
end
2024-09-09 13:10:39 +00:00
-- minetest.debug("Finished generating shapes.")
end
2024-09-09 13:10:39 +00:00
function internal.generate_connectivity_noise(minp, maxp)
return internal.generate_perlin_noise(
internal.connectivity_noise_params(), minp, maxp
)
end
2024-04-29 15:33:46 +00:00
2024-09-09 13:10:39 +00:00
function internal.generate_heat_noise(minp, maxp)
return internal.generate_perlin_noise(
internal.heat_noise_params(), minp, maxp
2024-04-29 15:33:46 +00:00
)
2024-09-09 13:10:39 +00:00
end
function internal.generate_humidity_noise(minp, maxp)
return internal.generate_perlin_noise(
internal.humidity_noise_params(), minp, maxp
2024-04-29 15:33:46 +00:00
)
2024-09-09 13:10:39 +00:00
end
2024-04-29 15:33:46 +00:00
2024-09-09 13:10:39 +00:00
-- Generate Perlin noise within given boundaries
function internal.generate_perlin_noise(noiseparams, minp, maxp)
local size = {
x = 1 + maxp.x - minp.x,
y = 1 + maxp.y - minp.y,
z = 1 + maxp.z - minp.z,
}
2024-04-30 15:37:34 +00:00
2024-09-09 13:10:39 +00:00
return PerlinNoiseMap(noiseparams, size):get_3d_map_flat(minp)
end
2024-04-29 16:26:59 +00:00
2024-09-09 13:10:39 +00:00
function internal.generate_shape(def, minp, maxp, va)
-- local noise_flat = {}
2024-04-29 16:26:59 +00:00
2024-09-09 13:10:39 +00:00
-- Get random noise if noise_params are given
if def.noise_params then
return internal.generate_perlin_noise(def.noise_params, minp, maxp)
else
return {}
-- for i in va:iterp(minp, maxp) do
-- noise_flat[i] = 0
-- end
end
2024-04-29 15:33:46 +00:00
2024-09-09 13:10:39 +00:00
-- -- Update noise with custom defined function
-- for i in va:iterp(minp, maxp) do
-- noise_flat[i] = def.func(va:position(i), noise_flat[i])
-- end
2024-04-29 15:33:46 +00:00
2024-09-09 13:10:39 +00:00
-- return noise_flat
end
2024-04-29 15:33:46 +00:00
2024-09-09 13:10:39 +00:00
-- Generate verticality noise within given boundaries
function internal.generate_verticality_noise(minp, maxp)
return internal.generate_perlin_noise(
internal.verticality_noise_params(), minp, maxp
)
2024-04-29 15:33:46 +00:00
end
-- Get the noise params for the cave biome temperature.
function internal.heat_noise_params()
2024-04-30 10:13:40 +00:00
return {
offset = 50,
scale = 50,
2024-09-09 13:10:39 +00:00
spread = BIOME_SIZE,
seed = SEED_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,
2024-09-09 13:10:39 +00:00
spread = BIOME_SIZE,
seed = SEED_HUMIDITY,
octaves = 2,
persistence = 0.1,
lacunarity = 2.0,
flags = ""
}
end
2024-09-09 13:10:39 +00:00
-- Take all necessary steps to execute the mapgen
function internal.mapgen(minp, maxp, blockseed, vm, va)
-- Create bordered VoxelArea.
-- The point of this is so walls and ceilings can be determined using
-- bordering nodes in different chunks.
local bminp = vector.offset(minp, -1, -1, -1)
local bmaxp = vector.offset(maxp, 1, 1, 1)
local bva = VoxelArea(bminp, bmaxp)
2024-09-09 13:10:39 +00:00
-- Find cave shape params
local connectivity = internal.generate_connectivity_noise(bminp, bmaxp)
local verticality = internal.generate_verticality_noise(bminp, bmaxp)
2024-09-09 13:10:39 +00:00
-- Draw cave shapes
local used_shapes = internal.find_shape_allocations(connectivity, verticality)
internal.find_shape_values(used_shapes, bminp, bmaxp, bva)
internal.shape_to_air(used_shapes, bminp, bmaxp, bva)
2024-09-09 13:10:39 +00:00
-- Find cave biome params
local heat = internal.generate_heat_noise(minp, maxp)
local humidity = internal.generate_humidity_noise(minp, maxp)
2024-09-09 13:10:39 +00:00
-- -- DEBUG: Write to air (or not)
-- local air = 0
2024-09-09 13:10:39 +00:00
-- for i in small_va:iterp(minp, maxp) do
-- if used_shapes[i] == true then
-- local pos = small_va:position(i)
-- local vmi = va:index(pos.x, pos.y, pos.z)
-- data[vmi] = minetest.CONTENT_AIR
2024-09-09 13:10:39 +00:00
-- air = air + 1
-- end
-- end
2024-09-09 13:10:39 +00:00
-- Classify various nodes as walls, floors, ceilings
local classified_nodes = internal.classify_nodes(used_shapes, bminp, bmaxp)
2024-04-30 16:50:07 +00:00
2024-09-09 13:10:39 +00:00
-- Draw cave biomes
local used_biomes = internal.find_biome_allocations(heat, humidity)
2024-04-30 16:50:07 +00:00
2024-09-09 13:10:39 +00:00
-- Manipulate `data` table by adding classified nodes based on which biome
-- they're in.
local data = vm:get_data()
2024-04-30 16:50:07 +00:00
2024-09-09 13:10:39 +00:00
internal.write_classified_biome_nodes(
data, va, classified_nodes, used_biomes, minp, maxp
)
2024-04-30 16:50:07 +00:00
2024-09-09 13:10:39 +00:00
vm:set_data(data)
vm:write_to_map()
end
2024-09-09 13:10:39 +00:00
-- Place items
function internal.place_node_on_data(data, i, name)
if name == nil then
return
end
2024-09-09 13:10:39 +00:00
local node_id = minetest.get_content_id(name)
2024-09-09 13:10:39 +00:00
if node_id == nil then
return
end
2024-09-09 13:10:39 +00:00
data[i] = node_id
end
2024-09-09 13:10:39 +00:00
-- Register a new biome
function internal.register_biome(biome)
biome = internal.clean_biome_def(biome)
ns_caves.registered_biomes[biome.name] = biome
end
2024-09-09 13:10:39 +00:00
-- Register a new shape
function internal.register_shape(shape)
shape = internal.clean_shape_def(shape)
ns_caves.registered_shapes[shape.name] = shape
end
2024-09-09 13:10:39 +00:00
-- Convert all shape noise into clarifications whether a node is wall or air.
-- This function does its operations in-place.
function internal.shape_to_air(noise_values, minp, maxp, va)
for i in va:iterp(minp, maxp) do
local vastness = internal.cave_vastness(va:position(i))
2024-09-09 13:10:39 +00:00
if noise_values[i] == nil then
error(
"Noise value ended up nil unexpectedly"
)
elseif noise_values[i] >= 1 - vastness then
noise_values[i] = true
2024-05-01 22:41:35 +00:00
else
2024-09-09 13:10:39 +00:00
noise_values[i] = false
2024-05-01 22:41:35 +00:00
end
end
end
-- Get verticality noise params
function internal.verticality_noise_params()
2024-09-09 13:10:39 +00:00
local factor = math.max(1, math.abs(#ns_caves.registered_shapes) ^ 0.5)
return {
offset = 50,
scale = 50,
2024-09-09 13:10:39 +00:00
spread = {
x = factor * VERTICALITY_BLOB.x,
y = factor * VERTICALITY_BLOB.y,
z = factor * VERTICALITY_BLOB.z,
},
seed = SEED_VERTICALITY,
octaves = 2,
persistence = 0.2,
lacunarity = 2.0,
flags = "eased"
}
end
2024-09-09 13:10:39 +00:00
function internal.write_classified_biome_nodes(vm_data, va, classified_nodes, used_biomes, minp, maxp)
local small_va = VoxelArea(minp, maxp)
2024-09-09 13:10:39 +00:00
-- assert(#classified_nodes == #used_biomes)
-- assert(#classified_nodes == small_va:getVolume())
2024-04-29 15:33:46 +00:00
2024-09-09 13:10:39 +00:00
local default_biome = internal.default_biome()
local schems = {}
2024-09-09 13:10:39 +00:00
for i in va:iterp(minp, maxp) do
local pos = va:position(i)
local si = small_va:index(pos.x, pos.y, pos.z)
2024-09-09 13:10:39 +00:00
local node_type = classified_nodes[si]
local biome = used_biomes[si]
2024-09-09 13:10:39 +00:00
if biome == nil then
biome = default_biome
else
biome = ns_caves.registered_biomes[biome] or default_biome
end
2024-09-09 13:10:39 +00:00
if node_type == ENUM_AIR then
internal.place_node_on_data(vm_data, i, biome.node_air)
elseif node_type == ENUM_CEILING then
internal.place_node_on_data(vm_data, i, biome.node_roof)
elseif node_type == ENUM_CEILING_DECORATION then
-- TODO: Return schematics to be placed
internal.place_node_on_data(vm_data, i, biome.node_air)
elseif node_type == ENUM_FLOOR then
internal.place_node_on_data(vm_data, i, biome.node_floor)
elseif node_type == ENUM_FLOOR_DECORATION then
-- TODO: Return schematics to be placed
internal.place_node_on_data(vm_data, i, biome.node_air)
elseif node_type == ENUM_STONE then
-- Nothing needs to be placed
elseif node_type == ENUM_WALL then
internal.place_node_on_data(vm_data, i, biome.node_wall)
else
if node_type == nil then
error(
"Expected enum value, encountered nil at index " .. si .. " (originally " .. i .. ")"
)
end
error(
"Encountered unknown node type enum value " .. node_type
)
2024-09-09 13:10:39 +00:00
end
end
2024-09-09 13:10:39 +00:00
return schems
end
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
2024-09-09 13:10:39 +00:00
--------------------------------- PUBLIC API ----------------------------------
2024-04-29 15:33:46 +00:00
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
2024-04-30 15:37:34 +00:00
2024-09-09 13:10:39 +00:00
minetest.register_on_generated(function(minp, maxp, blockseed)
if maxp.y < WORLD_DEPTH then
return
end
2024-04-29 15:33:46 +00:00
local vm = minetest.get_mapgen_object("voxelmanip")
2024-09-09 13:10:39 +00:00
local va = VoxelArea(vm:get_emerged_area())
internal.mapgen(minp, maxp, blockseed, vm, va)
end)
2024-04-30 15:37:34 +00:00
2024-09-09 13:10:39 +00:00
ns_caves = {
cave_vastness = function(pos)
if pos.y > 0 or pos.y < WORLD_DEPTH then
return 0
end
2024-04-29 15:33:46 +00:00
2024-09-09 13:10:39 +00:00
local y = math.abs(pos.y)
local d = math.abs(WORLD_DEPTH / 2)
return 1 - (math.abs(y - d) / d)
end,
2024-04-29 15:33:46 +00:00
2024-09-09 13:10:39 +00:00
register_biome = internal.register_biome,
2024-09-09 13:10:39 +00:00
register_shape = internal.register_shape,
2024-04-30 15:37:34 +00:00
2024-09-09 13:10:39 +00:00
registered_biomes = {},
2024-05-01 22:41:35 +00:00
2024-09-09 13:10:39 +00:00
registered_shapes = {},
}
2024-04-29 15:33:46 +00:00
2024-09-09 13:10:39 +00:00
dofile(minetest.get_modpath(minetest.get_current_modname()) .. "/lua/register.lua")