2024-10-25 13:46:47 +02:00

399 lines
12 KiB
Lua

-- The main mapgen code.
--[[ THIS FILE WILL BE RUN TWICE!
1) In the global Luanti environment,
2) In the heavily-restricted threaded mapgen environment
This means this file is very restricted and only has
access to functions that are available in both the
global and the mapgen environment. Refer to Luanti's
Lua API documentation for details.
In the global environment, this file runs to expose the
function lzr_mapgen.generate_piece, but it does not call
minetest.register_on_generated.
In the mapgen environment, it uses
minetest.register_on_generated, but it does not modify
the global environment.
The reason this file is called twice is to avoid code
duplication.
]]
-- This variable will be true when we're in the mapgen
-- environment. The mapgen environment does not have
-- access to global variables, so if lzr_mapgen does
-- not exist, we can decude we must be the mapgen
-- environment.
local IS_IN_MAPGEN_ENVIRONMENT = not minetest.global_exists("lzr_mapgen")
--[[ MAPGEN OVERVIEW:
The mapgen has two main zones:
Deep Ocean: The Deep Ocean is a very simple zone and very fast
to generate, it's just a flat layer of water with a flat seabed
way below the surface. Perfect to spawn the ship in.
Islands: Islands is a beautiful zone using 2D Perlin
noise, featuring tropical islands, palms, plants, hills, and
a “hilly” ocean floor. It is derived from the [islands] mod
by TheTermos, released under the MIT License in 2019.
Both zones are separated on the Z axis by DEEP_OCEAN_Z.
There is a hard ugly seam between both zones, so it is
important the game action happens far away from this
seam.
]]
-- Some basic values for mapgen coordinates
-- Water will be at this Y level, all the way down to SEABED_LEVEL+1
local WATER_LEVEL = 1
-- The seabed will be at this Y level, all the way down to SEASTONE_LEVEL+1 (deep ocean only)
local SEABED_LEVEL = -1000
-- The seastone will be at and below this Y level, all the way down to the bottom of the world (deep ocean only)
local SEASTONE_LEVEL = -1002
-- The deep ocean will begin when the Z coordinate is this value or lower.
-- WARNING: This value is duplicated in lzr_globals.
-- When you change it, you MUST also change it in lzr_globals!
local DEEP_OCEAN_Z = -20000
local floor = math.floor
local ceil = math.ceil
local min = math.min
local max = math.max
local random = math.random
local convex = false
local mult = 1.0
-- Set the 3D noise parameters for the terrain.
local np_terrain = {
offset = -11*mult, -- ratio 2:7 or 1:4 ?
scale = 40*mult,
spread = {x = 256*mult, y =256*mult, z = 256*mult},
seed = 1234,
octaves = convex and 1 or 5,
persist = 0.38,
lacunarity = 2.33,
}
local np_var = {
offset = 0,
scale = 6*mult,
spread = {x = 64*mult, y =64*mult, z = 64*mult},
seed = 567891,
octaves = 4,
persist = 0.4,
lacunarity = 1.89,
}
local np_hills = {
offset = 2.5, -- off/scale ~ 2:3
scale = -3.5,
spread = {x = 64*mult, y =64*mult, z = 64*mult},
seed = 2345,
octaves = 3,
persist = 0.40,
lacunarity = 2.0,
flags = "absvalue"
}
local np_cliffs = {
offset = 0,
scale = 0.72,
spread = {x = 180*mult, y =180*mult, z = 180*mult},
seed = 78901,
octaves = 2,
persist = 0.4,
lacunarity = 2.11,
}
local np_trees = {
offset = - 0.003,
scale = 0.008,
spread = {x = 64, y = 64, z = 64},
seed = 2,
octaves = 5,
persist = 1,
lacunarity = 1.91,
}
local hills_offset = np_hills.spread.x*0.5
local hills_thresh = floor((np_terrain.scale)*0.5)
local shelf_thresh = floor((np_terrain.scale)*0.5)
local cliffs_thresh = 10
local function max_height(noiseprm)
local height = 0
local scale = noiseprm.scale
for i=1,noiseprm.octaves do
height=height + scale
scale = scale * noiseprm.persist
end
return height+noiseprm.offset
end
local function min_height(noiseprm)
local height = 0
local scale = noiseprm.scale
for i=1,noiseprm.octaves do
height=height - scale
scale = scale * noiseprm.persist
end
return height+noiseprm.offset
end
local base_min = min_height(np_terrain)
local base_max = max_height(np_terrain)
local base_rng = base_max-base_min
local easing_factor = 1/(base_max*base_max*4)
local base_heightmap = {}
local result_heightmap = {}
-- Get the content IDs for the nodes used.
local c_stone = minetest.get_content_id("lzr_core:stone")
local c_sand = minetest.get_content_id("lzr_core:sand")
local c_sand_dark = minetest.get_content_id("lzr_core:seabed")
local c_dirt = minetest.get_content_id("lzr_core:dirt")
local c_dirt_g = minetest.get_content_id("lzr_core:dirt_with_grass")
local c_dirt_l = minetest.get_content_id("lzr_core:dirt_with_jungle_litter")
local c_snow = minetest.get_content_id("lzr_core:dirt") -- no snow
local c_water = minetest.get_content_id("lzr_core:water_source")
local function get_terrain_height(theight,hheight,cheight)
-- parabolic gradient
if theight > 0 and theight < shelf_thresh then
theight = theight * (theight*theight/(shelf_thresh*shelf_thresh)*0.5 + 0.5)
end
-- hills
if theight > hills_thresh then
theight = theight + max((theight-hills_thresh) * hheight,0)
-- cliffs
elseif theight > 1 and theight < hills_thresh then
local clifh = max(min(cheight,1),0)
if clifh > 0 then
clifh = -1*(clifh-1)*(clifh-1) + 1
theight = theight + (hills_thresh-theight) * clifh * ((theight<2) and theight-1 or 1)
end
end
return theight
end
-- Given an Y coordinate, returns the expected day light level
-- for water at this coordinate, assuming there are no other
-- obstacles
local get_ocean_light_level = function(y)
local ll = 15 - (WATER_LEVEL - (y-1))
return max(0, min(15, ll))
end
-- Change the day component of the given original param1
-- light value (0-255) to new_light_day (0-15) and
-- returns the result.
local change_day_light = function(original_light, new_light_day)
local light = original_light
local light_night = bit.band(light, 0xF0)
light = bit.bor(new_light_day, light_night)
return light
end
-- Localise VoxelManip data buffer tables outside the loop,
-- to be re-used for all mapchunks, therefore minimising memory use.
local vm_data = {}
local vm_light_data = {}
-- Generate a piece of the map and set nodes in the specified area.
-- * minp: Minimum position of the area
-- * maxp: Maximum position of the area
-- * vm: VoxelManip object (only required when calling this function in the mapgen environment. Set to nil otherwise)
-- * prot_min: Minimum position of protected area. The protected area will NOT be overwritten by this function
-- * prot_min: Maximum position of protected area
local generate_piece = function(minp, maxp, vm, prot_min, prot_max)
-- Start time of mapchunk generation.
local t0 = os.clock()
if IS_IN_MAPGEN_ENVIRONMENT then
minetest.log("info", "[lzr_mapgen] Generating piece at " .. minetest.pos_to_string(minp).." ... (mapgen environment)")
else
minetest.log("info", "[lzr_mapgen] Generating piece at " .. minetest.pos_to_string(minp).." ... (main environment)")
end
local sidelen_x = maxp.x - minp.x + 1
local sidelen_z = maxp.z - minp.z + 1
local permapdims3d = {x = sidelen_x, y = sidelen_z}
local clear = IS_IN_MAPGEN_ENVIRONMENT ~= true
-- voxelmanip stuff
if not vm then
vm = minetest.get_voxel_manip(minp, maxp)
end
local emin, emax = vm:get_emerged_area()
local area = VoxelArea:new{MinEdge = emin, MaxEdge = emax}
vm:get_data(vm_data)
vm:get_light_data(vm_light_data)
-- If we are fully outside the islands area,
-- we can switch to the ultra-fast deep ocean
-- algorithm.
local generate_islands = maxp.z > DEEP_OCEAN_Z
local isln_terrain = nil
local isln_var = nil
local isln_hills = nil
local isln_cliffs = nil
local isln_trees = nil
if generate_islands then
-- base terrain
local nobj_terrain = minetest.get_perlin_map(np_terrain, permapdims3d)
isln_terrain = nobj_terrain:get_2d_map({x=minp.x,y=minp.z})
-- base variation
local nobj_var = minetest.get_perlin_map(np_var, permapdims3d)
isln_var = nobj_var:get_2d_map({x=minp.x,y=minp.z})
-- hills
local nobj_hills = minetest.get_perlin_map(np_hills, permapdims3d)
isln_hills = nobj_hills:get_2d_map({x=minp.x+hills_offset,y=minp.z+hills_offset})
-- cliffs
local nobj_cliffs = minetest.get_perlin_map(np_cliffs, permapdims3d)
isln_cliffs = nobj_cliffs:get_2d_map({x=minp.x,y=minp.z})
-- trees
local nobj_trees = minetest.get_perlin_map(np_trees, permapdims3d)
isln_trees = nobj_trees:get_2d_map({x=minp.x,y=minp.z})
for z = 1, sidelen_z do
base_heightmap[z]={}
result_heightmap[z]={}
for x = 1, sidelen_x do
if not isln_terrain[z][x] then
minetest.log("error", "[lzr_mapgen] isln_terrain["..z.."]["..x.."] is nil!")
minetest.log("error", "[lzr_mapgen] LEN Z="..#isln_terrain)
for i=1, #isln_terrain do
minetest.log("error", "[lzr_mapgen] LEN X["..i.."]="..#isln_terrain[i])
end
minetest.log("error", "[lzr_mapgen] sidelen_x="..sidelen_x)
minetest.log("error", "[lzr_mapgen] sidelen_z="..sidelen_z)
return
end
if not isln_var[z][x] then
minetest.log("error", "[lzr_mapgen] isln_var["..z.."]["..x.."] is nil!")
minetest.log("error", "[lzr_mapgen] VAR LEN Z="..#isln_terrain)
minetest.log("error", "[lzr_mapgen] VAR LEN X="..#isln_terrain[1])
minetest.log("error", "[lzr_mapgen] sidelen_x="..sidelen_x)
minetest.log("error", "[lzr_mapgen] sidelen_z="..sidelen_z)
minetest.log("error", "[lzr_mapgen] isln_var="..dump(isln_var))
return
end
local theight = isln_terrain[z][x] + (convex and isln_var[z][x] or 0)
local hheight = isln_hills[z][x]
local cheight = isln_cliffs[z][x]
base_heightmap[z][x]=theight
result_heightmap[z][x]=get_terrain_height(theight,hheight,cheight)
end
end
end
for z = minp.z, maxp.z do
for y = minp.y, maxp.y do
for x = minp.x, maxp.x do
local mpos = vector.new(x, y, z)
if not prot_min or not vector.in_area(mpos, prot_min, prot_max) then
local vi = area:index(x, y, z)
-- Deep ocean
if z <= DEEP_OCEAN_Z then
if y <= SEASTONE_LEVEL then
vm_data[vi] = c_stone
elseif y <= SEABED_LEVEL then
vm_data[vi] = c_seabed
elseif y <= WATER_LEVEL then
vm_data[vi] = c_water
-- We calculate the light level for the ocean manually
-- because calc_lighting apparently doesn't calculate
-- the lighting of the ocean.
-- FIXME: Remove the manual manual calculation entirely when we
-- figured out a way to fix ocean lighting with calc_lighting alone
vm_light_data[vi] = change_day_light(vm_light_data[vi], get_ocean_light_level(y))
elseif clear then
vm_data[vi] = minetest.CONTENT_AIR
end
-- Islands mapgen
else
local bheight = base_heightmap[z-minp.z+1][x-minp.x+1]
local theight = result_heightmap[z-minp.z+1][x-minp.x+1]
local dirt = (theight > 2 and theight < hills_thresh and isln_trees[z-minp.z+1][x-minp.x+1] > 0) and c_dirt_l or c_dirt_g
if theight > y then
vm_data[vi] = c_stone
elseif y==ceil(theight) then
vm_data[vi]= y < -3 and c_sand_dark or y<4 and c_sand or (y<60-random(3) and dirt or c_snow)
elseif y <= WATER_LEVEL then
vm_data[vi] = c_water
-- Calculate lighting manually (see above)
vm_light_data[vi] = change_day_light(vm_light_data[vi], get_ocean_light_level(y))
elseif clear then
vm_data[vi] = minetest.CONTENT_AIR
end
end
end
end
end
end
vm:set_data(vm_data)
vm:set_light_data(vm_light_data)
if IS_IN_MAPGEN_ENVIRONMENT then
if generate_islands then
minetest.generate_decorations(vm, minp, maxp)
end
vm:calc_lighting(minp, maxp)
else
if generate_islands then
minetest.generate_decorations(vm, minp, maxp)
end
vm:write_to_map(true)
end
-- Print generation time of this piece
local chugent = ceil((os.clock() - t0) * 1000)
if IS_IN_MAPGEN_ENVIRONMENT then
minetest.log("info", "[lzr_mapgen] Generation time for piece at " .. minetest.pos_to_string(minp)..": " .. chugent .. " ms (mapgen environment)")
else
minetest.log("info", "[lzr_mapgen] Generation time for piece at " .. minetest.pos_to_string(minp)..": " .. chugent .. " ms (main environment)")
end
end
if IS_IN_MAPGEN_ENVIRONMENT then
minetest.log("action", "[lzr_mapgen] mapgen script successfully run in mapgen environment")
-- On generated function for the mapgen environment
minetest.register_on_generated(function(vmanip, minp, maxp)
generate_piece(minp, maxp, vmanip)
end)
else
minetest.log("action", "[lzr_mapgen] mapgen script successfully run in global environment")
-- Expose generate_piece to the global environment when this file does not
-- run as mapgen thrad
lzr_mapgen.generate_piece = generate_piece
end