From 14042b46889c8e891a0f88f41402e0fa0d5fa1f5 Mon Sep 17 00:00:00 2001 From: codiac Date: Sat, 25 May 2024 13:56:32 +1000 Subject: [PATCH] Add a grid based layout for villages --- .luacheckrc | 1 + mods/CORE/mcl_util/init.lua | 23 ++- mods/MAPGEN/mcl_villages/API.md | 23 +++ mods/MAPGEN/mcl_villages/buildings.lua | 264 ++++++++++++++++++++---- mods/MAPGEN/mcl_villages/const.lua | 2 +- mods/MAPGEN/mcl_villages/foundation.lua | 53 +++-- mods/MAPGEN/mcl_villages/init.lua | 5 +- mods/MAPGEN/mcl_villages/utils.lua | 19 +- 8 files changed, 312 insertions(+), 78 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 15a3ffa3e..7be81e28a 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -194,6 +194,7 @@ globals = { "mcl_lush_caves", "mcl_armor_trims", "mcl_villages", + "AreaStore", } read_globals = { diff --git a/mods/CORE/mcl_util/init.lua b/mods/CORE/mcl_util/init.lua index c7519c5c1..eb7653e92 100644 --- a/mods/CORE/mcl_util/init.lua +++ b/mods/CORE/mcl_util/init.lua @@ -1161,10 +1161,18 @@ function mcl_util.traverse_tower(pos, dir, callback) end -- Voxel manip function to replace a node type with another in an area -function mcl_util.replace_node_vm(pos1, pos2, mat_from, mat_to) - local c_from = minetest.get_content_id(mat_from) +function mcl_util.replace_node_vm(pos1, pos2, mat_from, mat_to, is_group) local c_to = minetest.get_content_id(mat_to) + local group_name + local c_from + + if is_group then + group_name = string.match(mat_from, "group:(.+)") + else + c_from = minetest.get_content_id(mat_from) + end + local vm = minetest.get_voxel_manip() local emin, emax = vm:read_from_map(pos1, pos2) local a = VoxelArea:new({ @@ -1178,8 +1186,15 @@ function mcl_util.replace_node_vm(pos1, pos2, mat_from, mat_to) for y = pos1.y, pos2.y do for x = pos1.x, pos2.x do local vi = a:index(x, y, z) - if data[vi] == c_from then - data[vi] = c_to + if is_group then + local node_name = minetest.get_name_from_content_id(data[vi]) + if minetest.get_item_group(node_name, group_name) > 0 then + data[vi] = c_to + end + else + if data[vi] == c_from then + data[vi] = c_to + end end end end diff --git a/mods/MAPGEN/mcl_villages/API.md b/mods/MAPGEN/mcl_villages/API.md index 2f086be90..c7ac545b8 100644 --- a/mods/MAPGEN/mcl_villages/API.md +++ b/mods/MAPGEN/mcl_villages/API.md @@ -276,3 +276,26 @@ If a crop cannot be found for a crop type in a biome, then a default will be used. This ensure all farming blocks are full, ven if it's al the same crop. The default is wheat. + +## Village Layout + +There are two methods for layout out villages, circle layout is more likely to be +used for small villages and grid for large villages. + +The circle layout uses circles (surprise) to calculate if buildings overlap. It +creates fairly widely spaced layouts. + +The grid layout uses a predetermined grid layout to positions buildings and uses +AreaStore to adjust building position if there are collisions. + +The predetermined grid is below, position 0 is the bell, the other numbers are the order of placement. + +|||||||| +| -- | -- | -- | -- | -- | -- | -- | +|48|41|33|25|29|37|45| +|40|17|13| 9|11|15|43| +|32|19| 5| 1| 3|22|35| +|28|23| 7| 0| 8|24|27| +|36|21| 4| 2| 6|20|31| +|44|16|12|10|14|18|39| +|46|38|30|26|34|42|47| diff --git a/mods/MAPGEN/mcl_villages/buildings.lua b/mods/MAPGEN/mcl_villages/buildings.lua index 1d9198d82..0d82c6de3 100644 --- a/mods/MAPGEN/mcl_villages/buildings.lua +++ b/mods/MAPGEN/mcl_villages/buildings.lua @@ -92,49 +92,187 @@ local function init_nodes(p1, p2, size, rotation, pr) end end -local function layout_town(minp, maxp, pr, input_settlement_info) - local settlement_info = {} - local xdist = math.abs(minp.x - maxp.x) - local zdist = math.abs(minp.z - maxp.z) +-- TODO consided using mod_storage to persist this... +local areas = AreaStore() +areas:set_cache_params({ enabled = true, block_radius = 16, limit = 100 }) - -- find center of village within interior of chunk - local center = vector.new( - minp.x + pr:next(math.floor(xdist * 0.2), math.floor(xdist * 0.8)), - maxp.y, - minp.z + pr:next(math.floor(zdist * 0.2), math.floor(zdist * 0.8)) - ) +local function record_building(record) + local placement_pos = vector.copy(record.pos) - -- find center_surface of village - local center_surface, surface_material = mcl_villages.find_surface(center, true) + -- Allow adjusting y axis + if record.yadjust then + placement_pos = vector.offset(placement_pos, 0, record.yadjust, 0) + end + + local size = math.ceil(math.max(record.size.x, record.size.z) / 2) + local minp = vector.offset(placement_pos, -size, -10, -size) + local maxp = vector.offset(placement_pos, size, record.size.y + 10, size) + + areas:insert_area(minp, maxp, record.name) +end + +-- TODO is there a good way to calculate this? +local squares = { + -- center + { x = 0, z = 0 }, + + --around center + { x = 1, z = 0 }, + { x = -1, z = 0 }, + { x = 1, z = 1 }, + { x = -1, z = -1 }, + { x = 1, z = -1 }, + { x = -1, z = -1 }, + { x = 0, z = 1 }, + { x = 0, z = -1 }, + + --one out + { x = 2, z = 0 }, + { x = -2, z = 0 }, + + { x = 2, z = 1 }, + { x = -2, z = -1 }, + { x = 2, z = -1 }, + { x = -2, z = 1 }, + + { x = 2, z = 2 }, + { x = -2, z = -2 }, + { x = 2, z = -2 }, + { x = -2, z = 2 }, + + { x = 0, z = 2 }, + { x = 0, z = -2 }, + { x = 1, z = 2 }, + { x = -1, z = -2 }, + { x = 1, z = -2 }, + { x = -1, z = 2 }, + + --two out + { x = 3, z = 0 }, --25 + { x = -3, z = 0 }, + + { x = 0, z = 3 }, + { x = 0, z = -3 }, + { x = 3, z = 1 }, + { x = -3, z = -1 }, + + { x = -1, z = 3 }, + { x = 1, z = -3 }, + { x = 3, z = -1 }, + { x = -3, z = 1 }, + + { x = 1, z = 3 }, + { x = -1, z = -3 }, + { x = 3, z = 2 }, + { x = -3, z = -2 }, + { x = 3, z = -2 }, + { x = -3, z = 2 }, --40 + + { x = 3, z = -2 }, + { x = -3, z = 2 }, + { x = 2, z = 3 }, + { x = -2, z = -3 }, + { x = 3, z = 3 }, + { x = -3, z = -3 }, + { x = -3, z = 3 }, + { x = 3, z = -3 }, +} + +local function layout_grid(pr, input_settlement_info, settlement_info, center) + + local bell_schem = settlement_info[1] + record_building(bell_schem) + local center_surface = bell_schem.pos + + for i = 2, #input_settlement_info do + if i > #squares then + minetest.log("warning", "Too many buldings for grid layout, remaining buildings will be missing.") + return settlement_info + end + + local cur_schem = input_settlement_info[i] + local placed = false + local iter = 0 + + local x_step = squares[i].x + local z_step = squares[i].z + + local b_length = math.max(bell_schem.size.x, bell_schem.size.z) + local c_length = math.max(cur_schem.size.x, cur_schem.size.z) + + local dist = math.ceil(b_length + c_length / 2) + local half_dist = math.ceil(dist / 2) + + local next_x = bell_schem.pos.x + ((dist + 1) * x_step) + local next_z = bell_schem.pos.z + ((dist + 1) * z_step) + + while not placed do + iter = iter + 1 + + local pos = vector.new(next_x, center_surface.y, next_z) + local pos_surface, surface_material = mcl_villages.find_surface(pos) + + if pos_surface then + pos_surface = vector.round(pos_surface) + + local minp = vector.offset(pos_surface, -half_dist, 0, -half_dist) + local maxp = vector.offset(pos_surface, half_dist, cur_schem.size.y, half_dist) + + local collisons = areas:get_areas_in_area(minp, maxp, true, true, true) + + if table.count(collisons) == 0 then + cur_schem.pos = vector.copy(pos_surface) + cur_schem.surface_mat = surface_material + record_building(cur_schem) + table.insert(settlement_info, cur_schem) + placed = true + else + local col + -- sometimes this is a table with keys, and sometimes not ... + for k, v in pairs(collisons) do + col = v + break + end + + if z_step < 0 then + next_z = col.max.z + half_dist + 1 + elseif z_step > 0 then + next_z = col.min.z - half_dist - 1 + elseif x_step < 0 then + next_x = col.min.x - half_dist - 1 + elseif x_step > 0 then + next_x = col.max.x + half_dist + 1 + end + end + else + next_x = next_x + (dist + 1 * x_step) + next_z = next_z + (dist + 1 * z_step) + end + + if not placed and (iter % 5 == 0) then + x_step = squares[i + (iter % 10)].x + z_step = squares[i + (iter % 10)].z + next_x = bell_schem.pos.x + ((dist + 1) * x_step) + next_z = bell_schem.pos.z + ((dist + 1) * z_step) + end + + if not placed and iter >= 30 then + minetest.log("warning", "Could not place " .. cur_schem.name) + break + end + end + end +end + +local function layout_circles(pr, input_settlement_info, settlement_info, center) + local center_surface = settlement_info[1].pos + local size = #input_settlement_info + local max_dist = 20 + (size * 3) -- Cache for chunk surfaces local chunks = {} chunks[mcl_vars.get_chunk_number(center)] = true - -- build settlement around center - if not center_surface then - minetest.log("info", string.format("[mcl_villages] Cannot build village at %s", minetest.pos_to_string(center))) - return false - else - minetest.log( - "info", - string.format( - "[mcl_villages] Will build a village at position %s with surface material %s", - minetest.pos_to_string(center_surface), - surface_material - ) - ) - end - - local bell_info = table.copy(input_settlement_info[1]) - bell_info["pos"] = vector.copy(center_surface) - bell_info["surface_mat"] = surface_material - - table.insert(settlement_info, bell_info) - - local size = #input_settlement_info - local max_dist = 20 + (size * 3) - for i = 2, size do local cur_schem = input_settlement_info[i] @@ -200,6 +338,52 @@ local function layout_town(minp, maxp, pr, input_settlement_info) end end end +end + +local function layout_town(minp, maxp, pr, input_settlement_info, grid) + local settlement_info = {} + local xdist = math.abs(minp.x - maxp.x) + local zdist = math.abs(minp.z - maxp.z) + + -- find center of village within interior of chunk + local center = vector.new( + minp.x + pr:next(math.floor(xdist * 0.2), math.floor(xdist * 0.8)), + maxp.y, + minp.z + pr:next(math.floor(zdist * 0.2), math.floor(zdist * 0.8)) + ) + + -- find center_surface of village + local center_surface, surface_material = mcl_villages.find_surface(center, true) + + -- build settlement around center + if not center_surface then + minetest.log( + "info", + string.format("[mcl_villages] Cannot build village at %s", minetest.pos_to_string(center)) + ) + return false + else + minetest.log( + "info", + string.format( + "[mcl_villages] Will build a village at position %s with surface material %s", + minetest.pos_to_string(center_surface), + surface_material + ) + ) + end + + local bell_info = table.copy(input_settlement_info[1]) + bell_info["pos"] = vector.copy(center_surface) + bell_info["surface_mat"] = surface_material + + table.insert(settlement_info, bell_info) + + if grid then + layout_grid(pr, input_settlement_info, settlement_info, center) + else + layout_circles(pr, input_settlement_info, settlement_info, center) + end return settlement_info end @@ -305,7 +489,13 @@ function mcl_villages.create_site_plan_new(minp, maxp, pr) table.insert(shuffled_settlement_info, 1, bell_info) - return layout_town(minp, maxp, pr, shuffled_settlement_info) + -- More jobs increases chances of grid layout + local grid = false + if pr:next(1, count_buildings.num_jobs) > 4 then + grid = true + end + + return layout_town(minp, maxp, pr, shuffled_settlement_info, grid), grid end function mcl_villages.place_schematics_new(settlement_info, pr, blockseed) diff --git a/mods/MAPGEN/mcl_villages/const.lua b/mods/MAPGEN/mcl_villages/const.lua index 17b1691a9..483ec9640 100644 --- a/mods/MAPGEN/mcl_villages/const.lua +++ b/mods/MAPGEN/mcl_villages/const.lua @@ -17,7 +17,7 @@ function mcl_villages.grundstellungen() "mcl_core:sand", "mcl_core:redsand", --"mcl_core:silver_sand", - "mcl_core:snow" + --"mcl_core:snow" } -- allow villages on more surfaces diff --git a/mods/MAPGEN/mcl_villages/foundation.lua b/mods/MAPGEN/mcl_villages/foundation.lua index 6f5be9b81..fe053eb47 100644 --- a/mods/MAPGEN/mcl_villages/foundation.lua +++ b/mods/MAPGEN/mcl_villages/foundation.lua @@ -1,10 +1,7 @@ -local circles = minetest.settings:get_bool("mcl_villages_circles", true) -local terrace = minetest.settings:get_bool("mcl_villages_terrace", true) -local padding = tonumber(minetest.settings:get("mcl_villages_padding")) or 2 -local top_padding = tonumber(minetest.settings:get("mcl_villages_top_padding")) or 8 -local terrace_max_ext = tonumber(minetest.settings:get("mcl_villages_terrace_max_ext")) or 6 - +local padding = 2 +local top_padding = 20 +local terrace_max_ext = 6 ------------------------------------------------------------------------------- -- function to fill empty space below baseplate when building on a hill @@ -26,16 +23,16 @@ function mcl_villages.ground(pos, pr) -- role model: Wendelsteinkircherl, Branne end -- Empty space above ground -local function overground(pos, fwidth, fdepth, fheight) +local function overground(pos, fwidth, fdepth, fheight, grid) - if circles then + if not grid then local y = math.ceil(pos.y + 1) local radius_base = math.max(fwidth, fdepth) local radius = math.round((radius_base / 2) + padding) local dome = fheight + top_padding for count2 = 1, fheight + top_padding do - if terrace and radius_base > 3 then + if radius_base > 3 then if count2 > dome then radius = radius - 1 elseif count2 <= terrace_max_ext then @@ -49,32 +46,30 @@ local function overground(pos, fwidth, fdepth, fheight) end else local count = 1 - if not terrace then - count = count + 2 - end - if terrace then - for y_adj = 1, pos.y + fheight + top_padding do - local pos1 = vector.offset(pos, -count, y_adj, -count) - local pos2 = vector.offset(pos, fwidth + count, y_adj, fdepth + count) - mcl_util.bulk_set_node_vm(pos1, pos2, "air") + -- Must treat as square because rotation is random + local adjust = math.round(math.max(fwidth, fdepth) / 2) - if terrace and count <= terrace_max_ext then - count = count + 1 - end - end - else - local x_adjust = fwidth / 2 - local z_adjust = fdepth / 2 - - local pos1 = vector.offset(pos, -x_adjust, 0, -z_adjust) - local pos2 = vector.offset(pos, x_adjust, fheight, z_adjust) + for y_adj = 1, fheight + top_padding do + local pos1 = vector.offset(pos, -(adjust + count), y_adj, -(adjust + count)) + local pos2 = vector.offset(pos, adjust + count, y_adj, adjust + count) mcl_util.bulk_set_node_vm(pos1, pos2, "air") + + -- Grid layout requires minimal terracing + if count <= padding then + count = count + 1 + end end + + -- clean out stumps and leaves + local pos1 = vector.offset(pos, -(adjust + count), 1, -(adjust + count)) + local pos2 = vector.offset(pos, adjust + count, fheight + top_padding, adjust + count) + mcl_util.replace_node_vm(pos1, pos2, "group:tree", "air", true) + mcl_util.replace_node_vm(pos1, pos2, "group:leaves", "air", true) end end -function mcl_villages.terraform_new(settlement_info) +function mcl_villages.terraform_new(settlement_info, grid) local fheight, fwidth, fdepth -- Do ground first so that we can clear overhang for lower buildings @@ -96,7 +91,7 @@ function mcl_villages.terraform_new(settlement_info) fheight = schematic_data["size"]["y"] if not schematic_data["no_clearance"] then - overground(pos, fwidth, fdepth, fheight) + overground(pos, fwidth, fdepth, fheight, grid) end end end diff --git a/mods/MAPGEN/mcl_villages/init.lua b/mods/MAPGEN/mcl_villages/init.lua index 6d209b9ad..8bdeb54c8 100644 --- a/mods/MAPGEN/mcl_villages/init.lua +++ b/mods/MAPGEN/mcl_villages/init.lua @@ -31,13 +31,14 @@ local function build_a_settlement(minp, maxp, blockseed) local pr = PseudoRandom(blockseed) - local settlement_info = mcl_villages.create_site_plan_new(minp, maxp, pr) + local settlement_info, grid = mcl_villages.create_site_plan_new(minp, maxp, pr) if not settlement_info then + minetest.log("No village for " .. blockseed) return end - mcl_villages.terraform_new(settlement_info, pr) + mcl_villages.terraform_new(settlement_info, grid) mcl_villages.place_schematics_new(settlement_info, pr, blockseed) -- TODO when run here minetest.find_path regularly fails :( diff --git a/mods/MAPGEN/mcl_villages/utils.lua b/mods/MAPGEN/mcl_villages/utils.lua index c7d79dea6..fb7dc0ba0 100644 --- a/mods/MAPGEN/mcl_villages/utils.lua +++ b/mods/MAPGEN/mcl_villages/utils.lua @@ -1,5 +1,5 @@ -local terrace_max_ext = tonumber(minetest.settings:get("mcl_villages_terrace_max_ext")) or 6 +local terrace_max_ext = 6 ------------------------------------------------------------------------------- -- function to copy tables @@ -26,7 +26,7 @@ end function mcl_villages.find_surface(pos, wait, quick) local p6 = vector.new(pos) local cnt = 0 - local itter = 1 -- count up or down + local itter = 1 -- look up local cnt_max = 200 local wait_time = 10000000 @@ -42,9 +42,18 @@ function mcl_villages.find_surface(pos, wait, quick) else surface_node = mcl_vars.get_node(p6) end - if surface_node.name=="air" or surface_node.name=="ignore" then - itter = -1 + + if + surface_node.name == "air" + or surface_node.name == "ignore" + or surface_node.name == "mcl_core:snow" + or (minetest.get_item_group(surface_node.name, "deco_block") > 0) + or (minetest.get_item_group(surface_node.name, "plant") > 0) + or (minetest.get_item_group(surface_node.name, "tree") > 0) + then + itter = -1 -- look down end + -- go through nodes an find surface while cnt < cnt_max do -- Check Surface_node and Node above @@ -63,7 +72,7 @@ function mcl_villages.find_surface(pos, wait, quick) mcl_villages.debug("find_surface7: " ..surface_node.name.. " " .. surface_node_plus_1.name) return p6, surface_node.name else - mcl_villages.debug("find_surface2: wrong surface+1") + mcl_villages.debug("find_surface2: wrong layer above " .. surface_node_plus_1.name) end else mcl_villages.debug("find_surface3: wrong surface "..surface_node.name.." at pos "..minetest.pos_to_string(p6))