mcl_structures.registered_structures = {} local peaceful = minetest.settings:get_bool("only_peaceful_mobs", false) local mob_cap_player = tonumber(minetest.settings:get("mcl_mob_cap_player")) or 75 local mob_cap_animal = tonumber(minetest.settings:get("mcl_mob_cap_animal")) or 10 local structure_boost = tonumber(minetest.settings:get("mcl_structures_boost")) or 1 local worldseed = minetest.get_mapgen_setting("seed") local RANDOM_SEED_OFFSET = 959 -- random constant that should be unique across each library local logging = minetest.settings:get_bool("mcl_logging_structures", true) local mg_name = minetest.get_mapgen_setting("mg_name") local disabled_structures = minetest.settings:get("mcl_disabled_structures") if disabled_structures then disabled_structures = disabled_structures:split(",") else disabled_structures = {} end function mcl_structures.is_disabled(structname) return table.indexof(disabled_structures,structname) ~= -1 end local ROTATIONS = { "0", "90", "180", "270" } function mcl_structures.parse_rotation(rotation, pr) if rotation == "random" and pr then return ROTATIONS[pr:next(1,#ROTATIONS)] end return rotation end --- Get the size after rotation. -- @param size vector: Size information -- @param rotation string or number: only 0, 90, 180, 270 are allowed -- @return vector: new vector, for safety function mcl_structures.size_rotated(size, rotation) if rotation == "90" or rotation == "270" or rotation == 90 or rotation == 270 then return vector.new(size.z, size.y, size.x) end return vector.copy(size) end --- Get top left position after apply centering flags and padding. -- @param pos vector: Placement position -- @param[opt] size vector: Size information -- @param[opt] flags string or table: as in minetest.place_schematic, place_center_x, place_center_y -- @param[opt] padding number: optional margin (integer) -- @return vector: new vector, for safety function mcl_structures.top_left_from_flags(pos, size, flags, padding) local dx, dy, dz = 0, 0, 0 -- must match src/mapgen/mg_schematic.cpp to be consistent if type(flags) == "table" then if flags["place_center_x"] ~= nil then dx = -math.floor((size.x-1)*0.5) end if flags["place_center_y"] ~= nil then dy = -math.floor((size.y-1)*0.5) end if flags["place_center_z"] ~= nil then dz = -math.floor((size.z-1)*0.5) end elseif type(flags) == "string" then if string.find(flags, "place_center_x") then dx = -math.floor((size.x-1)*0.5) end if string.find(flags, "place_center_y") then dy = -math.floor((size.y-1)*0.5) end if string.find(flags, "place_center_z") then dz = -math.floor((size.z-1)*0.5) end end if padding then dx = dx - padding dz = dz - padding end return vector.offset(pos, dx, dy, dz) end -- Expected contents of param: -- pos vector: position (center.x, base.y, center.z) -- flags NOT supported -- size vector: structure size after rotation (!) -- yoffset number: relative to base.y, typically <= 0 -- y_min number: minimum y range permitted -- y_max number: maximum y range permitted -- schematic string or schematic: as in minetest.place_schematic -- rotation string: as in minetest.place_schematic -- replacement table: as in minetest.place_schematic -- force_placement boolean: as in minetest.place_schematic -- prepare table: instructions for preparation (usually from definition) -- tolerance number: tolerable ground unevenness, -1 to disable, default 10 -- foundation boolean or number: level ground underneath structure (true is a minimum depth of -3) -- clearance boolean or string or number: clear overhead area (offset, or "top" to begin over the structure only) -- padding number: additional padding to increase the area, default 1 -- corners number: corner smoothing of foundation and clearance, default 1 -- pr PcgRandom: random generator -- name string: for logging local function emerge_schematic_vm(vm, param) local pos, size, prepare, surface_mat = param.pos, param.size, param.prepare, nil -- adjust ground to a move level position if pos and size and prepare and (prepare.tolerance or 10) >= 0 then pos, surface_mat = mcl_structures.find_level(vm, pos, size, prepare.tolerance) if not pos then minetest.log("warning", "[mcl_structures] Not spawning "..tostring(param.name or param.schematic.name).." at "..minetest.pos_to_string(param.pos).." because ground is too uneven.") return nil end if param.y_max and pos.y > param.y_max then pos.y = param.y_max end if param.y_min and pos.y < param.y_min then pos.y = param.y_min end end -- Prepare the environment if prepare and (prepare.clearance or prepare.foundation) then -- Get materials from biome: local b = mg_name ~= "v6" and minetest.registered_biomes[minetest.get_biome_name(minetest.get_biome_data(pos).biome)] local node_top = b and b.node_top or (surface_mat and surface_mat.name) or "mcl_core:dirt_with_grass" local node_filler = b and b.node_filler or "mcl_core:dirt" local node_stone = b and b.node_stone or "mcl_core:stone" -- FIXME: not yet used: local node_dust = b and b.node_dust local node_top_param2 = node_top == "mcl_core:dirt_with_grass" and b._mcl_grass_palette_index or 0 -- grass color, also other materials? local corners, padding, depth = prepare.corners or 1, prepare.padding or 1, (type(prepare.foundation) == "number" and prepare.foundation) or -4 local gp = vector.offset(pos, -math.floor((size.x-1)*0.5) - padding, 0, -math.floor((size.z-1)*0.5)-padding) local gs = vector.offset(size, padding*2, depth, padding*2) if prepare.clearance then -- minetest.log("action", "[mcl_structures] clearing air "..minetest.pos_to_string(gp).." +"..minetest.pos_to_string(gs).." corners "..corners) -- TODO: add more parameters? local yoff, height = 0, size.y + (param.yoffset or 0) if prepare.clearance == "top" or prepare.clearance == "above" then yoff, height = height, 0 elseif type(prepare.clearance) == "number" then yoff, height = prepare.clearance, height - prepare.clearance end mcl_structures.clearance(vm, gp.x, gp.y + yoff, gp.z, gs.x, height, gs.z, corners, {name=node_top, param2=node_top_param2}, param.pr) end if prepare.foundation then -- minetest.log("action", "[mcl_structures] fill foundation "..minetest.pos_to_string(gp).." +"..minetest.pos_to_string(gs).." corners "..corners) local depth = (type(prepare.foundation) == "number" and prepare.foundation) or -3 mcl_structures.foundation(vm, gp.x, gp.y - 1, gp.z, gs.x, depth, gs.z, corners, {name=node_top, param2=node_top_param2}, {name=node_filler}, {name=node_stone}, param.pr) end end -- place the actual schematic pos.y = pos.y + (param.yoffset or 0) minetest.place_schematic_on_vmanip(vm, pos, param.schematic, param.rotation, param.replacements, param.force_placement, "place_center_x,place_center_z") return pos end -- Additional parameters: -- emin vector: emerge area minimum -- emax vector: emerge area maximum -- after_placement_callback function: callback after placement, (pmin, pmax, size, rotation, pr, param) -- callback_param table: additional parameters to callback function local function emerge_schematic(blockpos, action, calls_remaining, param) if calls_remaining >= 1 then return end local vm = VoxelManip() vm:read_from_map(param.emin, param.emax) local pos = emerge_schematic_vm(vm, param) vm:write_to_map(true) if not pos then return end -- repair walls (TODO: port to vmanip? but no "vm.find_nodes_in_area" yet) local pmin = vector.offset(pos, -math.floor((param.size.x-1)*0.5), 0, -math.floor((param.size.z-1)*0.5)) local pmax = vector.offset(pmin, param.size.x-1, param.size.y-1, param.size.z-1) if pmin and pmax and mcl_walls then for _, n in pairs(minetest.find_nodes_in_area(pmin, pmax, { "group:wall" })) do mcl_walls.update_wall(n) end end if pmin and pmax and param.after_placement_callback then param.after_placement_callback(pmin, pmax, param.size, param.rotation, param.pr, param.callback_param) end end local DEFAULT_PREPARE = { tolerance = 8, foundation = -3, clearance = false, padding = 1, corners = 1 } local DEFAULT_FLAGS = "place_center_x,place_center_z" function mcl_structures.place_schematic(pos, yoffset, y_min, y_max, schematic, rotation, replacements, force_placement, flags, prepare, pr, after_placement_callback, callback_param) if schematic and not schematic.size then -- e.g., igloo still passes filenames schematic = loadstring(minetest.serialize_schematic(schematic, "lua", {lua_use_comments = false, lua_num_indent_spaces = 0}) .. " return schematic")() end rotation = mcl_structures.parse_rotation(rotation, pr) local size = mcl_structures.size_rotated(schematic.size, rotation) -- area to emerge; note that alignment flags could be non-center, although we almost always use place_center_x,place_center_z local pmin = mcl_structures.top_left_from_flags(pos, flags or DEFAULT_FLAGS) local ppos = vector.offset(pmin, math.floor((size.x-1)*0.5), 0, math.floor((size.z-1)*0.5)) -- center local pmax = vector.offset(pmin, size.x - 1, size.y - 1, size.z - 1) if prepare == nil or prepare == true then prepare = DEFAULT_PREPARE end if prepare == false then prepare = {} end -- area to emerge. Add some margin to allow for finding better suitable ground etc. local emin, emax = vector.offset(pmin, -1, -5, -1), vector.offset(pmax, 1, 5, 1) if prepare then emin.y = emin.y - (prepare.tolerance or 10) end -- if we need to generate a foundation, we need to emerge a larger area: if prepare.foundation or prepare.clearance then -- these functions need some extra margins local padding, depth, height = (prepare.padding or 0) + 3, (prepare.depth or -4) - 15, size.y * 2 + 6 emin = vector.offset(pmin, -padding, depth + math.min(yoffset or 0, 0), -padding) emax = vector.offset(pmax, padding, height + math.max(yoffset or 0, 0), padding) end minetest.emerge_area(emin, emax, emerge_schematic, { emin=emin, emax=emax, name=schematic.name or (type(schematic)=="string" and schematic), pos=ppos, size=size, yoffset=yoffset, y_min=y_min, y_max=y_max, schematic=schematic, rotation=rotation, replacements=replacements, force_placement=force_placement, prepare=prepare, pr=pr, after_placement_callback=after_placement_callback, callback_param=callback_param }) end -- Call all on_construct handlers -- also called from mcl_villages for job sites function mcl_structures.init_node_construct(pos) local node = minetest.get_node(pos) local def = node and minetest.registered_nodes[node.name] if def and def.on_construct then def.on_construct(pos) end end -- Find nodes to call on_construct handlers for function mcl_structures.construct_nodes(p1,p2,nodes) local nn = minetest.find_nodes_in_area(p1,p2,nodes) for _,p in pairs(nn) do mcl_structures.init_node_construct(p) end end function mcl_structures.fill_chests(p1,p2,loot,pr) for it,lt in pairs(loot) do local nodes = minetest.find_nodes_in_area(p1, p2, it) for _,p in pairs(nodes) do local lootitems = mcl_loot.get_multi_loot(lt, pr) mcl_structures.init_node_construct(p) local meta = minetest.get_meta(p) local inv = meta:get_inventory() mcl_loot.fill_inventory(inv, "main", lootitems, pr) end end end function mcl_structures.spawn_mobs(mob,spawnon,p1,p2,pr,n,water) n = n or 1 local sp = {} if water then local nn = minetest.find_nodes_in_area(p1,p2,spawnon) for k,v in pairs(nn) do if minetest.get_item_group(minetest.get_node(vector.offset(v,0,1,0)).name,"water") > 0 then table.insert(sp,v) end end else sp = minetest.find_nodes_in_area_under_air(p1,p2,spawnon) end table.shuffle(sp) local count = 0 local mob_def = minetest.registered_entities[mob] local enabled = (not peaceful) or (mob_def and mob_def.spawn_class ~= "hostile") for _,node in pairs(sp) do if enabled and count < n and minetest.add_entity(vector.offset(node, 0, 1, 0), mob) then count = count + 1 end minetest.get_meta(node):set_string("spawnblock", "yes") -- note: also in peaceful mode! end end function mcl_structures.place_structure(pos, def, pr, blockseed, rot) if not def then return end local log_enabled = logging and not def.terrain_feature -- currently only used by fallen_tree, to check for sufficient empty space to fall if def.on_place and not def.on_place(pos,def,pr,blockseed) then if log_enabled then minetest.log("warning","[mcl_structures] "..def.name.." at "..minetest.pos_to_string(pos).." not placed. on_place conditions not satisfied.") end return false end -- Apply vertical offset for schematic local yoffset = (type(def.y_offset) == "function" and def.y_offset(pr)) or def.y_offset or 0 if def.schematics and #def.schematics > 0 then local schematic = def.schematics[pr:next(1,#def.schematics)] rot = mcl_structures.parse_rotation(rot or "random", pr) if not def.daughters then mcl_structures.place_schematic(pos, yoffset, def.y_min, def.y_max, schematic, rot, def.replacements, def.force_placement, "place_center_x,place_center_z", def.prepare, pr, function(p1, p2, size, rotation) if def.loot then mcl_structures.fill_chests(p1,p2,def.loot,pr) end if def.construct_nodes then mcl_structures.construct_nodes(p1,p2,def.construct_nodes) end if def.after_place then def.after_place(pos,def,pr,p1,p2,size,rotation) end if log_enabled then minetest.log("action", "[mcl_structures] "..def.name.." spawned at "..minetest.pos_to_string(pos)) end end) else -- currently only nether bulwarks + nether outpost with bridges? -- FIXME: this really needs to be run in a single emerge! mcl_structures.place_schematic(pos, yoffset, def.y_min, def.y_max, schematic, rot, def.replacements, def.force_placement, "place_center_x,place_center_z", def.prepare, pr, function(p1, p2, size, rotation) for i,d in pairs(def.daughters) do local ds = d.files[pr:next(1,#d.files)] -- Daughter schematics are not loaded yet. if ds and not ds.size then ds = loadstring(minetest.serialize_schematic(ds, "lua", {lua_use_comments = false, lua_num_indent_spaces = 0}) .. " return schematic")() end -- FIXME: apply centering, apply parent rotation. local rot = d.rot or 0 local dsize = mcl_structures.size_rotated(ds.size, rot) local p = vector.new(math.floor((p1.x+p2.x)*0.5) + d.pos.x - math.floor((dsize.x-1)*0.5), p1.y + (yoffset or 0) + d.pos.y, math.floor((p1.z+p2.z)*0.5) + d.pos.z - math.floor((dsize.z-1)*0.5)) local callback = nil if i == #def.daughters then callback = function() -- Note: deliberately pos, p1 and p2 from the parent, as these are calls to the parent. if def.loot then mcl_structures.fill_chests(p1,p2,def.loot,pr) end if def.construct_nodes then mcl_structures.construct_nodes(p1,p2,def.construct_nodes) end if def.after_place then def.after_place(pos,def,pr,p1,p2,size,rotation) end if log_enabled then minetest.log("action", "[mcl_structures] "..def.name.." spawned at "..minetest.pos_to_string(pos)) end end end mcl_structures.place_schematic(p, yoffset, d.y_min or def.y_min, d.y_max or def.y_max, ds, rot, nil, true, "place_center_x,place_center_y", d.prepare, pr, callback) end end) end if log_enabled then minetest.log("verbose", "[mcl_structures] "..def.name.." to be placed at "..minetest.pos_to_string(pos)) end return true end if not def.place_func then minetest.log("warning","[mcl_structures] no schematics and no place_func for schematic "..def.name) return false end if def.solid_ground and def.sidelen and not def.prepare then -- TODO: this assumes place_center, make padding configurable, use actual size? local ground_p1 = vector.offset(pos,-math.floor(def.sidelen/2),-1,-math.floor(def.sidelen/2)) local ground_p2 = vector.offset(ground_p1,def.sidelen-1,0,def.sidelen-1) local solid = minetest.find_nodes_in_area(ground_p1,ground_p2,{"group:solid"}) if #solid < def.sidelen * def.sidelen then if log_enabled then minetest.log("warning", "[mcl_structures] "..def.name.." at "..minetest.pos_to_string(pos).." not placed. No solid ground.") end return false end end local pp = yoffset ~= 0 and vector.offset(pos, 0, yoffset, 0) or pos if def.place_func and def.place_func(pp,def,pr,blockseed) then if not def.after_place or (def.after_place and def.after_place(pp,def,pr,blockseed)) then if def.prepare then minetest.log("warning", "[mcl_structures] needed prepare for "..def.name.." placed at "..minetest.pos_to_string(pp).." but did not have size information") end if def.sidelen then local p1, p2 = vector.offset(pos,-def.sidelen,-def.sidelen,-def.sidelen), vector.offset(pos,def.sidelen,def.sidelen,def.sidelen) if def.loot then mcl_structures.fill_chests(p1,p2,def.loot,pr) end if def.construct_nodes then mcl_structures.construct_nodes(p1,p2,def.construct_nodes) end end if log_enabled then minetest.log("action","[mcl_structures] "..def.name.." placed at "..minetest.pos_to_string(pp)) end return true else minetest.log("warning","[mcl_structures] after_place failed for schematic "..def.name) return false end elseif log_enabled then minetest.log("warning","[mcl_structures] place_func failed for schematic "..def.name) end end 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 if mcl_structures.is_disabled(name) then return end def.name = name def.prepare = def.prepare or (type(def.make_foundation) == table and def.make_foundation) def.flags = def.flags or "place_center_x, place_center_z, force_placement" if def.filenames then if #def.filenames == 0 then minetest.log("warning","[mcl_structures] schematic "..name.." has an empty list of filenames.") end def.schematics = def.schematics or {} for _, filename in ipairs(def.filenames) do if not mcl_util.file_exists(filename) then minetest.log("warning","[mcl_structures] schematic "..name.." is missing file "..tostring(filename)) else -- load, and ensure we have size information local s = nil --minetest.read_schematic(filename) if not s or not s.size then s = loadstring(minetest.serialize_schematic(filename, "lua", {lua_use_comments = false, lua_num_indent_spaces = 0}) .. " return schematic")() end if not s then minetest.log("warning", "[mcl_structures] failed to load schematic "..tostring(filename)) elseif not s.size then minetest.log("warning", "[mcl_structures] no size information for schematic "..tostring(filename)) else if logging then minetest.log("verbose", "[mcl_structures] loaded schematic "..tostring(filename).." size "..minetest.pos_to_string(s.size)) end if not s.name then s.name = name or filename end table.insert(def.schematics, s) end end end end if not def.noise_params and def.chunk_probability and not def.fill_ratio then def.fill_ratio = 1.1/80/80 -- 1 per chunk, controlled by chunk probability only end mcl_structures.registered_structures[name] = def if nospawn then return end -- ice column, boulder if def.place_on then minetest.register_on_mods_loaded(function() --make sure all previous decorations and biomes have been registered mcl_mapgen_core.register_decoration({ name = "mcl_structures:"..name, rank = def.rank or (def.terrain_feature and 900) or 100, -- run before regular decorations deco_type = "schematic", schematic = EMPTY_SCHEMATIC, place_on = def.place_on, spawn_by = def.spawn_by, num_spawn_by = def.num_spawn_by, sidelen = 80, -- no def.sidelen subdivisions for now fill_ratio = def.fill_ratio, noise_params = def.noise_params, flags = def.flags, biomes = def.biomes, y_max = def.y_max, y_min = def.y_min }, function() def.deco_id = minetest.get_decoration_id("mcl_structures:"..name) minetest.set_gen_notify({decoration=true}, { def.deco_id }) --catching of gennotify happens in mcl_mapgen_core end ) end) end end local structure_spawns = {} function mcl_structures.register_structure_spawn(def) --name,y_min,y_max,spawnon,biomes,chance,interval,limit minetest.register_abm({ label = "Spawn "..def.name, nodenames = def.spawnon, min_y = def.y_min or -31000, max_y = def.y_max or 31000, interval = def.interval or 60, chance = def.chance or 5, action = function(pos, node, active_object_count, active_object_count_wider) local limit = def.limit or 7 if active_object_count_wider > limit + mob_cap_animal then return end if active_object_count_wider > mob_cap_player then return end local p = vector.offset(pos,0,1,0) local pname = minetest.get_node(p).name if def.type_of_spawning == "water" then if pname ~= "mcl_core:water_source" and pname ~= "mclx_core:river_water_source" then return end else if pname ~= "air" then return end end if minetest.get_meta(pos):get_string("spawnblock") == "" then return end if mg_name ~= "v6" and mg_name ~= "singlenode" and def.biomes then if table.indexof(def.biomes,minetest.get_biome_name(minetest.get_biome_data(p).biome)) == -1 then return end end local mobdef = minetest.registered_entities[def.name] if mobdef.can_spawn and not mobdef.can_spawn(p) then return end minetest.add_entity(p,def.name) 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)