-- biomegen/init.lua local make_biomelist = dofile(minetest.get_modpath(minetest.get_current_modname()) .. "/biomelist.lua") local make_decolist = dofile(minetest.get_modpath(minetest.get_current_modname()) .. "/decorations.lua") local np_filler_depth = { offset = 0, scale = 1.2, spread = {x=150, y=150, z=150}, seed = 261, octaves = 3, persist = 0.7, lacunarity = 2.0, } local nobj_filler_depth, nobj_heat, nobj_heat_blend, nobj_humid, nobj_humid_blend local nvals_filler_depth = {} local nvals_heat = {} local nvals_heat_blend = {} local nvals_humid = {} local nvals_humid_blend = {} local water_level = tonumber(minetest.get_mapgen_setting('water_level')) local elevation_chill = 0 local function set_elevation_chill(ec) elevation_chill = ec end local init = false local c_ignore local c_air local c_stone local c_water local c_rwater local biomes, decos local function initialize(chulens) print("[biomegen] Initializing") init = true local noiseparams = minetest.get_mapgen_setting_noiseparams local chulens2d = {x=chulens.x, y=chulens.z, z=1} local np_heat = noiseparams('mg_biome_np_heat') water_level = multi_map.get_absolute_centerpoint() np_heat.offset = np_heat.offset + water_level*elevation_chill nobj_filler_depth = minetest.get_perlin_map(np_filler_depth, chulens2d) nobj_heat = minetest.get_perlin_map(np_heat, chulens2d) nobj_heat_blend = minetest.get_perlin_map(noiseparams('mg_biome_np_heat_blend'), chulens2d) nobj_humid = minetest.get_perlin_map(noiseparams('mg_biome_np_humidity'), chulens2d) nobj_humid_blend = minetest.get_perlin_map(noiseparams('mg_biome_np_humidity_blend'), chulens2d) c_ignore = minetest.get_content_id("ignore") c_air = minetest.get_content_id("air") c_stone = minetest.get_content_id("mapgen_stone") c_water = minetest.get_content_id("mapgen_water_source") c_rwater = minetest.get_content_id("mapgen_river_water_source") biomes = make_biomelist() decos = make_decolist() end local biomemap = {} local heatmap = {} local humidmap = {} local function calculate_noises(minp) local minp2d = {x=minp.x, y=minp.z} nobj_filler_depth:get_2d_map_flat(minp2d, nvals_filler_depth) nobj_heat:get_2d_map_flat(minp2d, nvals_heat) nobj_heat_blend:get_2d_map_flat(minp2d, nvals_heat_blend) nobj_humid:get_2d_map_flat(minp2d, nvals_humid) nobj_humid_blend:get_2d_map_flat(minp2d, nvals_humid_blend) for i, heat in ipairs(nvals_heat) do -- use nvals_heat to iterate, could have been another one heatmap[i] = heat + nvals_heat_blend[i] humidmap[i] = nvals_humid[i] + nvals_humid_blend[i] end end local function calc_biome_from_noise(heat, humid, pos) --water_level = multi_map.get_absolute_centerpoint(multi_map.get_layer(p.y)) local offsety= multi_map.get_offset_y(pos.y,multi_map.get_layer(pos.y)) local biome_closest = nil local biome_closest_blend = nil local dist_min = 31000 local dist_min_blend = 31000 for i, biome in pairs(biomes) do local min_pos, max_pos = biome.min_pos, biome.max_pos --if offsety >= min_pos.y and if offsety <= max_pos.y+biome.vertical_blend and pos.x >= min_pos.x and pos.x <= max_pos.x and pos.z >= min_pos.z and pos.z <= max_pos.z then local d_heat = heat - biome.heat_point local d_humid = humid - biome.humidity_point local dist = d_heat*d_heat + d_humid*d_humid -- Pythagorean distance if offsety <= max_pos.y then -- Within y limits of biome if dist < dist_min then dist_min = dist biome_closest = biome end elseif dist < dist_min_blend then -- Blend area above biome dist_min_blend = dist biome_closest_blend = biome end end end -- Carefully tune pseudorandom seed variation to avoid single node dither -- and create larger scale blending patterns similar to horizontal biome -- blend. local seed = math.floor(pos.y + (heat+humid) * 0.9) local rng = PseudoRandom(seed) if biome_closest_blend and dist_min_blend <= dist_min and rng:next(0, biome_closest_blend.vertical_blend) >= pos.y - biome_closest_blend.max_pos.y then return biome_closest_blend end return biome_closest end local function get_biome_at_index(i, pos) water_level = multi_map.get_absolute_centerpoint() local heat = heatmap[i] - math.max(pos.y, water_level)*elevation_chill local humid = humidmap[i] return calc_biome_from_noise(heat, humid, pos) end local function generate_biomes(data, a, minp, maxp) water_level = multi_map.get_absolute_centerpoint(multi_map.get_layer(minp.y)) local chulens = {x=maxp.x-minp.x+1, y=maxp.y-minp.y+1, z=maxp.z-minp.z+1} local index = 1 if not init then initialize(chulens) end calculate_noises(minp) for z=minp.z, maxp.z do for x=minp.x, maxp.x do local biome = nil local water_biome = nil local biome_stone = c_stone local depth_top = 0 local base_filler = 0 local depth_water_top = 0 local depth_riverbed = 0 local biome_y_min = -31000 local y_start = maxp.y local vi = a:index(x, maxp.y, z) local ystride = a.ystride local c_above = data[vi+ystride] if c_above == c_ignore then y_start = y_start - 1 c_above = data[vi] vi = vi - ystride end local air_above = c_above == c_air local river_water_above = c_above == c_rwater local water_above = c_above == c_water or river_water_above biomemap[index] = nil local nplaced = (air_above or water_above) and 0 or 31000 for y=y_start, minp.y-1, -1 do local c = data[vi] local is_stone_surface = (c == c_stone) and (air_above or water_above or not biome or y < biome_y_min) local is_water_surface = (c == c_water or c == c_rwater) and (air_above or not biome or y < biome_y_min) if is_stone_surface or is_water_surface then biome = get_biome_at_index(index, {x=x, y=y, z=z}) biome_stone = biome.node_stone if not biomemap[index] and is_stone_surface then biomemap[index] = biome end if not water_biome and is_water_surface then water_biome = biome end depth_top = biome.depth_top base_filler = math.max(depth_top + biome.depth_filler + nvals_filler_depth[index], 0) depth_water_top = biome.depth_water_top depth_riverbed = biome.depth_riverbed biome_y_min = biome.min_pos.y end if c == c_stone or c == biome_stone then local c_below = data[vi-ystride] if c_below == c_air or c_below == c_rwater or c_below == c_water then nplaced = 31000 end if river_water_above then if nplaced < depth_riverbed then data[vi] = biome.node_riverbed nplaced = nplaced + 1 else nplaced = 31000 river_water_above = false end elseif nplaced < depth_top then data[vi] = biome.node_top nplaced = nplaced + 1 elseif nplaced < base_filler then data[vi] = biome.node_filler nplaced = nplaced + 1 else data[vi] = biome_stone nplaced = 31000 end air_above = false water_above = false elseif c == c_water then if y > water_level-depth_water_top then data[vi] = biome.node_water_top else data[vi] = biome.node_water end nplaced = 0 air_above = false water_above = true elseif c == c_rwater then data[vi] = biome.node_river_water nplaced = 0 air_above = false water_above = true river_water_above = true elseif c == c_air then nplaced = 0 air_above = true water_above = false else nplaced = 31000 air_above = false water_above = false end vi = vi - ystride end if not biomemap[index] then biomemap[index] = water_biome end index = index + 1 end end end -- Walkable, liquid, and dustable: memoization tables for better performance local walkable = setmetatable({}, { __index = function(t, c) local is_walkable = false local ndef = minetest.registered_nodes[minetest.get_name_from_content_id(c)] if ndef and ndef.walkable then is_walkable = true end t[c] = is_walkable return is_walkable end, }) local liquid = setmetatable({}, { __index = function(t, c) local is_liquid = false local ndef = minetest.registered_nodes[minetest.get_name_from_content_id(c)] if ndef and ndef.liquidtype then is_liquid = ndef.liquidtype ~= "none" end t[c] = is_liquid return is_liquid end, }) local function can_place_deco(deco, data, vi, pattern,y) local offsety= multi_map.get_offset_y(y,multi_map.get_layer(y)) if offsety < deco.y_min or offsety > deco.y_max then return false end if not deco.place_on[data[vi]] then return false elseif deco.num_spawn_by <= 0 then return true end local spawn_by = deco.spawn_by local nneighs = deco.num_spawn_by for i, incr in ipairs(pattern) do vi = vi + incr if spawn_by[data[vi]] then nneighs = nneighs - 1 if nneighs < 1 then return true end end end return false end local function place_deco(deco, data, a, vm, minp, maxp, blockseed) local ps = PcgRandom(blockseed + 53) local carea_size = maxp.x - minp.x + 1 local sidelen = deco.sidelen if carea_size % sidelen > 0 then sidelen = carea_size end local divlen = carea_size / sidelen - 1 local area = sidelen*sidelen local ystride, zstride = a.ystride, a.zstride local pattern = {1, zstride, -1, -1, -zstride, -zstride, 1, 1, ystride, zstride, zstride, -1, -1, -zstride, -zstride, 1} -- Successive increments to iterate over 16 neighbouring nodes for z0=0, divlen do for x0=0, divlen do local p2d_center = {x=minp.x+sidelen*(x0+0.5), y=minp.z+sidelen*(z0+0.5)} local p2d_min = {x=minp.x+sidelen*x0, y=minp.z+sidelen*z0} local p2d_max = {x=minp.x+sidelen*(x0+1)-1, y=minp.z+sidelen*(z0+1)-1} local cover = false local nval = deco.use_noise and deco.noise:get_2d(p2d_center) or deco.fill_ratio local deco_count = 0 if nval >= 10 then cover = true deco_count = area else local deco_count_f = area * nval if deco_count_f >= 1 then deco_count = deco_count_f elseif deco_count_f > 0 and ps:next(1, 1000) <= deco_count_f * 1000 then deco_count = 1 end end local x = p2d_min.x - 1 local z = p2d_min.y for i=1, deco_count do if not cover then x = ps:next(p2d_min.x, p2d_max.x) z = ps:next(p2d_min.y, p2d_max.y) else x = x + 1 if x == p2d_max.x + 1 then z = z + 1 x = p2d_min.x end end local mapindex = carea_size * (z - minp.z) + (x - minp.x) if deco.flags["all_floors"] == true and deco.flags["all_ceilings"] == true then local biome_ok = true if deco.use_biomes and #biomemap > 0 then local biome_here = biomemap[mapindex] if biome_here and not deco.biomes[biome_here.name] then biome_ok = false end end if biome_ok then local size = (maxp.x - minp.x + 1) / 2 local floors = {} local ceilings = {} local is_walkable = false local vi = a:index(x, maxp.y, z) local walkable_above = walkable[data[vi]] for y = maxp.y-1, minp.y, -1 do vi = vi - ystride is_walkable = walkable[data[vi]] if is_walkable and not walkable_above then table.insert(floors, y) elseif walkable_above and not walkable then table.insert(ceilings, y) end walkable_above = is_walkable end if deco.flags["all_floors"] then for _, y in ipairs(floors) do if y >= biome.y_min and y <= biome.y_max then local pos = {x=x, y=y, z=z} if can_place_deco(deco, data, vi, pattern,y) then deco:generate(vm, ps, pos, false) end end end end if deco.flags["all_ceilings"] then for _, y in ipairs(ceilings) do if y >= biome.y_min and y <= biome.y_max then local pos = {x=x, y=y, z=z} if can_place_deco(deco, data, vi, pattern,y) then deco:generate(vm, ps, pos, true) end end end end end else local y = -31000 if deco.flags["liquid_surface"] == true then local vi = a:index(x, maxp.y, z) for yi=maxp.y, minp.y, -1 do local c = data[vi] if walkable[c] then break elseif liquid[c] then y = yi break end vi = vi - ystride end else local vi = a:index(x, maxp.y, z) for yi=maxp.y, minp.y, -1 do if walkable[data[vi]] then y = yi break end vi = vi - ystride end end if y >= deco.y_min and y <= deco.y_max and y >= minp.y and y <= maxp.y then local biome_ok = true if deco.use_biomes and #biomemap > 0 then local biome_here = biomemap[mapindex] if biome_here and not deco.biomes[biome_here.name] then biome_ok = false end end if biome_ok then local pos = {x=x, y=y, z=z} if can_place_deco(deco, data, a:index(x,y,z), pattern,y) then deco:generate(vm, ps, pos, false) end end end end end end end return 0 end local function get_blockseed(p, seed) return seed + p.z * 38134234 + p.y * 42123 + p.x * 23 end local function place_all_decos(data, a, vm, minp, maxp, seed) local emin = vm:get_emerged_area() local blockseed = get_blockseed(emin, seed) local nplaced = 0 for i, deco in pairs(decos) do nplaced = nplaced + place_deco(deco, data, a, vm, minp, maxp, blockseed) end return nplaced end local dustable = setmetatable({}, { __index = function(t, c) local is_dustable = false local ndef = minetest.registered_nodes[minetest.get_name_from_content_id(c)] if ndef and ndef.walkable then local dtype = ndef.drawtype if dtype and dtype == "normal" or dtype == "allfaces" or dtype == "allfaces_optional" or dtype == "glasslike" or dtype == "glasslike_framed" or dtype == "glasslike_framed_optional" then is_dustable = true end end t[c] = is_dustable return is_dustable end, }) local function dust_top_nodes(data, a, vm, minp, maxp) water_level = multi_map.get_absolute_centerpoint() local full_maxp = a.MaxEdge local index = 1 local ystride = a.ystride for z = minp.z, maxp.z do for x = minp.x, maxp.x do local biome = biomemap[index] if biome and biome.node_dust then local vi = a:index(x, full_maxp.y, z) local c_full_max = data[vi] local y_start if c_full_max == c_air then y_start = full_maxp.y - 1 elseif c_full_max == c_ignore then vi = a:index(x, maxp.y, z) local c_max = data[vi] if c_max == c_air then y_start = maxp.y end end if y_start then -- workaround for the 'continue' statement vi = a:index(x, y_start, z) local y = y_start for y0=y_start, minp.y-1, -1 do if data[vi] ~= c_air then y = y0 break end vi = vi - ystride end local c = data[vi] if dustable[c] and c ~= biome.node_dust then local pos = {x=x, y=y+1, z=z} vm:set_node_at(pos, {name=biome.node_dust_name}) end end end index = index + 1 end end end biomegen = { set_elevation_chill = set_elevation_chill, calculate_noises = calculate_noises, get_biome_at_index = get_biome_at_index, calc_biome_from_noise = calc_biome_from_noise, generate_biomes = generate_biomes, place_all_decos = place_all_decos, dust_top_nodes = dust_top_nodes, } function biomegen.generate_all(data, a, vm, minp, maxp, seed) water_level = multi_map.get_absolute_centerpoint(multi_map.get_layer(minp.y)) generate_biomes(data, a, minp, maxp) vm:set_data(data) place_all_decos(data, a, vm, minp, maxp, seed) minetest.generate_ores(vm, minp, maxp) vm:get_data(data) dust_top_nodes(data, a, vm, minp, maxp) end