settlements/buildings.lua

628 lines
21 KiB
Lua

local S = settlements.S
local c_air = minetest.get_content_id("air")
local default_path_material = "default:gravel"
local default_deep_platform = "default:stone"
local default_shallow_platform = "default:dirt"
local surface_mats = settlements.surface_materials
local settlement_waypoint_def = {
default_name = S("a settlement"),
default_color = 0xFFFFFF,
discovery_volume_radius = tonumber(minetest.settings:get("settlements_discovery_range")) or 30,
}
if minetest.settings:get_bool("settlements_hud_requires_item", true) then
local item_required = minetest.settings:get("settlements_hud_item_required")
if item_required == nil or item_required == "" then
item_required = "map:mapping_kit"
end
settlement_waypoint_def.visibility_requires_item = item_required
end
if minetest.settings:get_bool("settlements_show_in_hud", true) then
settlement_waypoint_def.visibility_volume_radius = tonumber(minetest.settings:get("settlements_visibility_range")) or 600
settlement_waypoint_def.on_discovery = named_waypoints.default_discovery_popup
end
named_waypoints.register_named_waypoints("settlements", settlement_waypoint_def)
local buildable_to_set
local buildable_to = function(c_node)
if buildable_to_set then return buildable_to_set[c_node] end
buildable_to_set = {}
for k, v in pairs(minetest.registered_nodes) do
if v.buildable_to then
buildable_to_set[minetest.get_content_id(k)] = true
end
end
-- TODO: some way to discriminate between registered_settlements? For now, apply ignore_materials universally.
for _, def in pairs(settlements.registered_settlements) do
if def.ignore_surface_materials then
for _, ignore_material in ipairs(def.ignore_surface_materials) do
buildable_to_set[minetest.get_content_id(ignore_material)] = true
end
end
end
return buildable_to_set[c_node]
end
-- function to fill empty space below baseplate when building on a hill
local function ground(pos, data, va, c_shallow, c_deep) -- role model: Wendelsteinkircherl, Brannenburg
local p2 = vector.new(pos)
local cnt = 0
local mat = c_shallow
p2.y = p2.y-1
local depth = math.random(2,4)
while true do
cnt = cnt+1
if cnt > 20 then break end
if cnt > depth then mat = c_deep end
local vi = va:index(p2.x, p2.y, p2.z)
if not buildable_to(data[vi]) then break end -- stop when we hit solid ground
data[vi] = mat
p2.y = p2.y-1
end
end
-- for displacing building schematic positions so that they're more centered
local function get_corner_pos(center_pos, schematic, rotation)
local pos = center_pos
local size = vector.new(schematic.size)
size.y = 0
if rotation == "90" or rotation == "270" then
local tempz = size.z
size.z = size.x
size.x = tempz
end
local corner1 = vector.subtract(pos, vector.floor(vector.divide(size, 2)))
local corner2 = vector.add(schematic.size, corner1)
return corner1, corner2
end
local group_ids = {}
local is_in_group = function(c_id, groupname)
local grouplist = group_ids[groupname]
if grouplist then
return grouplist[c_id]
end
grouplist = {}
for name, def in pairs(minetest.registered_nodes) do
if minetest.get_item_group(name, groupname) > 0 then
grouplist[minetest.get_content_id(name)] = true
end
end
group_ids[groupname] = grouplist
return grouplist[c_id]
end
-- function clear space above baseplate
local function terraform(data, va, settlement_info)
local c_air = minetest.get_content_id(settlement_info.def.platform_air or "air")
local c_shallow = minetest.get_content_id(settlement_info.def.platform_shallow or default_shallow_platform)
local c_deep = minetest.get_content_id(settlement_info.def.platform_deep or default_deep_platform)
local fheight
local fwidth
local fdepth
for _, built_house in ipairs(settlement_info) do
local schematic_data = built_house.schematic_info
local replace_air = schematic_data.platform_clear_above
local build_platform = schematic_data.platform_build_below
if replace_air == nil then
replace_air = true
end
if build_platform == nil then
build_platform = true
end
local skip_group_above = schematic_data.platform_ignore_group_above
if skip_group_above then
skip_group_above = skip_group_above:gsub("^group:", "")
end
local size = schematic_data.schematic.size
local pos = built_house.build_pos_min
if built_house.rotation == "0" or built_house.rotation == "180"
then
fwidth = size.x
fdepth = size.z
else
fwidth = size.z
fdepth = size.x
end
fheight = size.y
if replace_air then-- remove trees and leaves above
fheight = fheight * 3
end
--
-- now that every info is available -> create platform and clear space above
--
for zi = 0,fdepth-1 do
for yi = 0,fheight do
for xi = 0,fwidth-1 do
if yi == 0 and build_platform then
local p = {x=pos.x+xi, y=pos.y, z=pos.z+zi}
ground(p, data, va, c_shallow, c_deep)
elseif replace_air then
local p = vector.new(pos.x+xi, pos.y+yi, pos.z+zi)
local vi = va:indexp(p)
if not (skip_group_above and is_in_group(data[vi], skip_group_above)) then
data[vi] = c_air
end
end
end
end
end
end
end
-------------------------------------------------------------------------------
-- function to find surface block y coordinate
-------------------------------------------------------------------------------
local function find_surface(pos, data, va, altitude_min, altitude_max)
if not va:containsp(pos) then return nil end
local y = pos.y
-- starting point for looking for surface
local previous_vi = va:indexp(pos)
local previous_node = data[previous_vi]
local itter -- count up or down
if buildable_to(previous_node) then
itter = -1 -- going down
else
itter = 1 -- going up
end
for cnt = 0, 100 do
local next_vi = previous_vi + va.ystride * itter
y = y + itter
if (altitude_min and altitude_min > y) or (altitude_max and altitude_max < y) then
-- an altitude range was specified and we're outside it
return nil
end
if not va:containsi(next_vi) then return nil end
local next_node = data[next_vi]
if buildable_to(previous_node) ~= buildable_to(next_node) then
--we transitioned through what may be a surface. Test if it was the right material.
local above_node, below_node, above_vi, below_vi
if itter > 0 then
-- going up
above_node, below_node = next_node, previous_node
above_vi, below_vi = next_vi, previous_vi
else
above_node, below_node = previous_node, next_node
above_vi, below_vi = previous_vi, next_vi
end
if surface_mats[below_node] then
return va:position(below_vi), below_node
else
return nil
end
end
previous_vi = next_vi
previous_node = next_node
end
return nil
end
local function shallowCopy(original)
local copy = {}
for key, value in pairs(original) do
copy[key] = value
end
return copy
end
-- randomize table
local function shuffle(tbl)
local ret = shallowCopy(tbl)
local size = #ret
for i = size, 1, -1 do
local rand = math.random(size)
ret[i], ret[rand] = ret[rand], ret[i]
end
return ret
end
-- If the building fits into the areastore without overlapping existing buildings,
-- add it to the areastore and return true. Otherwise return false.
local function insert_into_area(building, areastore)
local buffer = building.schematic_info.buffer or 0
local edge1 = vector.new(building.build_pos_min)
edge1 = vector.subtract(edge1, buffer)
edge1.y = 0
local edge2 = vector.new(building.build_pos_max)
edge2 = vector.add(edge2, buffer)
edge2.y = 1
local result = areastore:get_areas_in_area(edge1, edge2, true)
if next(result) then
return false
end
areastore:insert_area(edge1, edge2, "")
return true
end
local possible_rotations = {"0", "90", "180", "270"}
-------------------------------------------------------------------------------
-- everything necessary to pick a fitting next building
-------------------------------------------------------------------------------
local function pick_next_building(pos_surface, surface_material, count_buildings, settlement_info, settlement_def, areastore)
local number_of_buildings = settlement_info.number_of_buildings
local randomized_schematic_table = shuffle(settlement_def.schematics)
-- pick schematic
local size = #randomized_schematic_table
for i = size, 1, -1 do
-- already enough buildings of that type?
local current_schematic = randomized_schematic_table[i]
local current_schematic_name = current_schematic.name
count_buildings[current_schematic_name] = count_buildings[current_schematic_name] or 0
if count_buildings[current_schematic_name] < current_schematic.max_num*number_of_buildings then
local rotation = possible_rotations[math.random(#possible_rotations)]
local corner1, corner2 = get_corner_pos(pos_surface, current_schematic.schematic, rotation)
local building_info = {
center_pos = pos_surface,
build_pos_min = corner1,
build_pos_max = corner2,
schematic_info = current_schematic,
rotation = rotation,
surface_mat = surface_material,
}
if insert_into_area(building_info, areastore) then
count_buildings[current_schematic.name] = count_buildings[current_schematic.name] +1
return building_info
end
end
end
return nil
end
local function select_replacements(source)
local destination = {}
if source then
for original, replacement in pairs(source) do
if type(replacement) == "table" then
replacement = replacement[math.random(1, #replacement)]
end
destination[original] = replacement
end
end
return destination
end
-------------------------------------------------------------------------------
-- fill settlement_info with LVM
--------------------------------------------------------------------------------
local function create_site_plan(minp, maxp, data, va, existing_settlement_name)
-- find center of chunk
local center = vector.floor({
x=maxp.x-(maxp.x - minp.x)/2,
y=maxp.y,
z=maxp.z-(maxp.z - minp.z)/2,
})
-- find center_surface of chunk
local center_surface_pos, surface_material = find_surface(center, data, va)
if not center_surface_pos then
return nil
end
-- get a list of all the settlement defs that can be made on this surface mat
local material_defs = surface_mats[surface_material]
local registered_settlements = {}
-- cull out any that have altitude min/max set outside the range of the chunk
for _, def in ipairs(material_defs) do
if (not def.altitude_min or def.altitude_min < maxp.y) and
(not def.altitude_max or def.altitude_max > minp.y) then
table.insert(registered_settlements, def)
end
end
-- Nothing to pick from
if #registered_settlements == 0 then
return nil
end
-- pick one at random
local settlement_def = registered_settlements[math.random(1, #registered_settlements)]
-- Get a name for the settlement.
local name = existing_settlement_name or settlement_def.generate_name(center)
local min_number = settlement_def.building_count_min or 5
local max_number = settlement_def.building_count_max or 25
local settlement_info = {}
settlement_info.def = settlement_def
settlement_info.name = name
local number_of_buildings = math.random(min_number, max_number)
settlement_info.number_of_buildings = number_of_buildings
local areastore = AreaStore() -- An efficient structure for storing building footprints and testing for overlaps
settlement_info.areastore = areastore
areastore:reserve(number_of_buildings)
settlement_info.replacements = select_replacements(settlement_def.replacements)
settlement_info.replacements_optional = select_replacements(settlement_def.replacements_optional)
local count_buildings = {}
-- first building is selected from the central_schematics list, or randomly from schematics if that isn't defined.
local central_list = settlement_def.central_schematics or settlement_def.schematics
local townhall = central_list[math.random(#central_list)]
local rotation = possible_rotations[math.random(#possible_rotations)]
-- add to settlement info table
local number_built = 1
local corner1, corner2 = get_corner_pos(center_surface_pos, townhall.schematic, rotation)
local center_building = {
center_pos = center_surface_pos,
build_pos_min = corner1,
build_pos_max = corner2,
schematic_info = townhall,
rotation = rotation,
surface_mat = surface_material,
}
settlement_info[number_built] = center_building
insert_into_area(center_building, areastore)
-- now some buildings around in a circle, radius = size of town center
local x, z = center_surface_pos.x, center_surface_pos.z
local r = math.max(townhall.schematic.size.x, townhall.schematic.size.z) + (townhall.buffer or 0)
-- draw circles around center and increase radius by math.random(2,5)
for circle = 1,20 do
if number_built < number_of_buildings then
-- set position on imaginary circle
for angle_step = 0, 360, 15 do
local angle = angle_step * math.pi / 180
local ptx, ptz = x + r * math.cos( angle ), z + r * math.sin( angle )
ptx = math.floor(ptx + 0.5) -- round
ptz = math.floor(ptz + 0.5)
local pos1 = { x=ptx, y=center_surface_pos.y, z=ptz}
local pos_surface, surface_material = find_surface(pos1, data, va, settlement_def.altitude_min, settlement_def.altitude_max)
if pos_surface then
local building_info = pick_next_building(pos_surface, surface_material, count_buildings, settlement_info, settlement_def, areastore)
if building_info then
number_built = number_built + 1
settlement_info[number_built] = building_info
local name_built = building_info.schematic_info.name
--building_counts[name_built] = (building_counts[name_built] or 0) + 1
if number_of_buildings == number_built then
break
end
end
else
break
end
end
r = r + math.random(2,5)
else
break
end
end
if number_built <= 1 then
return nil
end
if not existing_settlement_name then
local waypoint_pos = vector.add(center_surface_pos, {x=0,y=2,z=0})
named_waypoints.add_waypoint("settlements", waypoint_pos, {name=name, settlement_type=settlement_def.name})
end
return settlement_info
end
local function initialize_nodes(settlement_info)
for i, built_house in ipairs(settlement_info) do
local pmin = built_house.build_pos_min
local pmax = built_house.build_pos_max
for yi = pmin.y, pmax.y do
for xi = pmin.x, pmax.x do
for zi = pmin.z, pmax.z do
local pos = {x=xi, y=yi, z=zi}
local node = minetest.get_node(pos)
local node_def = minetest.registered_nodes[node.name]
if node_def.on_construct then
-- if the node has an on_construct defined, call it.
node_def.on_construct(pos)
end
if built_house.schematic_info.initialize_node then
-- Hook for specialized initialization.
built_house.schematic_info.initialize_node(pos, node, node_def, settlement_info)
end
end
end
end
end
end
-- generate paths between buildings
local function paths(data, va, settlement_info)
local c_path_material = minetest.get_content_id(settlement_info.def.path_material or default_path_material)
local starting_point
local end_point
local distance
starting_point = settlement_info[1].center_pos
for i,built_house in ipairs(settlement_info) do
end_point = built_house.center_pos
if starting_point ~= end_point
then
-- loop until end_point is reached (distance == 0)
while true do
-- define surrounding pos to starting_point
local north_p = {x=starting_point.x+1, y=starting_point.y, z=starting_point.z}
local south_p = {x=starting_point.x-1, y=starting_point.y, z=starting_point.z}
local west_p = {x=starting_point.x, y=starting_point.y, z=starting_point.z+1}
local east_p = {x=starting_point.x, y=starting_point.y, z=starting_point.z-1}
-- measure distance to end_point
local dist_north_p_to_end = math.sqrt(
((north_p.x - end_point.x)*(north_p.x - end_point.x))+
((north_p.z - end_point.z)*(north_p.z - end_point.z))
)
local dist_south_p_to_end = math.sqrt(
((south_p.x - end_point.x)*(south_p.x - end_point.x))+
((south_p.z - end_point.z)*(south_p.z - end_point.z))
)
local dist_west_p_to_end = math.sqrt(
((west_p.x - end_point.x)*(west_p.x - end_point.x))+
((west_p.z - end_point.z)*(west_p.z - end_point.z))
)
local dist_east_p_to_end = math.sqrt(
((east_p.x - end_point.x)*(east_p.x - end_point.x))+
((east_p.z - end_point.z)*(east_p.z - end_point.z))
)
-- evaluate which pos is closer to the end_point
if dist_north_p_to_end <= dist_south_p_to_end and
dist_north_p_to_end <= dist_west_p_to_end and
dist_north_p_to_end <= dist_east_p_to_end
then
starting_point = north_p
distance = dist_north_p_to_end
elseif dist_south_p_to_end <= dist_north_p_to_end and
dist_south_p_to_end <= dist_west_p_to_end and
dist_south_p_to_end <= dist_east_p_to_end
then
starting_point = south_p
distance = dist_south_p_to_end
elseif dist_west_p_to_end <= dist_north_p_to_end and
dist_west_p_to_end <= dist_south_p_to_end and
dist_west_p_to_end <= dist_east_p_to_end
then
starting_point = west_p
distance = dist_west_p_to_end
elseif dist_east_p_to_end <= dist_north_p_to_end and
dist_east_p_to_end <= dist_south_p_to_end and
dist_east_p_to_end <= dist_west_p_to_end
then
starting_point = east_p
distance = dist_east_p_to_end
end
-- find surface of new starting point
local surface_point, surface_mat = find_surface(starting_point, data, va)
-- replace surface node with path material
if surface_point
then
local vi = va:index(surface_point.x, surface_point.y, surface_point.z)
data[vi] = c_path_material
-- don't set y coordinate, surface might be too low or high
starting_point.x = surface_point.x
starting_point.z = surface_point.z
end
if distance <= 1 or
starting_point == end_point
then
break
end
end
end
end
end
function settlements.place_building(vm, built_house, settlement_info)
local building_all_info = built_house.schematic_info
local pos = built_house.build_pos_min
pos.y = pos.y + (building_all_info.height_adjust or 0)
local rotation = built_house.rotation
-- get building node material for better integration to surrounding
local platform_material = built_house.surface_mat
local platform_material_name = minetest.get_name_from_content_id(platform_material)
local building_schematic = building_all_info.schematic
local replacements = {}
if settlement_info.replacements then
for target, repl in pairs(settlement_info.replacements) do
replacements[target] = repl
end
end
if building_all_info.replace_nodes_optional and settlement_info.replacements_optional then
for target, repl in pairs(settlement_info.replacements_optional) do
replacements[target] = repl
end
end
if settlement_info.def.replace_with_surface_material then
replacements[settlement_info.def.replace_with_surface_material] = platform_material_name
end
local force_place = building_all_info.force_place
if force_place == nil then
force_place = true
end
minetest.place_schematic_on_vmanip(
vm,
pos,
building_schematic,
rotation,
replacements,
force_place)
end
local trigger_timer_for_group = function(minp, maxp, nodenames)
if not nodenames then
return
end
local targets = minetest.find_nodes_in_area(minp, maxp, nodenames)
for _, pos in ipairs(targets) do
minetest.get_node_timer(pos):start(math.random(20, 120) / 10)
end
end
settlements.generate_settlement_vm = function(vm, va, minp, maxp, existing_settlement_name)
local data = {} -- normally this buffer would be outside the method to avoid
-- garbage collecting it between calls and overwhelming LUA's memory management.
-- But settlements should only need to generate on rare occasions so let's try letting
-- LUA garbage-collect it to free up the memory in between times.
vm:get_data(data)
local settlement_info = create_site_plan(minp, maxp, data, va, existing_settlement_name)
if not settlement_info
then
return false
end
-- prepare terrain
terraform(data, va, settlement_info)
--build paths between buildings
if settlement_info.def.path_material then
paths(data, va, settlement_info)
end
vm:set_data(data)
-- place schematics
for _, built_house in ipairs(settlement_info) do
settlements.place_building(vm, built_house, settlement_info)
end
vm:calc_lighting()
vm:update_liquids()
vm:write_to_map()
-- evaluate settlement_info and initialize furnaces and chests
initialize_nodes(settlement_info)
trigger_timer_for_group(minp, maxp, settlement_info.def.trigger_timers_for_nodes)
return true
end
-- try to build a settlement outside of map generation
settlements.generate_settlement = function(minp, maxp)
local vm = minetest.get_voxel_manip()
local emin, emax = vm:read_from_map(minp, maxp) -- add borders to simulate mapgen overgeneration
local va = VoxelArea:new{
MinEdge = emin,
MaxEdge = emax
}
return settlements.generate_settlement_vm(vm, va, minp, maxp)
end