diff --git a/API.md b/API.md index bce791c..a00f2e3 100644 --- a/API.md +++ b/API.md @@ -14,6 +14,9 @@ For shapes, the following functions are available: - `noordstar_caves.unregister_shape(name)` Remove a defined cave shape - `noordstar_caves.clear_registered_shapes()` Remove all known cave shapes +Generally, it is recommended to keep the number of cave shapes below 100. +A good number of shapes is 10 for diversity but performance. + The shapes are defined as follows: ```lua @@ -132,12 +135,6 @@ The biomes are defined as follows: -- Keep in mind that cave biomes can blend, so it might not always look -- very smooth. - node_shell = "foo:permafrost", - depth_shell = 3, - -- Node forming a layer around the entire cave and thickness of this layer - -- You can make the depth as high as you want, but raising it past 16 - -- might cause hard cut-offs at chunk edges. - y_max = -100, y_min = -31000, -- Upper and lower limits of the cave biome. @@ -160,3 +157,121 @@ The biomes are defined as follows: -- but can exceed these values. } ``` + +## Decorations + +As a final part of generating the caves, decorations can be added to add unique +structures to cave biomes. + +For decorations, the following functions are defined: + +- `noordstar_caves.register_decoration(decoration def)` Define a new cave decoration +- `noordstar_caves.clear_registered_decorations()` Remove all known cave decorations + +The decorations are defined as follows: + +```lua +{ + deco_type = "simple", + -- Type. "simple" or "schematic" supported + + place_on = "floor", + -- Side of the cave to place the decoration on. "floor" or "ceiling" supported + + fill_ratio = 0.02, + -- Percentage of surface nodes on which this decoration will spawn + + biomes = { "noordstar_caves:tundra", "foo:desert" }, + -- List of (cave!) biomes that this decoration will spawn in. Occurs in all + -- biomes if this is omitted. + + y_min = -31000, + y_max = 31000, + -- Lower and upper limits for decoration (inclusive). + -- These parameters refer to the Y-coordinate of the node where it is + -- originally chosen to be placed. + + ----- Simple-type parameters + + decoration = "foo:grass", + -- The node name used as the decoration. + -- If instead a list of strings, a randomly selected node from the list + -- is placed as the decoration. + + height = 1, + -- Decoration height in nodes. + -- If height_max is not 0, this is the lower limit of a randomly selected + -- height. + -- Height can not be over 16. + + height_max = 0, + -- Upper limit of the randomly selected height. + -- If absent, the parameter `height` is used as a constant. + -- Max height will be capped at 16. + + place_offset_y = 0, + -- Y offset of the decoration base node relative to the standard base node + -- position. + -- Can be positive or negative. Default is 0. + + ----- Schematic-type parameters + + schematic = "foobar.mts", + -- If schematic is a string, it is the filepath relative to the correct + -- working directory of the specified Minetest schematic file. + -- Could also be the ID of a previously registered schematic. + + schematic = { + size = {x = 4, y = 6, z = 4}, + data = { + {name = "default:cobble", param1 = 255, param2 = 0}, + {name = "default:dirt_with_grass", param1 = 255, param2 = 0}, + {name = "air", param1 = 255, param2 = 0}, + ... + }, + yslice_prob = { + {ypos = 2, prob = 128}, + {ypos = 5, prob = 64}, + ... + }, + }, + -- Alternative schematic specification by supplying a table. The fields + -- size and data are mandatory whereas yslice_prob is optional. + -- See 'schematic specifier' in the Minetest Lua API documentation. + + + replacements = {["oldname"] = "convert_to", ...}, + -- Map of node names to replace in the schematic after reading it. + + flags = "place_center_x, place_center_y, place_center_z", + -- Flags for schematic decorations. See 'Schematic attributes'. + + rotation = "90", + -- Rotation can be "0", "90", "180", "270", or "random" + + place_offset_y = 0, + -- If the flag 'place_center_y' is set this parameter is ignored. + -- Y offset of the schematic base node layer relative to the 'place_on' + -- node. + -- Can be positive or negative. Default is 0. + -- Effect is NOT inverted for decorations on the ceiling. + -- Ignored by 'y_min' and 'y_max' checks, which always refer to the + -- 'place_on' node. +} +``` + +## Ores + +This mod does not support adding ores to the caves, as this is a feature that +is already well-supported by the Minetest Lua API and doesn't need a mod like +this. + +## Misc + +In case you wish to do a few other operations, here's a few other functions +that might be helpful to you: + +- `noordstar_caves.set_world_height(h)` Set the world's height to a given number. + +This feature is currently only supported in VoxeLibre. Contributions that help +change the world depth in other games, are very welcome. diff --git a/init.lua b/init.lua index e60370b..2314bf5 100644 --- a/init.lua +++ b/init.lua @@ -92,6 +92,9 @@ noordstar_caves = -- A public list of all registered biomes , registered_biomes = {} + -- A public list of all registered decorations + , registered_decorations = {} + -- A public list of all registered shapes , registered_shapes = {} @@ -102,6 +105,11 @@ function noordstar_caves.clear_registered_biomes() noordstar_caves.registered_biomes = {} end +-- Remove all registered decorations and start with a clean slate +function noordstar_caves.clear_registered_decoration() + noordstar_caves.registered_decorations = {} +end + -- Remove all registered shapes and start with a clean slate function noordstar_caves.clear_registered_shapes() noordstar_caves.registered_shapes = {} @@ -116,6 +124,15 @@ function noordstar_caves.register_biome(def) end end +-- Register a new cave decoration +function noordstar_caves.register_decoration(def) + local d = internal.clean_deco_def(def) + + if d then + table.insert(noordstar_caves.registered_decorations, d) + end +end + -- Register a new cave shape function noordstar_caves.register_shape(def) local d = internal.clean_shape_def(def) @@ -257,6 +274,64 @@ function internal.clean_biome_def(def) return d end +-- Clean the user input on a decoration definition before inserting it. +function internal.clean_deco_def(def) + if def.deco_type ~= "simple" and def.deco_type ~= "schematic" then + return nil + end + if def.deco_type == "simple" and type(def.decoration) ~= "string" then + return nil + end + if def.deco_type == "schematic" then + if type(def.schematic) == "string" then + elseif type(def.schematic) == "table" then + else + return nil + end + end + if type(def.fill_ratio) ~= "number" then + return nil + end + + local d = { + deco_type = def.deco_type, + decoration = def.decoration, + schematic = def.schematic, + fill_ratio = def.fill_ratio, + biomes = def.biomes, + } + + if def.place_on == "floor" or def.place_on == "ceiling" then + d.place_on = def.place_on + else + d.place_on = "floor" + end + + local function place(key, t, default) + if type(def[key]) == t then + d[key] = def[key] + else + d[key] = default + end + end + + place("y_min", "number", internal.world_minp.y) + place("y_max", "number", internal.world_maxp.y) + place("height_max", "number", 0) + place("height", "number", 1) + place("place_offset_y", "number", 0) + place("replacements", "table", {}) + place("flags", "string", "place_center_x,place_center_z") + place("rotation", "string", "0") + + d.height = math.max(d.height , 0) + d.height = math.min(d.height , 16) + d.height_max = math.max(d.height_max, 0) + d.height_max = math.min(d.height_max, 16) + + return d +end + -- Clean the user input on a shape definition before inserting it. function internal.clean_shape_def(def) if type(def.name) ~= "string" then @@ -292,16 +367,18 @@ function internal.clean_shape_def(def) end -- Get the most nearby cave biome -function internal.closest_cave_biome(heat, humidity) +function internal.closest_cave_biome(heat, humidity, pos) local biome = internal.default_biome() local d = internal.biome_def_distance(biome, heat, humidity) for _, def in pairs(noordstar_caves.registered_biomes) do - local new_d = internal.biome_def_distance(def, heat, humidity) - - if new_d <= d then - biome = def - d = new_d + if internal.is_valid_pos(pos, def.minp, def.maxp) then + local new_d = internal.biome_def_distance(def, heat, humidity) + + if new_d <= d then + biome = def + d = new_d + end end end @@ -309,16 +386,18 @@ function internal.closest_cave_biome(heat, humidity) end -- Get the most nearby cave shape -function internal.closest_cave_shape(cnct, vrtcl) +function internal.closest_cave_shape(cnct, vrtcl, y) local shape = internal.default_shape() local d = internal.shape_def_distance(shape, cnct, vrtcl) for key, def in pairs(noordstar_caves.registered_shapes) do - local new_d = internal.shape_def_distance(def, cnct, vrtcl) - - if new_d <= d then - shape = def - d = new_d + if def.y_min <= y and y <= def.y_max then + local new_d = internal.shape_def_distance(def, cnct, vrtcl) + + if new_d <= d then + shape = def + d = new_d + end end end @@ -442,7 +521,7 @@ function internal.flat_from_cave_bools(minp, maxp) local cnct = connectivity:get_index(i) local vrtcl = verticality:get_index(i) - local def = internal.closest_cave_shape(cnct, vrtcl) + local def = internal.closest_cave_shape(cnct, vrtcl, pos.y) return def.name end) @@ -496,6 +575,11 @@ function internal.flat_from_node_types(bools, minp, maxp) return nt.floor_deco end + if bools:get_index(i + bools.up) == false then + -- Block is near the ceiling + return nt.ceiling_deco + end + return nt.air else -- NOT PART OF CAVE @@ -614,6 +698,8 @@ function internal.generate_caves(data, minp, maxp) -- Get cave bools local bools = internal.flat_from_cave_bools(bminp, bmaxp) + timer.checkpoint("Calculate cave shape") + -- Get node types local nts = internal.flat_from_node_types(bools, bminp, bmaxp) @@ -625,13 +711,16 @@ function internal.generate_caves(data, minp, maxp) internal.humidity_noise_params(), bminp, bmaxp ) + timer.checkpoint("Calculate block types & biomes") + local air = minetest.get_content_id("air") + local schems = {} -- Place blocks where necessary internal.iter_3d_area(bminp, bmaxp, function (i, pos) + local vi = vmanip:pos_to_index(pos) local function place(name) - local vi = vmanip:pos_to_index(pos) if vmanip:get_index(vi) == air then elseif type(name) == "string" then @@ -648,14 +737,12 @@ function internal.generate_caves(data, minp, maxp) 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) + local def = internal.closest_cave_biome(heat, humidity, pos) if nt == internal.node_types.floor then place(def.node_floor) @@ -663,6 +750,41 @@ function internal.generate_caves(data, minp, maxp) place(def.node_wall) elseif nt == internal.node_types.ceiling then place(def.node_roof) + elseif nt == internal.node_types.floor_deco or nt == internal.node_types.ceiling_deco then + local schem_placed = false + local appropriate_place = "floor" + if nt == internal.node_types.ceiling_deco then + appropriate_place = "ceiling" + end + + if not schem_placed then + -- Prevent decorations from spawning in the air + if vmanip:get_index(vi) == air then + schem_placed = true + end + end + + if not schem_placed then + for _, deco in ipairs(noordstar_caves.registered_decorations) do + if deco.place_on ~= appropriate_place then + elseif math.random() > deco.fill_ratio then + elseif not internal.is_deco_biome(deco, def.name) then + else + table.insert(schems, { pos = pos, deco = deco }) + schem_placed = true + break + end + end + end + + if not schem_placed then + local n = "air" + if nt == internal.node_types.floor_deco then + n = def.node_dust or n + end + place(n) + schem_placed = true + end else error( table.concat( @@ -679,12 +801,12 @@ function internal.generate_caves(data, minp, maxp) end end) - return vmanip.arr + return vmanip, schems end -- Get the noise params for the cave biome temperature. function internal.heat_noise_params() - return { + return { offset = 50, scale = 50, spread = internal.biome_size, @@ -715,6 +837,23 @@ function internal.is_cave_node(noise, vastness) return noise > 1 - vastness end +-- Return whether a given decoration definition can spawn in a given biome name +function internal.is_deco_biome(def, biome_name) + if type(def.biomes) == "nil" then + return true + elseif type(def.biomes) == "string" then + return def.biomes == biome_name + elseif type(def.biomes) == "table" then + for _, name in pairs(def.biomes) do + if name == biome_name then + return true + end + end + end + + return false +end + -- Return a bool whether a position is within a given volume function internal.is_valid_pos(pos, minp, maxp) if minp.x <= pos.x and pos.x <= maxp.x then @@ -785,6 +924,85 @@ end -- minetest.chat_send_all(os.time() .. " - " .. text) -- end +-- Place all schematic decorations into the world +function internal.place_schematic_decorations(vmanip, schems) + for _, schem in ipairs(schems) do + local pos = { + x = schem.pos.x, + y = schem.pos.y + schem.deco.place_offset_y, + z = schem.pos.z, + } + local deco = schem.deco + + if deco.deco_type ~= "schematic" then + else + if deco.place_on == "floor" then + minetest.place_schematic_on_vmanip( + vmanip, pos, + deco.schematic, deco.rotation, deco.replacement, + true, deco.flags + ) + elseif deco.place_on == "ceiling" then + local h = deco.schematic + + if type(deco.schematic) == "string" then + h = minetest.read_schematic(h, {}) + + if type(h) == "nil" then + error("Could not find schematic! Perhaps it the filename is incorrect?") + end + end + + h = h.size.y + + minetest.place_schematic_on_vmanip( + vmanip, { x = pos.x, y = pos.y - h + 1, z = pos.z }, + deco.schematic, deco.rotation, deco.replacement, + true, deco.flags + ) + else + error("Unknown place_on value `" .. deco.place_on .. "`") + end + end + end +end + +-- Place all simple decorations into the world +function internal.place_simple_decorations(flat, schems) + for _, schem in ipairs(schems) do + if schem.deco.deco_type == "simple" then + local pos = { + x = schem.pos.x, + y = schem.pos.y + schem.deco.place_offset_y, + z = schem.pos.z, + } + local deco = schem.deco + + local i = flat:pos_to_index(pos) + + local dir = nil + + if deco.place_on == "floor" then + dir = flat.up + elseif deco.place_on == "ceiling" then + dir = flat.down + else + error("Invalid place_on value `" .. deco.place_on .. "`") + end + + local h_min = deco.height + local h_max = math.max(deco.height, deco.height_max) + + for dy = 0, math.random(h_min, h_max) - 1, 1 do + local li = i + dir * dy + + flat:set_index(li, minetest.get_content_id(deco.decoration)) + end + + end + end +end + -- Helper function to convert a set of coordinates to a readable string function internal.pos_to_str(pos) return "(" .. pos.x .. ", " .. pos.y .. ", " .. pos.z .. " )" @@ -1006,13 +1224,25 @@ end ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- minetest.register_on_generated(function(minp, maxp, blockseed) + timer.start() + + math.randomseed(blockseed) + local vm = minetest.get_mapgen_object("voxelmanip") local data = vm:get_data() - data = internal.generate_caves(data, minp, maxp) + local flat, schems = internal.generate_caves(data, minp, maxp) + timer.checkpoint("Place caves") - vm:set_data(data) + internal.place_simple_decorations(flat, schems) + + vm:set_data(flat.arr) vm:write_to_map() + + internal.place_schematic_decorations(vm, schems) + timer.checkpoint("Place decorations") + + timer.stop() end) ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- @@ -1057,75 +1287,129 @@ noordstar_caves.register_shape({ connectivity_point = 10, verticality_point = 40, }) --- noordstar_caves.register_shape({ --- name = "noordstar_caves:cliffs", +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 = "" --- }, + 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, + func = function(pos, n) + return n + end, --- connectivity_point = 30, --- verticality_point = 80, --- }) --- noordstar_caves.register_shape({ --- name = "noordstar_caves:donuts", + 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" --- }, + 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, + 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", + connectivity_point = 50, + verticality_point = 40, +}) +noordstar_caves.register_shape({ + name = "noordstar_caves:wall", --- func = function(pos, n) --- return -0.5 --- end, + func = function(pos, n) + return -0.5 + end, --- connectivity_point = 0, --- verticality_point = 0, --- }) + 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", + name = "nc:glowing_floor", node_floor = "mcl_core:crying_obsidian", node_wall = "mcl_core:sand", node_roof = "mcl_ocean:sea_lantern", - heat_point = 0, + node_dust = "mcl_core:snow", + heat_point = 20, humidity_point = 0, }) noordstar_caves.register_biome({ - name = "test2", + name = "nc:crazy_glowin_walls", node_floor = "mcl_amethyst:amethyst_block", node_wall = "mcl_crimson:shroomlight", node_roof = "mcl_colorblocks:glazed_terracotta_silver", heat_point = 100, + humidity_point = 0, +}) +noordstar_caves.register_biome({ + name = "nc:regular", + heat_point = 50, + humidity_point = 50, +}) +noordstar_caves.register_biome({ + name = "nc:cold", + node_dust = "mcl_core:snow", + node_roof = "mcl_core:ice", + heat_point = 10, humidity_point = 100, }) +noordstar_caves.register_biome({ + name = "nc:grass", + node_floor = "mcl_core:dirt_with_grass", + heat_point = 50, + humidity_point = 70, +}) +noordstar_caves.register_biome({ + name = "nc:dangerous_lava", + node_floor = "mcl_core:lava_source", + y_max = -40, + heat_point = 100, + humidity_point = 100, +}) + +noordstar_caves.register_decoration({ + deco_type = "simple", + place_on = "floor", + fill_ratio = 0.01, + decoration = "mcl_core:cactus", + height = 3, + height_max = 8, + biomes = { "nc:glowing_floor" }, +}) +noordstar_caves.register_decoration({ + deco_type = "simple", + place_on = "ceiling", + fill_ratio = 0.1, + decoration = "mcl_ocean:sea_lantern", + height = 1, + height_max = 4, + biomes = { "nc:crazy_glowin_walls" }, + place_offset_y = -3, +}) +noordstar_caves.register_decoration({ + deco_type = "schematic", + place_on = "floor", + fill_ratio = 0.001, + schematic = squeak, + rotation = "random", + -- place_offset_y = 5, +})