Merge pull request 'Make structure generation more deterministic' (#4550) from kno10/VoxeLibre:mapgen-random-structures into master

Reviewed-on: https://git.minetest.land/VoxeLibre/VoxeLibre/pulls/4550
Reviewed-by: Mikita Wiśniewski <rudzik8@protonmail.com>
This commit is contained in:
the-real-herowl 2025-01-07 16:13:12 +01:00
commit 3c0f45c231
7 changed files with 383 additions and 298 deletions

File diff suppressed because it is too large Load Diff

@ -141,3 +141,58 @@ function mcl_mapgen_core.unregister_generator(id)
if rec.needs_param2 then param2 = param2 - 1 end if rec.needs_param2 then param2 = param2 - 1 end
--if rec.needs_level0 then level0 = level0 - 1 end --if rec.needs_level0 then level0 = level0 - 1 end
end end
-- Try to make decorations more deterministic in order, by sorting by rank and name
-- At least for low-rank this should make map seeds more comparable, but
-- adding for example a new structure can still change everything that comes
-- later, because currently decoration blockseeds are incremented sequentially
-- c.f., https://github.com/minetest/minetest/issues/14919
local pending_decorations = {}
function mcl_mapgen_core.register_decoration(def, callback)
if pending_decorations == nil then
-- Please do not register decorations in minetest.register_on_mods_loaded.
-- This should usually not happen, but modders may misuse this.
-- Nothing really bad should happen though, but the rank is ignored.
minetest.log("warning", "Decoration registered after mapgen: "..tostring(def.name))
minetest.register_decoration(def)
if callback then callback() end
return
end
def = table.copy(def) -- defensive deep copy, needed for water lily
def.callback = callback
pending_decorations[#pending_decorations+1] = def
end
local function sort_decorations()
local keys, map = {}, {}
for i, def in pairs(pending_decorations) do
-- Name, or fallback names, for better ordering:
local name = def.name or def.decoration
if not name and type(def.schematic) == "string" then -- filename based
local sc = string.split(def.schematic:gsub(".mts",""), "/")
name = sc[#sc]
end
if not name and type(def.schematic) == "table" and def.schematic.data then
name = "" -- "serialize" the schematic
for _, v in ipairs(def.schematic.data) do
if v.name then name = name .. v.name .. ":" end
end
if name == "" then name = nil end
end
-- sorting key is: rank, then insertion sequence, then name
local key = string.format("%05d:%04d:%s", def.rank or 1000, i, name or "deco")
keys[#keys+1] = key
map[key] = def
end
table.sort(keys)
for _, key in ipairs(keys) do
-- minetest.log("action", "Deco: "..key) -- dump the resulting order
minetest.register_decoration(map[key])
if map[key].callback then map[key].callback() end
end
pending_decorations = nil -- as we will not run again
end
minetest.register_on_mods_loaded(sort_decorations)

@ -427,31 +427,6 @@ if mg_name == "v6" then
dofile(modpath.."/v6.lua") dofile(modpath.."/v6.lua")
end end
-- This should be moved to mcl_structures eventually if the dependencies can be sorted out.
mcl_mapgen_core.register_generator("structures",nil, function(minp, maxp, blockseed)
local gennotify = minetest.get_mapgen_object("gennotify")
local has = false
local poshash = minetest.hash_node_position(minp)
for _,struct in pairs(mcl_structures.registered_structures) do
local pr = PseudoRandom(blockseed + 42)
if struct.deco_id then
for _, pos in pairs(gennotify["decoration#"..struct.deco_id] or {}) do
if struct.chunk_probability == nil or (not has and pr:next(1,struct.chunk_probability) == 1 ) then
mcl_structures.place_structure(vector.offset(pos,0,1,0),struct,pr,blockseed)
has=true
end
end
elseif struct.static_pos then
for _,p in pairs(struct.static_pos) do
if vector.in_area(p,minp,maxp) then
mcl_structures.place_structure(p,struct,pr,blockseed)
end
end
end
end
return false, false, false
end, 100, true)
minetest.register_lbm({ minetest.register_lbm({
label = "Fix grass palette indexes", -- This LBM fixes any incorrect grass palette indexes. label = "Fix grass palette indexes", -- This LBM fixes any incorrect grass palette indexes.
name = "mcl_mapgen_core:fix_grass_palette_indexes", name = "mcl_mapgen_core:fix_grass_palette_indexes",

@ -45,6 +45,7 @@ local function register_mgv6_decorations()
-- Doubletall grass -- Doubletall grass
minetest.register_decoration({ minetest.register_decoration({
rank = 1500,
deco_type = "schematic", deco_type = "schematic",
schematic = { schematic = {
size = { x=1, y=3, z=1 }, size = { x=1, y=3, z=1 },
@ -81,6 +82,7 @@ local function register_mgv6_decorations()
}, },
-- v6 hack: This makes sure large ferns only appear in jungles -- v6 hack: This makes sure large ferns only appear in jungles
spawn_by = { "mcl_core:jungletree", "mcl_flowers:fern" }, spawn_by = { "mcl_core:jungletree", "mcl_flowers:fern" },
rank = 1510, -- larger than fern
num_spawn_by = 1, num_spawn_by = 1,
place_on = {"group:grass_block_no_snow"}, place_on = {"group:grass_block_no_snow"},
@ -192,6 +194,7 @@ local function register_mgv6_decorations()
}, },
-- Small trick to make sure melon spawn in jungles -- Small trick to make sure melon spawn in jungles
spawn_by = { "mcl_core:jungletree", "mcl_flowers:fern" }, spawn_by = { "mcl_core:jungletree", "mcl_flowers:fern" },
rank = 1510, -- larger than fern
num_spawn_by = 1, num_spawn_by = 1,
y_min = 1, y_min = 1,
y_max = 40, y_max = 40,
@ -214,6 +217,7 @@ local function register_mgv6_decorations()
y_min = 1, y_min = 1,
y_max = mcl_vars.overworld_max, y_max = mcl_vars.overworld_max,
decoration = "mcl_flowers:tallgrass", decoration = "mcl_flowers:tallgrass",
rank = 1500,
}) })
minetest.register_decoration({ minetest.register_decoration({
deco_type = "simple", deco_type = "simple",
@ -230,6 +234,7 @@ local function register_mgv6_decorations()
y_min = 1, y_min = 1,
y_max = mcl_vars.overworld_max, y_max = mcl_vars.overworld_max,
decoration = "mcl_flowers:tallgrass", decoration = "mcl_flowers:tallgrass",
rank = 1500,
}) })
-- Seagrass and kelp -- Seagrass and kelp
@ -256,6 +261,7 @@ local function register_mgv6_decorations()
y_min = mcl_vars.overworld_min, y_min = mcl_vars.overworld_min,
y_max = 0, y_max = 0,
decoration = "mcl_ocean:seagrass_"..mat, decoration = "mcl_ocean:seagrass_"..mat,
rank = 1500,
}) })
minetest.register_decoration({ minetest.register_decoration({
deco_type = "simple", deco_type = "simple",
@ -276,6 +282,7 @@ local function register_mgv6_decorations()
y_min = mcl_vars.overworld_min, y_min = mcl_vars.overworld_min,
y_max = -5, y_max = -5,
decoration = "mcl_ocean:seagrass_"..mat, decoration = "mcl_ocean:seagrass_"..mat,
rank = 1500,
}) })
minetest.register_decoration({ minetest.register_decoration({
@ -356,6 +363,7 @@ local function register_mgv6_decorations()
y_min = 1, y_min = 1,
y_max = mcl_vars.overworld_max, y_max = mcl_vars.overworld_max,
decoration = "mcl_flowers:tallgrass", decoration = "mcl_flowers:tallgrass",
rank = 1500,
}) })
local mushrooms = {"mcl_mushrooms:mushroom_red", "mcl_mushrooms:mushroom_brown"} local mushrooms = {"mcl_mushrooms:mushroom_red", "mcl_mushrooms:mushroom_brown"}

@ -22,6 +22,7 @@ If nospawn is truthy the structure will not be placed by mapgen and the decorati
-- called before placement. denies placement when returning falsy. -- called before placement. denies placement when returning falsy.
after_place = function(pos,def,pr) after_place = function(pos,def,pr)
-- executed after successful placement -- executed after successful placement
rank = int, -- decorations are generated by increasing rank. Default for structures is 100, terrain features 900
sidelen = int, --length of one side of the structure. used for foundations. sidelen = int, --length of one side of the structure. used for foundations.
solid_ground = bool, -- structure requires solid ground solid_ground = bool, -- structure requires solid ground
make_foundation = bool, -- a foundation is automatically built for the structure. needs the sidelen param make_foundation = bool, -- a foundation is automatically built for the structure. needs the sidelen param

@ -1,5 +1,7 @@
mcl_structures.registered_structures = {} mcl_structures.registered_structures = {}
local RANDOM_SEED_OFFSET = 959 -- random constant that should be unique across each library
local disabled_structures = minetest.settings:get("mcl_disabled_structures") local disabled_structures = minetest.settings:get("mcl_disabled_structures")
if disabled_structures then disabled_structures = disabled_structures:split(",") if disabled_structures then disabled_structures = disabled_structures:split(",")
else disabled_structures = {} end else disabled_structures = {} end
@ -326,15 +328,15 @@ function mcl_structures.place_structure(pos, def, pr, blockseed, rot)
end end
local EMPTY_SCHEMATIC = { size = {x = 0, y = 0, z = 0}, data = { } } local EMPTY_SCHEMATIC = { size = {x = 0, y = 0, z = 0}, data = { } }
function mcl_structures.register_structure(name,def,nospawn) --nospawn means it will not be placed by mapgen decoration mechanism function mcl_structures.register_structure(name,def,nospawn) --nospawn means it will not be placed by mapgen decoration mechanism
if mcl_structures.is_disabled(name) then return end if mcl_structures.is_disabled(name) then return end
local flags = def.flags or "place_center_x, place_center_z, force_placement" local flags = def.flags or "place_center_x, place_center_z, force_placement"
def.name = name def.name = name
if not nospawn and def.place_on then if not nospawn and def.place_on then
minetest.register_on_mods_loaded(function() --make sure all previous decorations and biomes have been registered minetest.register_on_mods_loaded(function() --make sure all previous decorations and biomes have been registered
def.deco = minetest.register_decoration({ mcl_mapgen_core.register_decoration({
name = "mcl_structures:deco_"..name, name = "mcl_structures:"..name,
rank = def.rank or (def.terrain_feature and 900) or 100, -- run before regular decorations
deco_type = "schematic", deco_type = "schematic",
schematic = EMPTY_SCHEMATIC, schematic = EMPTY_SCHEMATIC,
place_on = def.place_on, place_on = def.place_on,
@ -347,10 +349,13 @@ function mcl_structures.register_structure(name,def,nospawn) --nospawn means it
biomes = def.biomes, biomes = def.biomes,
y_max = def.y_max, y_max = def.y_max,
y_min = def.y_min y_min = def.y_min
}) },
def.deco_id = minetest.get_decoration_id("mcl_structures:deco_"..name) function()
minetest.set_gen_notify({decoration=true}, { def.deco_id }) def.deco_id = minetest.get_decoration_id("mcl_structures:"..name)
--catching of gennotify happens in mcl_mapgen_core minetest.set_gen_notify({decoration=true}, { def.deco_id })
--catching of gennotify happens in mcl_mapgen_core
end
)
end) end)
end end
mcl_structures.registered_structures[name] = def mcl_structures.registered_structures[name] = def
@ -390,4 +395,28 @@ function mcl_structures.register_structure_spawn(def)
}) })
end end
-- To avoid a cyclic dependency, run this when modules have finished loading
minetest.register_on_mods_loaded(function()
mcl_mapgen_core.register_generator("structures", nil, function(minp, maxp, blockseed)
local gennotify = minetest.get_mapgen_object("gennotify")
for _,struct in pairs(mcl_structures.registered_structures) do
if struct.deco_id then
for _, pos in pairs(gennotify["decoration#"..struct.deco_id] or {}) do
local pr = PcgRandom(minetest.hash_node_position(pos) + blockseed + RANDOM_SEED_OFFSET)
if struct.chunk_probability == nil or pr:next(1, struct.chunk_probability) == 1 then
mcl_structures.place_structure(vector.offset(pos,0,1,0),struct,pr,blockseed)
if struct.chunk_probability then break end -- one (attempt) per chunk only
end
end
elseif struct.static_pos then
local pr = PcgRandom(blockseed + RANDOM_SEED_OFFSET)
for _, pos in pairs(struct.static_pos) do
if vector.in_area(pos, minp, maxp) then
mcl_structures.place_structure(pos, struct, pr, blockseed)
end
end
end
end
return false, false, false
end, 100, true)
end)

@ -134,6 +134,7 @@ local function get_fallen_tree_schematic(pos,pr)
end end
mcl_structures.register_structure("fallen_tree",{ mcl_structures.register_structure("fallen_tree",{
rank = 1100, -- after regular trees
place_on = {"group:grass_block"}, place_on = {"group:grass_block"},
terrain_feature = true, terrain_feature = true,
noise_params = { noise_params = {
@ -151,12 +152,9 @@ mcl_structures.register_structure("fallen_tree",{
y_min = minetest.get_mapgen_setting("water_level"), y_min = minetest.get_mapgen_setting("water_level"),
on_place = function(pos,def,pr) on_place = function(pos,def,pr)
local air_p1 = vector.offset(pos,-def.sidelen/2,1,-def.sidelen/2) local air_p1 = vector.offset(pos,-def.sidelen/2,1,-def.sidelen/2)
local air_p2 = vector.offset(pos,def.sidelen/2,1,def.sidelen/2) local air_p2 = vector.offset(air_p1,def.sidelen-1,0,def.sidelen-1)
local air = minetest.find_nodes_in_area(air_p1,air_p2,{"air"}) local air = minetest.find_nodes_in_area(air_p1,air_p2,{"air"})
if #air < ( def.sidelen * def.sidelen ) / 2 then return #air >= (def.sidelen * def.sidelen) / 2
return false
end
return true
end, end,
place_func = function(pos,def,pr) place_func = function(pos,def,pr)
local schem=get_fallen_tree_schematic(pos,pr) local schem=get_fallen_tree_schematic(pos,pr)