360 lines
11 KiB
Lua

-- The main mapgen code.
--[[ THIS FILE WILL BE RUN TWICE!
1) In the global Minetest 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 Minetest'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:ocean_water_source")
-- Localise data buffer table outside the loop, to be re-used for all
-- mapchunks, therefore minimising memory use.
local data = {}
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
-- 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)
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(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
data[vi] = c_stone
elseif y <= SEABED_LEVEL then
data[vi] = c_seabed
elseif y <= WATER_LEVEL then
data[vi] = c_water
elseif clear then
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
data[vi] = c_stone
elseif y==ceil(theight) then
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 <= 1 then
data[vi] = c_water
elseif clear then
data[vi] = minetest.CONTENT_AIR
end
end
end
end
end
end
vm:set_data(data)
if generate_islands then
minetest.generate_decorations(vm)
minetest.generate_ores(vm)
if IS_IN_MAPGEN_ENVIRONMENT then
vm:calc_lighting()
else
vm:write_to_map(true)
end
elseif not IS_IN_MAPGEN_ENVIRONMENT then
vm:write_to_map(true)
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)
-- Start time of mapchunk generation.
local t0 = os.clock()
generate_piece(minp, maxp, vmanip)
-- Print generation time of this mapchunk.
local chugent = ceil((os.clock() - t0) * 1000)
minetest.log("action", "[lzr_mapgen] Mapchunk generation time for piece at " .. minetest.pos_to_string(minp)..": " .. chugent .. " ms")
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