MineClone2/mods/MAPGEN/mcl_structures/api.lua

468 lines
22 KiB
Lua

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)