nether/mapgen_mantle.lua
Treer 52038017f3
Make ShiftExistingBiomes available via API (#40)
* bug fixes

Fixes wrong new_y_min when shift_existing_biomes() shifts a biome below the nether floor,
find_surface_anchorPos() no longer assumes y=0 will be outside the nether.
Nil-reference fixed when a mods tries to register a portal after mods are finished loading, but the portal shape+material was already registered by another mod.

* Make ShiftExistingBiomes available via API

makes the ShiftExistingBiomes function available to other mods via the nether global, since it's not a simple function and biomes would also need to be shifted if another mod wants to add a second nether layer.

* Allow layers to extend the depth of nether effects

Mods can set/lower nether.DEPTH_FLOOR_LAYERS when creating a layer under the nether. This allows multiple layer mods to know where their ceiling should start, and to be included in the effects which only happen in the nether.

* document nether API

More of a tentative interop guide than an API.
Use snake_case for API functions.
2021-07-18 16:44:37 +10:00

513 lines
19 KiB
Lua

--[[
Nether mod for minetest
This file contains helper functions for generating the Mantle
(AKA center region), which are moved into a separate file to keep the
size of mapgen.lua manageable.
Copyright (C) 2021 Treer
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted, provided that the
above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR
BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
SOFTWARE.
]]--
local debugf = nether.debug
local mapgen = nether.mapgen
local S = nether.get_translator
local BASALT_COLUMN_UPPER_LIMIT = mapgen.BASALT_COLUMN_UPPER_LIMIT
local BASALT_COLUMN_LOWER_LIMIT = mapgen.BASALT_COLUMN_LOWER_LIMIT
-- 2D noise for basalt formations
local np_basalt = {
offset =-0.85,
scale = 1,
spread = {x = 46, y = 46, z = 46},
seed = 1000,
octaves = 5,
persistence = 0.5,
lacunarity = 2.6,
flags = "eased"
}
-- Buffers and objects we shouldn't recreate every on_generate
local nobj_basalt = nil
local nbuf_basalt = {}
-- Content ids
local c_air = minetest.get_content_id("air")
local c_netherrack_deep = minetest.get_content_id("nether:rack_deep")
local c_glowstone = minetest.get_content_id("nether:glowstone")
local c_lavasea_source = minetest.get_content_id("nether:lava_source") -- same as lava but with staggered animation to look better as an ocean
local c_lava_crust = minetest.get_content_id("nether:lava_crust")
local c_basalt = minetest.get_content_id("nether:basalt")
-- Math funcs
local math_max, math_min, math_abs, math_floor = math.max, math.min, math.abs, math.floor -- avoid needing table lookups each time a common math function is invoked
function random_unit_vector()
return vector.normalize({
x = math.random() - 0.5,
y = math.random() - 0.5,
z = math.random() - 0.5
})
end
-- returns the smallest component in the vector
function vector_min(v)
return math_min(v.x, math_min(v.y, v.z))
end
-- Mantle mapgen functions (AKA Center region)
-- Returns (absolute height, fractional distance from ceiling or sea floor)
-- the fractional distance from ceiling or sea floor is a value between 0 and 1 (inclusive)
-- Note it may find the most relevent sea-level - not necesssarily the one you are closest
-- to, since the space above the sea reaches much higher than the depth below the sea.
mapgen.find_nearest_lava_sealevel = function(y)
-- todo: put oceans near the bottom of chunks to improve ability to generate tunnels to the center
-- todo: constrain y to be not near the bounds of the nether
-- todo: add some random adj at each level, seeded only by the level height
local sealevel = math.floor((y + 100) / 200) * 200
--local sealevel = math.floor((y + 80) / 160) * 160
--local sealevel = math.floor((y + 120) / 240) * 240
local cavern_limits_fraction
local height_above_sea = y - sealevel
if height_above_sea >= 0 then
cavern_limits_fraction = math_min(1, height_above_sea / 95)
else
-- approaches 1 much faster as the lava sea is shallower than the cavern above it
cavern_limits_fraction = math_min(1, -height_above_sea / 40)
end
return sealevel, cavern_limits_fraction
end
mapgen.add_basalt_columns = function(data, area, minp, maxp)
-- Basalt columns are structures found in lava oceans, and the only way to obtain
-- nether basalt.
-- Their x, z position is determined by a 2d noise map and a 2d slice of the cave
-- noise (taken at lava-sealevel).
local x0, y0, z0 = minp.x, math_max(minp.y, nether.DEPTH_FLOOR), minp.z
local x1, y1, z1 = maxp.x, math_min(maxp.y, nether.DEPTH_CEILING), maxp.z
local yStride = area.ystride
local yCaveStride = x1 - x0 + 1
local cavePerlin = mapgen.get_cave_point_perlin()
nobj_basalt = nobj_basalt or minetest.get_perlin_map(np_basalt, {x = yCaveStride, y = yCaveStride})
local nvals_basalt = nobj_basalt:get_2d_map_flat({x=minp.x, y=minp.z}, {x=yCaveStride, y=yCaveStride}, nbuf_basalt)
local nearest_sea_level, _ = mapgen.find_nearest_lava_sealevel(math_floor((y0 + y1) / 2))
local leeway = mapgen.CENTER_CAVERN_LIMIT * 0.18
for z = z0, z1 do
local noise2di = 1 + (z - z0) * yCaveStride
for x = x0, x1 do
local basaltNoise = nvals_basalt[noise2di]
if basaltNoise > 0 then
-- a basalt column is here
local abs_sealevel_cave_noise = math_abs(cavePerlin:get_3d({x = x, y = nearest_sea_level, z = z}))
-- Add Some quick deterministic noise to the column heights
-- This is probably not good noise, but it doesn't have to be.
local fastNoise = 17
fastNoise = 37 * fastNoise + y0
fastNoise = 37 * fastNoise + z
fastNoise = 37 * fastNoise + x
fastNoise = 37 * fastNoise + math_floor(basaltNoise * 32)
local columnHeight = basaltNoise * 18 + ((fastNoise % 3) - 1)
-- columns should drop below sealevel where lava rivers are flowing
-- i.e. anywhere abs_sealevel_cave_noise < BASALT_COLUMN_LOWER_LIMIT
-- And we'll also have it drop off near the edges of the lava ocean so that
-- basalt columns can only be found by the player reaching a lava ocean.
local lowerClip = (math_min(math_max(abs_sealevel_cave_noise, BASALT_COLUMN_LOWER_LIMIT - leeway), BASALT_COLUMN_LOWER_LIMIT + leeway) - BASALT_COLUMN_LOWER_LIMIT) / leeway
local upperClip = (math_min(math_max(abs_sealevel_cave_noise, BASALT_COLUMN_UPPER_LIMIT - leeway), BASALT_COLUMN_UPPER_LIMIT + leeway) - BASALT_COLUMN_UPPER_LIMIT) / leeway
local columnHeightAdj = lowerClip * -upperClip -- all are values between 1 and -1
columnHeight = columnHeight + math_floor(columnHeightAdj * 12 - 12)
local vi = area:index(x, y0, z) -- Initial voxelmanip index
for y = y0, y1 do -- Y loop first to minimise tcave & lava-sea calculations
if y < nearest_sea_level + columnHeight then
local id = data[vi] -- Existing node
if id == c_lava_crust or id == c_lavasea_source or (id == c_air and y > nearest_sea_level) then
-- Avoid letting columns extend beyond the central region.
-- (checking node ids saves having to calculate abs_cave_noise_adjusted here
-- to test it against CENTER_CAVERN_LIMIT)
data[vi] = c_basalt
end
end
vi = vi + yStride
end
end
noise2di = noise2di + 1
end
end
end
-- returns an array of points from pos1 and pos2 which deviate from a straight line
-- but which don't venture too close to a chunk boundary
function generate_waypoints(pos1, pos2, minp, maxp)
local segSize = 10
local maxDeviation = 7
local minDistanceFromChunkWall = 5
local pathVec = vector.subtract(pos2, pos1)
local pathVecNorm = vector.normalize(pathVec)
local pathLength = vector.distance(pos1, pos2)
local minBound = vector.add(minp, minDistanceFromChunkWall)
local maxBound = vector.subtract(maxp, minDistanceFromChunkWall)
local result = {}
result[1] = pos1
local segmentCount = math_floor(pathLength / segSize)
for i = 1, segmentCount do
local waypoint = vector.add(pos1, vector.multiply(pathVec, i / (segmentCount + 1)))
-- shift waypoint a few blocks in a random direction orthogonally to the pathVec, to make the path crooked.
local crossProduct
repeat
crossProduct = vector.normalize(vector.cross(pathVecNorm, random_unit_vector()))
until vector.length(crossProduct) > 0
local deviation = vector.multiply(crossProduct, math.random(1, maxDeviation))
waypoint = vector.add(waypoint, deviation)
waypoint = {
x = math_min(maxBound.x, math_max(minBound.x, waypoint.x)),
y = math_min(maxBound.y, math_max(minBound.y, waypoint.y)),
z = math_min(maxBound.z, math_max(minBound.z, waypoint.z))
}
result[#result + 1] = waypoint
end
result[#result + 1] = pos2
return result
end
function excavate_pathway(data, area, nether_pos, center_pos, minp, maxp)
local ystride = area.ystride
local zstride = area.zstride
math.randomseed(nether_pos.x + 10 * nether_pos.y + 100 * nether_pos.z) -- so each tunnel generates deterministically (this doesn't have to be a quality seed)
local dist = math_floor(vector.distance(nether_pos, center_pos))
local waypoints = generate_waypoints(nether_pos, center_pos, minp, maxp)
-- First pass: record path details
local linedata = {}
local last_pos = {}
local line_index = 1
local first_filled_index, boundary_index, last_filled_index
for i = 0, dist do
-- Bresenham's line would be good here, but too much lua code
local waypointProgress = (#waypoints - 1) * i / dist
local segmentIndex = math_min(math_floor(waypointProgress) + 1, #waypoints - 1) -- from the integer portion of waypointProgress
local segmentInterp = waypointProgress - (segmentIndex - 1) -- the remaining fractional portion
local segmentStart = waypoints[segmentIndex]
local segmentVector = vector.subtract(waypoints[segmentIndex + 1], segmentStart)
local pos = vector.round(vector.add(segmentStart, vector.multiply(segmentVector, segmentInterp)))
if not vector.equals(pos, last_pos) then
local vi = area:indexp(pos)
local node_id = data[vi]
linedata[line_index] = {
pos = pos,
vi = vi,
node_id = node_id
}
if boundary_index == nil and node_id == c_netherrack_deep then
boundary_index = line_index
end
if node_id == c_air then
if boundary_index ~= nil and last_filled_index == nil then
last_filled_index = line_index
end
else
if first_filled_index == nil then
first_filled_index = line_index
end
end
line_index = line_index + 1
last_pos = pos
end
end
first_filled_index = first_filled_index or 1
last_filled_index = last_filled_index or #linedata
boundary_index = boundary_index or last_filled_index
-- limit tunnel radius to roughly the closest that startPos or stopPos comes to minp-maxp, so we
-- don't end up exceeding minp-maxp and having excavation filled in when the next chunk is generated.
local startPos, stopPos = linedata[first_filled_index].pos, linedata[last_filled_index].pos
local radiusLimit = vector_min(vector.subtract(startPos, minp))
radiusLimit = math_min(radiusLimit, vector_min(vector.subtract(stopPos, minp)))
radiusLimit = math_min(radiusLimit, vector_min(vector.subtract(maxp, startPos)))
radiusLimit = math_min(radiusLimit, vector_min(vector.subtract(maxp, stopPos)))
if radiusLimit < 4 then -- This is a logic check, ignore it. It could be commented out
-- 4 is (79 - 75), and values less than 4 shouldn't be possible if sampling-skip was 10
-- i.e. if sampling-skip was 10 then {5, 15, 25, 35, 45, 55, 65, 75} should be sampled from possible positions 0 to 79
debugf("Error: radiusLimit %s is smaller then half the sampling distance. min %s, max %s, start %s, stop %s", radiusLimit, minp, maxp, startPos, stopPos)
end
radiusLimit = radiusLimit + 1 -- chunk walls wont be visibly flat if the radius only exceeds it a little ;)
-- Second pass: excavate
local start_index, stop_index = math_max(1, first_filled_index - 2), math_min(#linedata, last_filled_index + 3)
for i = start_index, stop_index, 3 do
-- Adjust radius so that tunnels start wide but thin out in the middle
local distFromEnds = 1 - math_abs(((start_index + stop_index) / 2) - i) / ((stop_index - start_index) / 2) -- from 0 to 1, with 0 at ends and 1 in the middle
-- Have it more flaired at the ends, rather than linear.
-- i.e. sizeAdj approaches 1 quickly as distFromEnds increases
local distFromMiddle = 1 - distFromEnds
local sizeAdj = 1 - (distFromMiddle * distFromMiddle * distFromMiddle)
local radius = math_min(radiusLimit, math.random(50 - (25 * sizeAdj), 80 - (45 * sizeAdj)) / 10)
local radiusSquared = radius * radius
local radiusCeil = math_floor(radius + 0.5)
linedata[i].radius = radius -- Needed in third pass
linedata[i].distFromEnds = distFromEnds -- Needed in third pass
local vi = linedata[i].vi
for z = -radiusCeil, radiusCeil do
local vi_z = vi + z * zstride
for y = -radiusCeil, radiusCeil do
local vi_zy = vi_z + y * ystride
local xSquaredLimit = radiusSquared - (z * z + y * y)
for x = -radiusCeil, radiusCeil do
if x * x < xSquaredLimit then
data[vi_zy + x] = c_air
end
end
end
end
end
-- Third pass: decorate
-- Add glowstones to make tunnels to the mantle easier to find
-- https://i.imgur.com/sRA28x7.jpg
for i = start_index, stop_index, 3 do
if linedata[i].distFromEnds < 0.3 then
local glowcount = 0
local radius = linedata[i].radius
for _ = 1, 20 do
local testPos = vector.round(vector.add(linedata[i].pos, vector.multiply(random_unit_vector(), radius + 0.5)))
local vi = area:indexp(testPos)
if data[vi] ~= c_air then
data[vi] = c_glowstone
glowcount = glowcount + 1
--else
-- data[vi] = c_debug
end
if glowcount >= 2 then break end
end
end
end
end
-- excavates a tunnel connecting the Primary or Secondary region with the mantle / central region
-- if a suitable path is found.
-- Returns true if successful
mapgen.excavate_tunnel_to_center_of_the_nether = function(data, area, nvals_cave, minp, maxp)
local result = false
local extent = vector.subtract(maxp, minp)
local skip = 10 -- sampling rate of 1 in 10
local highest = -1000
local lowest = 1000
local lowest_vi
local highest_vi
local yCaveStride = maxp.x - minp.x + 1
local zCaveStride = yCaveStride * yCaveStride
local vi_offset = area:indexp(vector.add(minp, math_floor(skip / 2))) -- start half the sampling distance away from minp
local vi, ni
for y = 0, extent.y - 1, skip do
local sealevel = mapgen.find_nearest_lava_sealevel(minp.y + y)
if minp.y + y > sealevel then -- only create tunnels above sea level
for z = 0, extent.z - 1, skip do
vi = vi_offset + y * area.ystride + z * area.zstride
ni = z * zCaveStride + y * yCaveStride + 1
for x = 0, extent.x - 1, skip do
local noise = math_abs(nvals_cave[ni])
if noise < lowest then
lowest = noise
lowest_vi = vi
end
if noise > highest then
highest = noise
highest_vi = vi
end
ni = ni + skip
vi = vi + skip
end
end
end
end
if lowest < mapgen.CENTER_CAVERN_LIMIT and highest > mapgen.TCAVE + 0.03 then
local mantle_y = area:position(lowest_vi).y
local _, cavern_limit_distance = mapgen.find_nearest_lava_sealevel(mantle_y)
local _, centerRegionLimit_adj = mapgen.get_mapgenblend_adjustments(mantle_y)
-- cavern_noise_adj gets added to noise value instead of added to the limit np_noise
-- is compared against, so subtract centerRegionLimit_adj instead of adding
local cavern_noise_adj =
mapgen.CENTER_REGION_LIMIT * (cavern_limit_distance * cavern_limit_distance * cavern_limit_distance) -
centerRegionLimit_adj
if lowest + cavern_noise_adj < mapgen.CENTER_CAVERN_LIMIT then
excavate_pathway(data, area, area:position(highest_vi), area:position(lowest_vi), minp, maxp)
result = true
end
end
return result
end
-- an enumerated list of the different regions in the nether
mapgen.RegionEnum = {
OVERWORLD = {name = "overworld", desc = S("The Overworld") }, -- Outside the Nether / none of the regions in the Nether
POSITIVE = {name = "positive", desc = S("Positive nether") }, -- The classic nether caverns are here - where cavePerlin > 0.6
POSITIVESHELL = {name = "positive shell", desc = S("Shell between positive nether and center region") }, -- the nether side of the wall/buffer area separating classic nether from the mantle
CENTER = {name = "center", desc = S("Center/Mantle, inside cavern") },
CENTERSHELL = {name = "center shell", desc = S("Center/Mantle, but outside the caverns") }, -- the mantle side of the wall/buffer area separating the positive and negative regions from the center region
NEGATIVE = {name = "negative", desc = S("Negative nether") }, -- Secondary/spare region - where cavePerlin < -0.6
NEGATIVESHELL = {name = "negative shell", desc = S("Shell between negative nether and center region") } -- the spare region side of the wall/buffer area separating the negative region from the mantle
}
-- Returns (region, noise) where region is a value from mapgen.RegionEnum
-- and noise is the unadjusted cave perlin value
mapgen.get_region = function(pos)
if pos.y > nether.DEPTH_CEILING or pos.y < nether.DEPTH_FLOOR then
return mapgen.RegionEnum.OVERWORLD, nil
end
local caveNoise = mapgen.get_cave_perlin_at(pos)
local sealevel, cavern_limit_distance = mapgen.find_nearest_lava_sealevel(pos.y)
local tcave_adj, centerRegionLimit_adj = mapgen.get_mapgenblend_adjustments(pos.y)
local tcave = mapgen.TCAVE + tcave_adj
local tmantle = mapgen.CENTER_REGION_LIMIT + centerRegionLimit_adj
-- cavern_noise_adj gets added to noise value instead of added to the limit np_noise
-- is compared against, so subtract centerRegionLimit_adj instead of adding
local cavern_noise_adj =
mapgen.CENTER_REGION_LIMIT * (cavern_limit_distance * cavern_limit_distance * cavern_limit_distance) -
centerRegionLimit_adj
local region
if caveNoise > tcave then
region = mapgen.RegionEnum.POSITIVE
elseif -caveNoise > tcave then
region = mapgen.RegionEnum.NEGATIVE
elseif math_abs(caveNoise) < tmantle then
if math_abs(caveNoise) + cavern_noise_adj < mapgen.CENTER_CAVERN_LIMIT then
region = mapgen.RegionEnum.CENTER
else
region = mapgen.RegionEnum.CENTERSHELL
end
elseif caveNoise > 0 then
region = mapgen.RegionEnum.POSITIVESHELL
else
region = mapgen.RegionEnum.NEGATIVESHELL
end
return region, caveNoise
end
minetest.register_chatcommand("nether_whereami",
{
description = S("Describes which region of the nether the player is in"),
privs = {debug = true},
func = function(name, param)
local player = minetest.get_player_by_name(name)
if player == nil then return false, S("Unknown player position") end
local playerPos = vector.round(player:get_pos())
local region, caveNoise = mapgen.get_region(playerPos)
local seaLevel, cavernLimitDistance = mapgen.find_nearest_lava_sealevel(playerPos.y)
local tcave_adj, centerRegionLimit_adj = mapgen.get_mapgenblend_adjustments(playerPos.y)
local seaDesc = ""
local boundaryDesc = ""
local perlinDesc = ""
if region ~= mapgen.RegionEnum.OVERWORLD then
local seaPos = playerPos.y - seaLevel
if seaPos > 0 then
seaDesc = S(", @1m above lava-sea level", seaPos)
else
seaDesc = S(", @1m below lava-sea level", seaPos)
end
if tcave_adj > 0 then
boundaryDesc = S(", approaching y boundary of Nether")
end
perlinDesc = S("[Perlin @1] ", (math_floor(caveNoise * 1000) / 1000))
end
return true, S("@1@2@3@4", perlinDesc, region.desc, seaDesc, boundaryDesc)
end
}
)