diff --git a/init.lua b/init.lua index 48c6795..fba26a7 100644 --- a/init.lua +++ b/init.lua @@ -1,893 +1,3 @@ --- Average size of each cave biome -local BIOME_SIZE = { x = 250, y = 250, z = 250 } - --- 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 } - -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 - --- 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 - --- Average size of each cave shape -local SHAPE_SIZE = { x = 128, y = 128, z = 128 } - --- Several seeds for mapgen -local SEED_CONNECTIVITY = 297948 -local SEED_HEAT = 320523 -local SEED_HUMIDITY = 9923473 -local SEED_VERTICALITY = 35644 - --- 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 } - --- Lower bound for cave generation -local WORLD_MINP = { x = -1e6, y = -1e6, z = -1e6 } - --- Upper bound for cave generation -local WORLD_MAXP = { x = 1e6, y = 1e6, z = 1e6 } - -local WORLD_DEPTH = -60 - -local internal = {} - -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- --------------------------------- INTERNAL API --------------------------------- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- - -function internal.cave_vastness(pos) - local v = ns_cavegen.cave_vastness(pos) - - if not v then - return 0 - elseif v < 0 then - return 0 - elseif v > 1 then - return 1 - else - return v - end -end - --- Classify all nodes in the chunk into what they are -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) - - local ceiling_decos = {} - local ceilings = {} - local contents = {} - local floor_decos = {} - local floors = {} - local walls = {} - - for i in sva:iterp(sminp, smaxp) do - local pos = sva:position(i) - - if used_shapes[va:index(pos.x, pos.y, pos.z)] == true then - -- Part of cave - if not used_shapes[va:index(pos.x, pos.y + 1, pos.z)] then - table.insert(ceiling_decos, i) - elseif not used_shapes[va:index(pos.x, pos.y - 1, pos.z)] then - table.insert(floor_decos, i) - else - table.insert(contents, i) - end - else - -- Not part of cave - if used_shapes[va:index(pos.x, pos.y + 1, pos.z)] then - table.insert(floors, i) - elseif used_shapes[va:index(pos.x, pos.y - 1, pos.z)] then - table.insert(ceilings, i) - elseif used_shapes[va:index(pos.x - 1, pos.y, pos.z)] then - table.insert(walls, i) - elseif used_shapes[va:index(pos.x + 1, pos.y, pos.z)] then - table.insert(walls, i) - elseif used_shapes[va:index(pos.x, pos.y, pos.z - 1)] then - table.insert(walls, i) - elseif used_shapes[va:index(pos.x, pos.y, pos.z + 1)] then - table.insert(walls, i) - end - end - end - - return { - ceiling_decos = ceiling_decos, - ceilings = ceilings, - contents = contents, - floor_decos = floor_decos, - floors = floors, - walls = walls, - } -end - -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") - - 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 - - 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 } - - 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") - - def.node_air = def.node_air or "air" - - 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") - - return def -end - -function internal.clean_deco_def(def) - def.deco_type = def.deco_type or "simple" - def.place_on = def.place_on or "floor" - def.y_min = def.y_min or WORLD_MINP.y - def.y_max = def.y_max or WORLD_MAXP.y - - assert(def.deco_type == "simple" or def.deco_type == "schematic") - assert(def.place_on == "floor" or def.place_on == "ceiling") - assert(type(def.fill_ratio) == "number") - assert(def.biomes == nil or type(def.biomes) == "table") - - if def.deco_type == "simple" then - def.height = def.height or 1 - def.height_max = def.height_max or def.height - def.place_offset_y = def.place_offset_y or 0 - - assert(type(def.deco_type) == "string") - assert(type(def.height) == "number") - assert(type(def.height_max) == "number") - assert(type(def.place_offset_y) == "number") - - elseif def.deco_type == "schematic" then - def.replacements = def.replacements or {} - def.place_offset_y = def.place_offset_y or 0 - - assert(type(def.schematic) == "string" or type(def.schematic) == "table") - assert(type(def.replacements) == "table") - - assert(def.rotation == "0" or def.rotation == "90" or def.rotation == "180" or def.rotation == "270" or def.rotation == "random") - assert(type(def.place_offset_y) == "number") - end - - return def -end - -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") - - 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 - - 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") - - return def -end - --- Get connectivity noise params -function internal.connectivity_noise_params() - local factor = math.max(1, math.abs(#ns_cavegen.registered_shapes) ^ 0.5) - - return { - offset = 50, - scale = 50, - 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 = "ns_cavegen:default_biome" - , 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 = "ns_cavegen:none" - , connectivity_point = OUTLANDISH_POINT - , verticality_point = OUTLANDISH_POINT - , func = function (pos, v) return 0 end - } - ) -end - --- 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 - --- For each node, determine which cave biome they're in. -function internal.find_biome_allocations(heat, humidity) - assert(#heat == #humidity) - - local allocs = {} - local default_biome = internal.default_biome() - - 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 - - -- Find the appropriate biome - for name, def in pairs(ns_cavegen.registered_biomes) do - local def_d = internal.euclidian( - e, def.heat_point, u, def.humidity_point - ) - - if def_d < d then - d = def_d - biome_name = name - end - end - - allocs[i] = biome_name - end - - return allocs -end - --- For each node, determine which cave shape they follow. -function internal.find_shape_allocations(connectivity, verticality) - assert(#connectivity == #verticality) - - local allocs = {} - local default_shape = internal.default_shape() - - 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 - - -- Find the appropriate shape - for name, def in pairs(ns_cavegen.registered_shapes) do - local def_d = internal.euclidian( - c, def.connectivity_point, v, def.verticality_point - ) - - if def_d < d then - d = def_d - shape_name = name - end - end - - -- Assign the chosen name - allocs[i] = shape_name - end - - return allocs -end - --- 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() - - for i in va:iterp(minp, maxp) do - -- Get shape values per item - local name = used_shapes[i] - local shape - - if name == nil then - shape = default_shape - else - shape = ns_cavegen.registered_shapes[name] - end - - if captured_shapes[shape] == nil then - -- minetest.debug("Generating new shape! " .. shape.name) - captured_shapes[shape] = internal.generate_shape(shape, minp, maxp, va) - end - - 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 - - -- minetest.debug("Finished generating shapes.") -end - -function internal.generate_connectivity_noise(minp, maxp) - return internal.generate_perlin_noise( - internal.connectivity_noise_params(), minp, maxp - ) -end - -function internal.generate_heat_noise(minp, maxp) - return internal.generate_perlin_noise( - internal.heat_noise_params(), minp, maxp - ) -end - -function internal.generate_humidity_noise(minp, maxp) - return internal.generate_perlin_noise( - internal.humidity_noise_params(), minp, maxp - ) -end - --- 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, - } - - return PerlinNoiseMap(noiseparams, size):get_3d_map_flat(minp) -end - -function internal.generate_shape(def, minp, maxp, va) - -- local noise_flat = {} - - -- 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 - - -- -- 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 - - -- return noise_flat -end - --- Generate verticality noise within given boundaries -function internal.generate_verticality_noise(minp, maxp) - return internal.generate_perlin_noise( - internal.verticality_noise_params(), minp, maxp - ) -end - --- Get the noise params for the cave biome temperature. -function internal.heat_noise_params() - return { - offset = 50, - scale = 50, - 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, - spread = BIOME_SIZE, - seed = SEED_HUMIDITY, - octaves = 2, - persistence = 0.1, - lacunarity = 2.0, - flags = "" - } -end - --- 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) - - -- Find cave shape params - local connectivity = internal.generate_connectivity_noise(bminp, bmaxp) - local verticality = internal.generate_verticality_noise(bminp, bmaxp) - - -- 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) - - -- Find cave biome params - local heat = internal.generate_heat_noise(minp, maxp) - local humidity = internal.generate_humidity_noise(minp, maxp) - - -- -- DEBUG: Write to air (or not) - -- local air = 0 - - -- 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 - - -- air = air + 1 - -- end - -- end - - -- Classify various nodes as walls, floors, ceilings - local classified_nodes = internal.classify_nodes(used_shapes, bminp, bmaxp) - - -- Draw cave biomes - local used_biomes = internal.find_biome_allocations(heat, humidity) - - -- Manipulate `data` table by adding classified nodes based on which biome - -- they're in. - local sva = VoxelArea(minp, maxp) - local data = vm:get_data() - local ground = { [ minetest.CONTENT_AIR ] = false } - - internal.write_classified_node( - data, va, used_biomes, classified_nodes.ceilings, sva, "node_roof", - ground - ) - internal.write_classified_node( - data, va, used_biomes, classified_nodes.contents, sva, "node_air", - ground - ) - internal.write_classified_node( - data, va, used_biomes, classified_nodes.floors, sva, "node_floor", - ground - ) - internal.write_classified_node( - data, va, used_biomes, classified_nodes.walls, sva, "node_wall", - ground - ) - - -- Place floor decorations - -- In case the dust has not been defined, place air nodes first - internal.write_classified_node( - data, va, used_biomes, classified_nodes.floor_decos, sva, "node_air", - ground - ) - internal.write_classified_node( - data, va, used_biomes, classified_nodes.floor_decos, sva, "node_dust", - ground - ) - local claimed_floor = internal.write_simple_floor_decorations( - data, va, used_biomes, classified_nodes.floor_decos, sva - ) - - -- Place ceiling decorations - internal.write_classified_node( - data, va, used_biomes, classified_nodes.ceiling_decos, sva, "node_air", - ground - ) - local claimed_ceiling = internal.write_simple_ceiling_decorations( - data, va, used_biomes, classified_nodes.ceiling_decos, sva - ) - - vm:set_data(data) - - -- Set schematic decorations - internal.write_schematic_floor_decoration( - vm, used_biomes, classified_nodes.floor_decos, sva, claimed_floor - ) - internal.write_schematic_ceiling_decoration( - vm, used_biomes, classified_nodes.ceiling_decos, sva, claimed_ceiling - ) - - vm:write_to_map() -end - --- Register a new biome -function internal.register_biome(biome) - biome = internal.clean_biome_def(biome) - ns_cavegen.registered_biomes[biome.name] = biome -end - --- Register a new decoration -function internal.register_decoration(deco) - table.insert( - ns_cavegen.registered_decorations, - internal.clean_deco_def(deco) - ) -end - --- Register a new shape -function internal.register_shape(shape) - shape = internal.clean_shape_def(shape) - ns_cavegen.registered_shapes[shape.name] = shape -end - --- 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)) - - if noise_values[i] == nil then - error( - "Noise value ended up nil unexpectedly" - ) - elseif noise_values[i] >= 1 - vastness then - noise_values[i] = true - else - noise_values[i] = false - end - end -end - --- Get verticality noise params -function internal.verticality_noise_params() - local factor = math.max(1, math.abs(#ns_cavegen.registered_shapes) ^ 0.5) - - return { - offset = 50, - scale = 50, - 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 - -function internal.write_classified_node(vm_data, va, used_biomes, classified_nodes, small_va, biome_key, ground_content_nodes) - local default_biome = internal.default_biome() - - local biome_to_id = {} - biome_to_id[default_biome.name] = default_biome[biome_key] or "" - - for _, i in ipairs(classified_nodes) do - local pos = small_va:position(i) - local biome = used_biomes[i] or default_biome.name - - if biome_to_id[biome] == nil then - local biome_def - - if biome == default_biome.name then - biome_def = default_biome - else - biome_def = ns_cavegen.registered_biomes[biome] or default_biome - end - - if biome_def[biome_key] == nil then - biome_to_id[biome] = "" - else - biome_to_id[biome] = minetest.get_content_id(biome_def[biome_key]) or "" - end - end - - local content_id = biome_to_id[biome] - - if content_id ~= "" then - local vi = va:index(pos.x, pos.y, pos.z) - local vm_node = vm_data[vi] - - if ground_content_nodes[vm_node] == nil then - local name = minetest.get_name_from_content_id(content_id) - ground_content_nodes[vm_node] = minetest.registered_nodes[name].is_ground_content or true - end - - if ground_content_nodes[vm_node] then - vm_data[vi] = content_id - end - end - end -end - -function internal.write_schematic_ceiling_decoration(vmanip, used_biomes, classified_nodes, sva, claimed_spots) - for _, def in pairs(ns_cavegen.registered_decorations) do - if def.deco_type == "schematic" and def.place_on == "ceiling" then - -- Place the decoration, if they're in the appropriate biome - for _, i in ipairs(classified_nodes) do - local good_biome = def.biomes == nil - local unclaimed_spot = true - - for _, ci in ipairs(claimed_spots) do - if ci == i then - unclaimed_spot = false - break - end - end - - if unclaimed_spot and not good_biome then - local current_biome = used_biomes[i] - for _, name in ipairs(def.biomes) do - if name == current_biome then - good_biome = true - break - end - end - end - - if unclaimed_spot and good_biome and math.random() < def.fill_ratio then - -- Automatically place the top at the top of the cave - local pos = sva:position(i) - local h = def.schematic - - if type(def.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 + def.place_offset_y, z = pos.z }, - def.schematic, def.rotation, def.replacement, true, - def.flags - ) - - table.insert(claimed_spots, i) - end - end - end - end - - return claimed_spots -end - -function internal.write_schematic_floor_decoration(vmanip, used_biomes, classified_nodes, sva, claimed_spots) - for _, def in pairs(ns_cavegen.registered_decorations) do - if def.deco_type == "schematic" and def.place_on == "floor" then - -- Place the decoration, if they're in the appropriate biome - for _, i in ipairs(classified_nodes) do - local good_biome = def.biomes == nil - local unclaimed_spot = true - - for _, ci in ipairs(claimed_spots) do - if ci == i then - unclaimed_spot = false - break - end - end - - if unclaimed_spot and not good_biome then - local current_biome = used_biomes[i] - for _, name in ipairs(def.biomes) do - if name == current_biome then - good_biome = true - break - end - end - end - - if unclaimed_spot and good_biome and math.random() < def.fill_ratio then - minetest.place_schematic_on_vmanip( - vmanip, sva:position(i), def.schematic, def.rotation, - def.replacement, true, def.flags - ) - - table.insert(claimed_spots, i) - end - end - end - end - - return claimed_spots -end - -function internal.write_simple_ceiling_decorations(vm_data, va, used_biomes, classified_nodes, sva) - local claimed_spots = {} - - for _, def in pairs(ns_cavegen.registered_decorations) do - if def.deco_type == "simple" and def.place_on == "ceiling" then - -- Place the decoration, if they're in the appropriate biome. - for _, i in ipairs(classified_nodes) do - local pos = sva:position(i) - local good_biome = def.biomes == nil - local unclaimed_spot = true - - for _, ci in ipairs(claimed_spots) do - if ci == i then - unclaimed_spot = false - break - end - end - - if not good_biome and unclaimed_spot then - local current_biome = used_biomes[i] - for _, name in ipairs(def.biomes) do - if name == current_biome then - good_biome = true - break - end - end - end - - if unclaimed_spot and good_biome and math.random() < def.fill_ratio then - -- Determine the height - local height = def.height - if def.height_max > height then - height = math.min( - 16, math.random(def.height, def.height_max) - ) - end - - -- Place the structure! - for h = 1, height, 1 do - local y = pos.y - h + def.place_offset_y + 1 - - if sva:contains(pos.x, y, pos.z) then - vm_data[va:index(pos.x, y, pos.z)] = minetest.get_content_id(def.decoration) - end - end - - table.insert(claimed_spots, i) - end - end - end - end - - return claimed_spots -end -function internal.write_simple_floor_decorations(vm_data, va, used_biomes, classified_nodes, sva) - local claimed_spots = {} - - for _, def in pairs(ns_cavegen.registered_decorations) do - if def.deco_type == "simple" and def.place_on == "floor" then - -- Place the decoration, if they're in the appropriate biome. - for _, i in ipairs(classified_nodes) do - local pos = sva:position(i) - local good_biome = def.biomes == nil - local unclaimed_spot = true - - for _, ci in ipairs(claimed_spots) do - if ci == i then - unclaimed_spot = false - break - end - end - - if unclaimed_spot and not good_biome then - local current_biome = used_biomes[i] - for _, name in ipairs(def.biomes) do - if name == current_biome then - good_biome = true - break - end - end - end - - if unclaimed_spot and good_biome and math.random() < def.fill_ratio then - -- Determine the height - local height = def.height - if def.height_max > height then - height = math.min( - 16, math.random(def.height, def.height_max) - ) - end - - -- Place the structure! - for h = 1, height, 1 do - local y = pos.y + h + def.place_offset_y - 1 - - if sva:contains(pos.x, y, pos.z) then - vm_data[va:index(pos.x, y, pos.z)] = minetest.get_content_id(def.decoration) - end - end - - table.insert(claimed_spots, i) - end - end - end - end - - return claimed_spots -end - -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- ---------------------------------- PUBLIC API ---------------------------------- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- - -minetest.register_on_generated(function(minp, maxp, blockseed) - if maxp.y < WORLD_DEPTH then - return - end - - local vm = minetest.get_mapgen_object("voxelmanip") - local va = VoxelArea(vm:get_emerged_area()) - - math.randomseed(blockseed) - - internal.mapgen(minp, maxp, blockseed, vm, va) -end) - -ns_cavegen = { - cave_vastness = function(pos) - if pos.y > 0 or pos.y < WORLD_DEPTH then - return 0 - end - - local y = math.abs(pos.y) - local d = math.abs(WORLD_DEPTH / 2) - return 1 - (math.abs(y - d) / d) - end, - - register_biome = internal.register_biome, - - register_decoration = internal.register_decoration, - - register_shape = internal.register_shape, - - registered_biomes = {}, - - registered_decorations = {}, - - registered_shapes = {}, -} - -dofile(minetest.get_modpath(minetest.get_current_modname()) .. "/lua/register.lua") \ No newline at end of file +minetest.register_mapgen_script( + minetest.get_modpath(minetest.get_current_modname()) .. "/script.lua" +) \ No newline at end of file diff --git a/lua/register.lua b/lua/register.lua deleted file mode 100644 index 74381b9..0000000 --- a/lua/register.lua +++ /dev/null @@ -1,84 +0,0 @@ -ns_cavegen.register_shape({ - name = "ns_cavegen:bubbles", - noise_params = { - offset = -0.2, - scale = 0.5, - spread = { x = 100, y = 100, z = 100 }, - seed = 248039, - octaves = 2, - persistence = 0.6, - lacunarity = 2.0, - flags = "eased", - }, - y_min = -31000, - y_max = 0, - connectivity_point = 10, - verticality_point = 10, -}) - -ns_cavegen.register_shape({ - name = "ns_cavegen:cliffs", - noise_params = { - offset = -0.4, - scale = 0.9, - spread = { x = 30, y = 200, z = 30 }, - seed = 92742002, - octaves = 2, - persistence = 0.6, - lacunarity = 3.0, - flags = "eased", - }, - y_min = -31000, - y_max = 0, - connectivity_point = 20, - verticality_point = 80, -}) - -ns_cavegen.register_biome({ - name = "ns_cavegen:snow", - node_dust = "mcl_core:snow", - node_floor = "mcl_crimson:shroomlight", - node_wall = "mcl_core:cobble", - node_roof = "dripstone:dry_dripstone_block", - node_air = "air", - heat_point = 0, - humidity_point = 50, -}) - -ns_cavegen.register_biome({ - name = "ns_cavegen:drip", - node_floor = "dripstone:dry_dripstone_block", - node_wall = "dripstone:dry_dripstone_block", - node_roof = "dripstone:dry_dripstone_block", - heat_point = 50, - humidity_point = 0, -}) - -ns_cavegen.register_decoration({ - deco_type = "simple", - place_on = "ceiling", - fill_ratio = 0.25, - biomes = { "ns_cavegen:snow" }, - decoration = "mcl_core:water_source", - height = 1, - place_offset_y = 2, -}) - -ns_cavegen.register_decoration({ - deco_type = "simple", - place_on = "ceiling", - fill_ratio = 0.025, - decoration = "mcl_colorblocks:concrete_green", - height = 3, - height_max = 7, -}) - -ns_cavegen.register_decoration({ - deco_type = "simple", - place_on = "floor", - fill_ratio = 0.025, - decoration = "mcl_bamboo:bamboo_block", - height = 3, - height_max = 7, -}) - diff --git a/lua/timer.lua b/lua/timer.lua deleted file mode 100644 index a6a3773..0000000 --- a/lua/timer.lua +++ /dev/null @@ -1,139 +0,0 @@ -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() diff --git a/script.lua b/script.lua new file mode 100644 index 0000000..626a10d --- /dev/null +++ b/script.lua @@ -0,0 +1,886 @@ +-- This file is executed in the mapgen environment. + +-- Average size of each cave biome +local BIOME_SIZE = { x = 250, y = 250, z = 250 } + +-- 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 } + +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 + +-- 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 + +-- Average size of each cave shape +local SHAPE_SIZE = { x = 128, y = 128, z = 128 } + +-- Several seeds for mapgen +local SEED_CONNECTIVITY = 297948 +local SEED_HEAT = 320523 +local SEED_HUMIDITY = 9923473 +local SEED_VERTICALITY = 35644 + +-- 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 } + +-- Lower bound for cave generation +local WORLD_MINP = { x = -1e6, y = -1e6, z = -1e6 } + +-- Upper bound for cave generation +local WORLD_MAXP = { x = 1e6, y = 1e6, z = 1e6 } + +local WORLD_DEPTH = -60 + +local internal = {} + +------------------------------------------------------------------------------- +------------------------------------------------------------------------------- +-------------------------------- INTERNAL API --------------------------------- +------------------------------------------------------------------------------- +------------------------------------------------------------------------------- + +function internal.cave_vastness(pos) + local v = ns_cavegen.cave_vastness(pos) + + if not v then + return 0 + elseif v < 0 then + return 0 + elseif v > 1 then + return 1 + else + return v + end +end + +-- Classify all nodes in the chunk into what they are +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) + + local ceiling_decos = {} + local ceilings = {} + local contents = {} + local floor_decos = {} + local floors = {} + local walls = {} + + for i in sva:iterp(sminp, smaxp) do + local pos = sva:position(i) + + if used_shapes[va:index(pos.x, pos.y, pos.z)] == true then + -- Part of cave + if not used_shapes[va:index(pos.x, pos.y + 1, pos.z)] then + table.insert(ceiling_decos, i) + elseif not used_shapes[va:index(pos.x, pos.y - 1, pos.z)] then + table.insert(floor_decos, i) + else + table.insert(contents, i) + end + else + -- Not part of cave + if used_shapes[va:index(pos.x, pos.y + 1, pos.z)] then + table.insert(floors, i) + elseif used_shapes[va:index(pos.x, pos.y - 1, pos.z)] then + table.insert(ceilings, i) + elseif used_shapes[va:index(pos.x - 1, pos.y, pos.z)] then + table.insert(walls, i) + elseif used_shapes[va:index(pos.x + 1, pos.y, pos.z)] then + table.insert(walls, i) + elseif used_shapes[va:index(pos.x, pos.y, pos.z - 1)] then + table.insert(walls, i) + elseif used_shapes[va:index(pos.x, pos.y, pos.z + 1)] then + table.insert(walls, i) + end + end + end + + return { + ceiling_decos = ceiling_decos, + ceilings = ceilings, + contents = contents, + floor_decos = floor_decos, + floors = floors, + walls = walls, + } +end + +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") + + 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 + + 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 } + + 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") + + def.node_air = def.node_air or "air" + + 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") + + return def +end + +function internal.clean_deco_def(def) + def.deco_type = def.deco_type or "simple" + def.place_on = def.place_on or "floor" + def.y_min = def.y_min or WORLD_MINP.y + def.y_max = def.y_max or WORLD_MAXP.y + + assert(def.deco_type == "simple" or def.deco_type == "schematic") + assert(def.place_on == "floor" or def.place_on == "ceiling") + assert(type(def.fill_ratio) == "number") + assert(def.biomes == nil or type(def.biomes) == "table") + + if def.deco_type == "simple" then + def.height = def.height or 1 + def.height_max = def.height_max or def.height + def.place_offset_y = def.place_offset_y or 0 + + assert(type(def.deco_type) == "string") + assert(type(def.height) == "number") + assert(type(def.height_max) == "number") + assert(type(def.place_offset_y) == "number") + + elseif def.deco_type == "schematic" then + def.replacements = def.replacements or {} + def.place_offset_y = def.place_offset_y or 0 + + assert(type(def.schematic) == "string" or type(def.schematic) == "table") + assert(type(def.replacements) == "table") + + assert(def.rotation == "0" or def.rotation == "90" or def.rotation == "180" or def.rotation == "270" or def.rotation == "random") + assert(type(def.place_offset_y) == "number") + end + + return def +end + +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") + + 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 + + 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") + + return def +end + +-- Get connectivity noise params +function internal.connectivity_noise_params() + local factor = math.max(1, math.abs(#ns_cavegen.registered_shapes) ^ 0.5) + + return { + offset = 50, + scale = 50, + 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 = "ns_cavegen:default_biome" + , 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 = "ns_cavegen:none" + , connectivity_point = OUTLANDISH_POINT + , verticality_point = OUTLANDISH_POINT + , func = function (pos, v) return 0 end + } + ) +end + +-- 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 + +-- For each node, determine which cave biome they're in. +function internal.find_biome_allocations(heat, humidity) + assert(#heat == #humidity) + + local allocs = {} + local default_biome = internal.default_biome() + + 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 + + -- Find the appropriate biome + for name, def in pairs(ns_cavegen.registered_biomes) do + local def_d = internal.euclidian( + e, def.heat_point, u, def.humidity_point + ) + + if def_d < d then + d = def_d + biome_name = name + end + end + + allocs[i] = biome_name + end + + return allocs +end + +-- For each node, determine which cave shape they follow. +function internal.find_shape_allocations(connectivity, verticality) + assert(#connectivity == #verticality) + + local allocs = {} + local default_shape = internal.default_shape() + + 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 + + -- Find the appropriate shape + for name, def in pairs(ns_cavegen.registered_shapes) do + local def_d = internal.euclidian( + c, def.connectivity_point, v, def.verticality_point + ) + + if def_d < d then + d = def_d + shape_name = name + end + end + + -- Assign the chosen name + allocs[i] = shape_name + end + + return allocs +end + +-- 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() + + for i in va:iterp(minp, maxp) do + -- Get shape values per item + local name = used_shapes[i] + local shape + + if name == nil then + shape = default_shape + else + shape = ns_cavegen.registered_shapes[name] + end + + if captured_shapes[shape] == nil then + -- minetest.debug("Generating new shape! " .. shape.name) + captured_shapes[shape] = internal.generate_shape(shape, minp, maxp, va) + end + + 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 + + -- minetest.debug("Finished generating shapes.") +end + +function internal.generate_connectivity_noise(minp, maxp) + return internal.generate_perlin_noise( + internal.connectivity_noise_params(), minp, maxp + ) +end + +function internal.generate_heat_noise(minp, maxp) + return internal.generate_perlin_noise( + internal.heat_noise_params(), minp, maxp + ) +end + +function internal.generate_humidity_noise(minp, maxp) + return internal.generate_perlin_noise( + internal.humidity_noise_params(), minp, maxp + ) +end + +-- 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, + } + + return PerlinNoiseMap(noiseparams, size):get_3d_map_flat(minp) +end + +function internal.generate_shape(def, minp, maxp, va) + -- local noise_flat = {} + + -- 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 + + -- -- 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 + + -- return noise_flat +end + +-- Generate verticality noise within given boundaries +function internal.generate_verticality_noise(minp, maxp) + return internal.generate_perlin_noise( + internal.verticality_noise_params(), minp, maxp + ) +end + +-- Get the noise params for the cave biome temperature. +function internal.heat_noise_params() + return { + offset = 50, + scale = 50, + 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, + spread = BIOME_SIZE, + seed = SEED_HUMIDITY, + octaves = 2, + persistence = 0.1, + lacunarity = 2.0, + flags = "" + } +end + +-- Take all necessary steps to execute the mapgen +function internal.mapgen(vm, minp, maxp, blockseed) + -- 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) + local va = VoxelArea(vm:get_emerged_area()) + math.randomseed(blockseed) + + -- Find cave shape params + local connectivity = internal.generate_connectivity_noise(bminp, bmaxp) + local verticality = internal.generate_verticality_noise(bminp, bmaxp) + + -- 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) + + -- Find cave biome params + local heat = internal.generate_heat_noise(minp, maxp) + local humidity = internal.generate_humidity_noise(minp, maxp) + + -- -- DEBUG: Write to air (or not) + -- local air = 0 + + -- 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 + + -- air = air + 1 + -- end + -- end + + -- Classify various nodes as walls, floors, ceilings + local classified_nodes = internal.classify_nodes(used_shapes, bminp, bmaxp) + + -- Draw cave biomes + local used_biomes = internal.find_biome_allocations(heat, humidity) + + -- Manipulate `data` table by adding classified nodes based on which biome + -- they're in. + local sva = VoxelArea(minp, maxp) + local data = vm:get_data() + local ground = { [ minetest.CONTENT_AIR ] = false } + + internal.write_classified_node( + data, va, used_biomes, classified_nodes.ceilings, sva, "node_roof", + ground + ) + internal.write_classified_node( + data, va, used_biomes, classified_nodes.contents, sva, "node_air", + ground + ) + internal.write_classified_node( + data, va, used_biomes, classified_nodes.floors, sva, "node_floor", + ground + ) + internal.write_classified_node( + data, va, used_biomes, classified_nodes.walls, sva, "node_wall", + ground + ) + + -- Place floor decorations + -- In case the dust has not been defined, place air nodes first + internal.write_classified_node( + data, va, used_biomes, classified_nodes.floor_decos, sva, "node_air", + ground + ) + internal.write_classified_node( + data, va, used_biomes, classified_nodes.floor_decos, sva, "node_dust", + ground + ) + local claimed_floor = internal.write_simple_floor_decorations( + data, va, used_biomes, classified_nodes.floor_decos, sva + ) + + -- Place ceiling decorations + internal.write_classified_node( + data, va, used_biomes, classified_nodes.ceiling_decos, sva, "node_air", + ground + ) + local claimed_ceiling = internal.write_simple_ceiling_decorations( + data, va, used_biomes, classified_nodes.ceiling_decos, sva + ) + + vm:set_data(data) + + -- Set schematic decorations + internal.write_schematic_floor_decoration( + vm, used_biomes, classified_nodes.floor_decos, sva, claimed_floor + ) + internal.write_schematic_ceiling_decoration( + vm, used_biomes, classified_nodes.ceiling_decos, sva, claimed_ceiling + ) + + -- vm:write_to_map() +end + +-- Register a new biome +function internal.register_biome(biome) + biome = internal.clean_biome_def(biome) + ns_cavegen.registered_biomes[biome.name] = biome +end + +-- Register a new decoration +function internal.register_decoration(deco) + table.insert( + ns_cavegen.registered_decorations, + internal.clean_deco_def(deco) + ) +end + +-- Register a new shape +function internal.register_shape(shape) + shape = internal.clean_shape_def(shape) + ns_cavegen.registered_shapes[shape.name] = shape +end + +-- 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)) + + if noise_values[i] == nil then + error( + "Noise value ended up nil unexpectedly" + ) + elseif noise_values[i] >= 1 - vastness then + noise_values[i] = true + else + noise_values[i] = false + end + end +end + +-- Get verticality noise params +function internal.verticality_noise_params() + local factor = math.max(1, math.abs(#ns_cavegen.registered_shapes) ^ 0.5) + + return { + offset = 50, + scale = 50, + 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 + +function internal.write_classified_node(vm_data, va, used_biomes, classified_nodes, small_va, biome_key, ground_content_nodes) + local default_biome = internal.default_biome() + + local biome_to_id = {} + biome_to_id[default_biome.name] = default_biome[biome_key] or "" + + for _, i in ipairs(classified_nodes) do + local pos = small_va:position(i) + local biome = used_biomes[i] or default_biome.name + + if biome_to_id[biome] == nil then + local biome_def + + if biome == default_biome.name then + biome_def = default_biome + else + biome_def = ns_cavegen.registered_biomes[biome] or default_biome + end + + if biome_def[biome_key] == nil then + biome_to_id[biome] = "" + else + biome_to_id[biome] = minetest.get_content_id(biome_def[biome_key]) or "" + end + end + + local content_id = biome_to_id[biome] + + if content_id ~= "" then + local vi = va:index(pos.x, pos.y, pos.z) + local vm_node = vm_data[vi] + + if ground_content_nodes[vm_node] == nil then + local name = minetest.get_name_from_content_id(content_id) + ground_content_nodes[vm_node] = minetest.registered_nodes[name].is_ground_content or true + end + + if ground_content_nodes[vm_node] then + vm_data[vi] = content_id + end + end + end +end + +function internal.write_schematic_ceiling_decoration(vmanip, used_biomes, classified_nodes, sva, claimed_spots) + for _, def in pairs(ns_cavegen.registered_decorations) do + if def.deco_type == "schematic" and def.place_on == "ceiling" then + -- Place the decoration, if they're in the appropriate biome + for _, i in ipairs(classified_nodes) do + local good_biome = def.biomes == nil + local unclaimed_spot = true + + for _, ci in ipairs(claimed_spots) do + if ci == i then + unclaimed_spot = false + break + end + end + + if unclaimed_spot and not good_biome then + local current_biome = used_biomes[i] + for _, name in ipairs(def.biomes) do + if name == current_biome then + good_biome = true + break + end + end + end + + if unclaimed_spot and good_biome and math.random() < def.fill_ratio then + -- Automatically place the top at the top of the cave + local pos = sva:position(i) + local h = def.schematic + + if type(def.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 + def.place_offset_y, z = pos.z }, + def.schematic, def.rotation, def.replacement, true, + def.flags + ) + + table.insert(claimed_spots, i) + end + end + end + end + + return claimed_spots +end + +function internal.write_schematic_floor_decoration(vmanip, used_biomes, classified_nodes, sva, claimed_spots) + for _, def in pairs(ns_cavegen.registered_decorations) do + if def.deco_type == "schematic" and def.place_on == "floor" then + -- Place the decoration, if they're in the appropriate biome + for _, i in ipairs(classified_nodes) do + local good_biome = def.biomes == nil + local unclaimed_spot = true + + for _, ci in ipairs(claimed_spots) do + if ci == i then + unclaimed_spot = false + break + end + end + + if unclaimed_spot and not good_biome then + local current_biome = used_biomes[i] + for _, name in ipairs(def.biomes) do + if name == current_biome then + good_biome = true + break + end + end + end + + if unclaimed_spot and good_biome and math.random() < def.fill_ratio then + minetest.place_schematic_on_vmanip( + vmanip, sva:position(i), def.schematic, def.rotation, + def.replacement, true, def.flags + ) + + table.insert(claimed_spots, i) + end + end + end + end + + return claimed_spots +end + +function internal.write_simple_ceiling_decorations(vm_data, va, used_biomes, classified_nodes, sva) + local claimed_spots = {} + + for _, def in pairs(ns_cavegen.registered_decorations) do + if def.deco_type == "simple" and def.place_on == "ceiling" then + -- Place the decoration, if they're in the appropriate biome. + for _, i in ipairs(classified_nodes) do + local pos = sva:position(i) + local good_biome = def.biomes == nil + local unclaimed_spot = true + + for _, ci in ipairs(claimed_spots) do + if ci == i then + unclaimed_spot = false + break + end + end + + if not good_biome and unclaimed_spot then + local current_biome = used_biomes[i] + for _, name in ipairs(def.biomes) do + if name == current_biome then + good_biome = true + break + end + end + end + + if unclaimed_spot and good_biome and math.random() < def.fill_ratio then + -- Determine the height + local height = def.height + if def.height_max > height then + height = math.min( + 16, math.random(def.height, def.height_max) + ) + end + + -- Place the structure! + for h = 1, height, 1 do + local y = pos.y - h + def.place_offset_y + 1 + + if sva:contains(pos.x, y, pos.z) then + vm_data[va:index(pos.x, y, pos.z)] = minetest.get_content_id(def.decoration) + end + end + + table.insert(claimed_spots, i) + end + end + end + end + + return claimed_spots +end +function internal.write_simple_floor_decorations(vm_data, va, used_biomes, classified_nodes, sva) + local claimed_spots = {} + + for _, def in pairs(ns_cavegen.registered_decorations) do + if def.deco_type == "simple" and def.place_on == "floor" then + -- Place the decoration, if they're in the appropriate biome. + for _, i in ipairs(classified_nodes) do + local pos = sva:position(i) + local good_biome = def.biomes == nil + local unclaimed_spot = true + + for _, ci in ipairs(claimed_spots) do + if ci == i then + unclaimed_spot = false + break + end + end + + if unclaimed_spot and not good_biome then + local current_biome = used_biomes[i] + for _, name in ipairs(def.biomes) do + if name == current_biome then + good_biome = true + break + end + end + end + + if unclaimed_spot and good_biome and math.random() < def.fill_ratio then + -- Determine the height + local height = def.height + if def.height_max > height then + height = math.min( + 16, math.random(def.height, def.height_max) + ) + end + + -- Place the structure! + for h = 1, height, 1 do + local y = pos.y + h + def.place_offset_y - 1 + + if sva:contains(pos.x, y, pos.z) then + vm_data[va:index(pos.x, y, pos.z)] = minetest.get_content_id(def.decoration) + end + end + + table.insert(claimed_spots, i) + end + end + end + end + + return claimed_spots +end + +------------------------------------------------------------------------------- +------------------------------------------------------------------------------- +--------------------------------- PUBLIC API ---------------------------------- +------------------------------------------------------------------------------- +------------------------------------------------------------------------------- + +minetest.register_on_generated(internal.mapgen) + +ns_cavegen = { + cave_vastness = function(pos) + if pos.y > 0 or pos.y < WORLD_DEPTH then + return 0 + end + + local y = math.abs(pos.y) + local d = math.abs(WORLD_DEPTH / 2) + return 1 - (math.abs(y - d) / d) + end, + + register_biome = internal.register_biome, + + register_decoration = internal.register_decoration, + + register_shape = internal.register_shape, + + registered_biomes = {}, + + registered_decorations = {}, + + registered_shapes = {}, +} + +-- dofile(minetest.get_modpath(minetest.get_current_modname()) .. "/lua/register.lua") \ No newline at end of file