mapgen_helper/place_schematic.lua

320 lines
12 KiB
Lua

-- These functions are a modification of the schematic placement code from src/mapgen/mg_schematic.cpp.
-- As such, this file is separately licened under the LGPL as follows:
-- License of Minetest source code
-------------------------------
--Minetest
--Copyright (C) 2010-2018 celeron55, Perttu Ahola <celeron55@gmail.com>
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--(at your option) any later version.
--This program is distributed in the hope that it will be useful,
--but WITHOUT ANY WARRANTY; without even the implied warranty of
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--GNU Lesser General Public License for more details.
--You should have received a copy of the GNU Lesser General Public License along
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
local c_air = minetest.get_content_id("air")
local c_ignore = minetest.get_content_id("ignore")
-- Table value = rotated facedir
-- Columns: 90, 180, 270 degrees rotation around vertical axis
-- Rotation is anticlockwise as seen from above (+Y)
local rotate_facedir_y =
{
[0] = {1, 2, 3} ,
[1] = {2, 3, 0} ,
[2] = {3, 0, 1} ,
[3] = {0, 1, 2} ,
[4] = {13, 10, 19} ,
[5] = {14, 11, 16} ,
[6] = {15, 8, 17} ,
[7] = {12, 9, 18} ,
[8] = {17, 6, 15} ,
[9] = {18, 7, 12} ,
[10] = {19, 4, 13} ,
[11] = {16, 5, 14} ,
[12] = {9, 18, 7} ,
[13] = {10, 19, 4} ,
[14] = {11, 16, 5} ,
[15] = {8, 17, 6} ,
[16] = {5, 14, 11} ,
[17] = {6, 15, 8} ,
[18] = {7, 12, 9} ,
[19] = {4, 13, 10} ,
[20] = {23, 22, 21} ,
[21] = {20, 23, 22} ,
[22] = {21, 20, 23} ,
[23] = {22, 21, 20} ,
}
local random_rotations = {0, 90, 180, 270}
local rotate_param2 = function(param2, paramtype2, rotation)
param2 = param2 or 0
if paramtype2 == "facedir" then
if rotation == 90 then
param2 = rotate_facedir_y[param2][1]
elseif rotation == 180 then
param2 = rotate_facedir_y[param2][2]
elseif rotation == 270 then
param2 = rotate_facedir_y[param2][3]
end
elseif paramtype2 == "wallmounted" then
--TODO
elseif paramtype2 == "colorfacedir" then
--TODO
elseif paramtype2 == "colorwallmounted" then
--TODO
end
return param2
end
local swap = function(size_x, size_z)
return size_z, size_x
end
-- Returns the minpos and maxpos of the bounding box that this schematic will be placed in given
-- the rotation and flag parameters. Useful for testing whether a schematic will fit in a place before actually
-- writing it to the data, so that you can abort and try something else instead.
mapgen_helper.get_schematic_bounding_box = function(pos, schematic, rotation, flags)
flags = flags or {}
local size = schematic.size
local size_x = size.x
local size_y = size.y
local size_z = size.z
local center_pos = schematic.center_pos
if center_pos and rotation ~= nil and rotation ~= 0 then
center_pos = vector.new(center_pos) -- make a copy so we can mess with it without damaging the schematic
if rotation == 90 then
center_pos.x, center_pos.z = swap(size_x - center_pos.x - 1, center_pos.z)
elseif rotation == 180 then
center_pos.z = size_z - center_pos.z - 1
center_pos.x = size_x - center_pos.x - 1
elseif rotation == 270 then
center_pos.x, center_pos.z = swap(center_pos.x, size_z - center_pos.z - 1)
end
end
if rotation == 90 or rotation == 270 then
size_x, size_z = swap(size_x, size_z)
end
local minpos = vector.new(pos)
if center_pos then
if not flags.place_center_x then
minpos.x = minpos.x - center_pos.x
end
if not flags.place_center_y then
minpos.y = minpos.y - center_pos.y
end
if not flags.place_center_z then
minpos.z = minpos.z - center_pos.z
end
end
if flags.place_center_x then
minpos.x = math.floor(minpos.x - (size_x - 1) / 2)
end
if flags.place_center_y then
minpos.y = math.floor(minpos.y - (size_y - 1) / 2)
end
if flags.place_center_z then
minpos.z = math.floor(minpos.z - (size_z - 1) / 2)
end
local maxpos = vector.add(minpos, {x=size_x-1, y=size_y-1, z=size_z-1})
return minpos, maxpos
end
-- Takes a lua-format schematic and applies it to the data and param2_data arrays produced by vmanip instead of being applied to the vmanip directly. Useful in a mapgen loop that's doing other things with the data before and after applying schematics. A VoxelArea for the data also needs to be provided.
-- Schematic enhancements beyond the basic Minetest API that work with this:
-- * node defs can have a "place_on_condition" property defined, which is a function that takes a node content ID parameter and returns true to indicate the schematic should replace it or false to indicate it should not. Useful for, for example, a schematic that should replace water but not stone (placing decorations on the bottom of the ocean), or a schematic that replaces all buildable_to nodes (to prevent tufts of grass from knocking holes in foundations). "data, area, vi" parameters are also provided if you want to get fancy and base the condition on surrounding nodes, but bear in mind that the schematic is in the process of being written already so some neighbors will already have been replaced with schematic nodes and some neighbors will be replaced in the future.
-- * schematic can have a "center_pos" position defined relative to the placement pos that will be treated as the rotation and placement origin, unless overridden by the flags parameter
-- returns true if the schematic was entirely contained within the voxelarea, false otherwise.
local empty_table = {}
mapgen_helper.place_schematic_on_data = function(data, data_param2, area, pos, schematic, rotation, replacements, force_placement, flags)
pos = vector.new(pos)
replacements = replacements or empty_table
flags = flags or empty_table -- TODO: support all flags formats
if flags.force_placement ~= nil then
force_placement = flags.force_placement -- TODO: unclear which force_placement parameter should have prededence here
end
local center_pos = schematic.center_pos
if rotation == "random" then rotation = random_rotations[math.random(1,4)] end
local schemdata = schematic.data
local slice_probs = schematic.yslice_prob or {}
local size = schematic.size
local size_x = size.x
local size_y = size.y
local size_z = size.z
local xstride = 1
local ystride = size_x
local zstride = size_x * size_y
if center_pos and rotation ~= nil and rotation ~= 0 then
center_pos = vector.new(center_pos) -- make a copy so we can mess with it without damaging the schematic
if rotation == 90 then
center_pos.x, center_pos.z = swap(size_x - center_pos.x - 1, center_pos.z)
elseif rotation == 180 then
center_pos.z = size_z - center_pos.z - 1
center_pos.x = size_x - center_pos.x - 1
elseif rotation == 270 then
center_pos.x, center_pos.z = swap(center_pos.x, size_z - center_pos.z - 1)
end
end
local i_start, i_step_x, i_step_z
if rotation == 90 then
i_start = size_x
i_step_x = zstride
i_step_z = -xstride
size_x, size_z = swap(size_x, size_z)
elseif rotation == 180 then
i_start = zstride * (size_z - 1) + size_x
i_step_x = -xstride
i_step_z = -zstride
elseif rotation == 270 then
i_start = zstride * (size_z - 1) + 1
i_step_x = -zstride
i_step_z = xstride
size_x, size_z = swap(size_x, size_z)
else
i_start = 1
i_step_x = xstride
i_step_z = zstride
end
-- Adjust placement position if necessary
if center_pos then
if not flags.place_center_x then
pos.x = pos.x - center_pos.x
end
if not flags.place_center_y then
pos.y = pos.y - center_pos.y
end
if not flags.place_center_z then
pos.z = pos.z - center_pos.z
end
end
if flags.place_center_x then
pos.x = math.floor(pos.x - (size_x - 1) / 2)
end
if flags.place_center_y then
pos.y = math.floor(pos.y - (size_y - 1) / 2)
end
if flags.place_center_z then
pos.z = math.floor(pos.z - (size_z - 1) / 2)
end
local maxpos = vector.add(pos, {x=size_x-1, y=size_y-1, z=size_z-1})
local minEdge = area.MinEdge
local maxEdge = area.MaxEdge
if not (pos.x <= maxEdge.x and maxpos.x >= minEdge.x and
pos.z <= maxEdge.z and maxpos.z >= minEdge.z and
pos.y <= maxEdge.y and maxpos.y >= minEdge.y) then
return false -- the bounding boxes of the area and the schematic don't overlap at all
end
local contained_in_area = true
local on_place_callbacks = {}
local y_map = pos.y
for y = 0, size_y-1 do
if slice_probs[y] == nil or slice_probs[y] == 255 or slice_probs[y] <= math.random(1, 255) then
for z = 0, size_z-1 do
local i = z * i_step_z + y * ystride + i_start
for x = 0, size_x-1 do
local vi = area:index(pos.x + x, y_map, pos.z + z)
if area:containsi(vi) then
local node_def = schemdata[i]
local node_name = replacements[node_def.name] or node_def.name
if node_name ~= "ignore" then
local placement_prob = node_def.prob or 255
if placement_prob ~= 0 then
local force_place_node = node_def.force_place
local place_on_condition = node_def.place_on_condition
local on_place = node_def.on_place
local old_node_id = data[vi]
if (force_placement or force_place_node
or (place_on_condition and place_on_condition(old_node_id, data, area, vi))
or (not place_on_condition and (old_node_id == c_air or old_node_id == c_ignore)))
and (placement_prob == 255 or math.random(1,255) <= placement_prob)
then
local registered_def = minetest.registered_nodes[node_name]
if registered_def ~= nil then
local paramtype2 = registered_def.paramtype2
data[vi] = minetest.get_content_id(node_name)
data_param2[vi] = rotate_param2(node_def.param2, paramtype2, rotation)
if on_place then
table.insert(on_place_callbacks, {on_place, vi})
end
else
minetest.log("error", "mapgen_helper.place_schematic was given a schematic with unregistered node " .. tostring(node_name) .. " in it.")
end
end
end
end
else
contained_in_area = false -- schematic spilled over the edge of the area
end
i = i + i_step_x
end
end
end
y_map = y_map + 1
end
for k, callback in pairs(on_place_callbacks) do
callback[1](callback[2], data, data_param2, area, pos, schematic, rotation, replacements, force_placement, flags)
end
return contained_in_area
end
-- aborts schematic placement if it won't fit into the provided data
mapgen_helper.place_schematic_on_data_if_it_fits = function(data, data_param2, area, pos, schematic, rotation, replacements, force_placement, flags)
local minbound, maxbound = mapgen_helper.get_schematic_bounding_box(pos, schematic, rotation, flags)
if mapgen_helper.is_box_within_box(minbound, maxbound, area.MinEdge, area.MaxEdge) then
return mapgen_helper.place_schematic_on_data(data, data_param2, area, pos, schematic, rotation, replacements, force_placement, flags)
end
return false
end
-- wraps the above for convenience, so you can use this style of schematic in non-mapgen contexts as well
mapgen_helper.place_schematic = function(pos, schematic, rotation, replacements, force_placement, flags)
local minpos, maxpos = mapgen_helper.get_schematic_bounding_box(pos, schematic, rotation, flags)
local vmanip = minetest.get_voxel_manip(minpos, maxpos)
local data = vmanip:get_data()
local data_param2 = vmanip:get_param2_data()
local emin, emax = vmanip:get_emerged_area()
local area = VoxelArea:new{MinEdge=emin, MaxEdge=emax}
local ret = mapgen_helper.place_schematic_on_data(data, data_param2, area, pos, schematic, rotation, replacements, force_placement, flags)
vmanip:set_data(data)
vmanip:set_param2_data(data_param2)
vmanip:write_to_map()
return ret -- should always be true since we created the voxelarea to fit the schematic
end