naturalslopeslib/update_shape.lua

578 lines
22 KiB
Lua

--[[
Describes the falling/eroding effect for slopes
--]]
--[[
Pick replacement, node and area
--]]
-- Manage color for param2
-- @param replacement the replacement table
-- @param source the name or id of the node being transformed
-- @param dest the name or id of the new shape
-- @param param2_source the param2 value before transformation
-- @param param2_dest the param2 value for facedir (if any) after transformation
-- @return a new param2 value for dest node with color if necessary
local function manage_param2_color(replacement, source, dest, param2_source, param2_dest)
if not replacement._colored_source then
return param2_dest
end
if dest == replacement.source then
-- param2_source will hold a 'color' value
if source == replacement.source then
-- from 'color' to 'color'
return param2_source
else
-- from 'color' to 'colorfacedir'
local new_color_index = replacement._color_convert(param2_source, true) % 8
return param2_dest + (new_color_index * 32)
end
else
-- param2_source will hold a 'colorfacedir' value
if dest == replacement.source then
-- from 'colorfacedir' to 'color'
local old_color = math.floor(param2_source / 32)
return replacement._color_convert(old_color, false) % 256
else
-- from 'colorfacedir' to an other 'colorfacedir'
local color = math.floor(param2_source / 32)
return param2_dest + (color * 32)
end
end
end
--- {Private} Pick a replacement node.
-- @param type The replacement shape. Either 'block', 'straight', 'ic' or 'oc'
-- @param name The name (or id for area) of the node to replace.
-- @param old_param2 The current value of param2 for the node to replace
-- @param param2 Facedir value to orient the new node.
-- @param for_area True when picking for an area, changes the parameter types
-- @return node {name=new_name, param2=new_param2} or area data {id=new_id, param2_data=new_param2}
-- or nil if dest node is not found.
local function pick_replacement(slope_type, name, old_param2, param2, for_area)
local replacement
if for_area then
replacement = naturalslopeslib.get_replacement_id(name)
else
replacement = naturalslopeslib.get_replacement(name)
end
if not replacement then return nil end
local dest_node_name = nil
if slope_type == 'block' and replacement.source then
dest_node_name = replacement.source
elseif slope_type == 'pike' and replacement.pike then
dest_node_name = replacement.pike
elseif slope_type == 'straight' and replacement.straight then
dest_node_name = replacement.straight
elseif slope_type == 'ic' and replacement.inner then
dest_node_name = replacement.inner
elseif slope_type == 'oc' and replacement.outer then
dest_node_name = replacement.outer
end
if dest_node_name then
if param2 == nil then param2 = 0 end
local color_param2 = manage_param2_color(replacement, name, dest_node_name, old_param2, param2)
if for_area then
return {id = dest_node_name, param2_data = color_param2}
else
return {name = dest_node_name, param2 = color_param2}
end
end
return nil
end
--[[
Surrounding checks and get replacement
--]]
--- Check if a node is considered empty to switch shape.
-- @param pos The position to check
function naturalslopeslib.is_free_for_shape_update(pos)
if not pos then return nil end
local node = minetest.get_node_or_nil(pos)
if node == nil then
return nil
end
return node.name == 'air'
end
local air_id = minetest.get_content_id('air')
function naturalslopeslib.area_is_free_for_shape_update(area, data, index)
if not area:containsi(index) then
return nil
end
return data[index] == air_id
end
-- Deprecated name
naturalslopeslib.area_is_free_for_erosion = naturalslopeslib.area_is_free_for_shape_update
--- Get the replacement node according to it's surroundings.
-- @param pos The position of the node or index with VoxelArea.
-- @param node The node at that position or content id with VoxelArea.
-- @param area The VoxelArea, nil for single position update.
-- @param data Data from VoxelManip, nil for single position update.
-- @param param2_data Param2 data from VoxelManip, nil for single position update.
-- @return A node to use with minetest.set_node
-- or a table with id and param2_data if called with an area.
-- Nil if no replacement is found or a neighbour cannot be read.
function naturalslopeslib.get_replacement_node(pos, node, area, data, param2_data)
-- Set functions and data according to update mode: single or VoxelManip
local is_free = nil
local new_pos = nil
local replacement = nil
local node_name = nil -- Either name or id
local for_area = false
local old_param2 = 0
if area then
for_area = true
is_free = function (at_index) -- always use with new_pos
return naturalslopeslib.area_is_free_for_shape_update(area, data, at_index)
end
new_pos = function(add) -- Get new index from current with add position
local area_pos = area:position(pos)
return area:indexp(vector.add(area_pos, add))
end
node_name = node
old_param2 = param2_data[pos]
else
is_free = naturalslopeslib.is_free_for_shape_update
new_pos = function(add) return vector.add(pos, add) end
node_name = node.name
old_param2 = node.param2
end
local is_ground -- ground or ceiling node
local pointing_y = -1
-- If there's something above and below, get back to full block
local above_free = is_free(new_pos({x=0, y=1, z=0}))
local below_free = is_free(new_pos({x=0, y=-1, z=0}))
if above_free == nil or below_free == nil then
return nil
end
if above_free and not below_free then
is_ground = true
pointing_y = 1
elseif below_free and not above_free then
is_ground = false
pointing_y = 5
else -- nothing below and above
return pick_replacement("block", node_name, old_param2, 0, for_area)
end
-- Check blocks around
local airXP = is_free(new_pos({x=1, y=0, z=0}))
if airXP == nil then return nil end
local airXM = is_free(new_pos({x=-1, y=0, z=0}))
if airXM == nil then return nil end
local airZP = is_free(new_pos({x=0, y=0, z=1}))
if airZP == nil then return nil end
local airZM = is_free(new_pos({x=0, y=0, z=-1}))
if airZM == nil then return nil end
local free_neighbors = 0
for index, free in next, {airXP, airXM, airZP, airZM} do
if free then free_neighbors = free_neighbors + 1 end
end
-- For four or three free neighbors, pike (slab)
if free_neighbors == 4 or free_neighbors == 3 then
local param2 = 0
if is_ground == false then param2 = 20 end
return pick_replacement("pike", node_name, old_param2, param2, for_area)
-- For two free neighbors
elseif free_neighbors == 2 then
-- at opposite sides, block
local param2
if (airXP and airXM) or (airZP and airZM) then
return pick_replacement('block', node_name, old_param2, 0, for_area)
-- side by side, outer corner
elseif (airXP and airZP) then
if is_ground then param2 = 3 else param2 = 22 end
return pick_replacement("oc", node_name, old_param2, param2, for_area)
elseif (airXP and airZM) then
if is_ground then param2 = 0 else param2 = 21 end
return pick_replacement("oc", node_name, old_param2, param2, for_area)
elseif (airXM and airZP) then
if is_ground then param2 = 2 else param2 = 23 end
return pick_replacement("oc", node_name, old_param2, param2, for_area)
elseif (airXM and airZM) then
if is_ground then param2 = 1 else param2 = 20 end
return pick_replacement("oc", node_name, old_param2, param2, for_area)
end
-- For one free neighbor, straight slope
elseif free_neighbors == 1 then
local param2 = 0
if airXP then if is_ground then param2 = 3 else param2 = 15 end
elseif airXM then if is_ground then param2 = 1 else param2 = 17 end
elseif airZP then if is_ground then param2 = 2 else param2 = 6 end
elseif airZM then if is_ground then param2 = 0 else param2 = 8 end
end
return pick_replacement("straight", node_name, old_param2, param2, for_area)
-- For no free neighbor check for a free diagonal for an inner corner
-- or fully surrounded for a rebuild
else
local airXPZP = is_free(new_pos({x=1, y=0, z=1}))
local airXPZM = is_free(new_pos({x=1, y=0, z=-1}))
local airXMZP = is_free(new_pos({x=-1, y=0, z=1}))
local airXMZM = is_free(new_pos({x=-1, y=0, z=-1}))
local param2
if airXPZP and not airXPZM and not airXMZP and not airXMZM then
if is_ground then param2 = 3 else param2 = 15 end
return pick_replacement("ic", node_name, old_param2, param2, for_area)
elseif not airXPZP and airXPZM and not airXMZP and not airXMZM then
if is_ground then param2 = 0 else param2 = 8 end
return pick_replacement("ic", node_name, old_param2, param2, for_area)
elseif not airXPZP and not airXPZM and airXMZP and not airXMZM then
if is_ground then param2 = 2 else param2 = 23 end
return pick_replacement("ic", node_name, old_param2, param2, for_area)
elseif not airXPZP and not airXPZM and not airXMZP and airXMZM then
if is_ground then param2 = 1 else param2 = 17 end
return pick_replacement("ic", node_name, old_param2, param2, for_area)
else
return pick_replacement('block', node_name, old_param2, 0, for_area)
end
end
end
--[[
Do the replacement
--]]
-- Do shape update when random roll passes on a single node.
function naturalslopeslib.chance_update_shape(pos, node, factor, type)
if factor == nil then factor = 1 end
local replacement = naturalslopeslib.get_replacement(node.name)
if not replacement then return false end
local chance_factor = 1
if type == "mapgen" or type == "stomp" or type == "place" or type == "time" then
chance_factor = replacement.chance_factors[type]
end
if (math.random() * (replacement.chance * factor * chance_factor)) < 1.0 then
return naturalslopeslib.update_shape(pos, node)
end
return false
end
--- Try to update the shape of a node according to it's surroundings.
-- @param pos The position of the node.
-- @param node The node at that position.
-- @return True if the node was updated, false otherwise.
function naturalslopeslib.update_shape(pos, node)
local replacement = naturalslopeslib.get_replacement_node(pos, node)
if replacement and (replacement.name ~= node.name or node.param2 ~= replacement.param2) then
minetest.set_node(pos, replacement)
return true
else
return false
end
end
local function get_edges(minp, maxp)
-- corner000 = minp
local corner001 = {x = minp.x, y = minp.y, z = maxp.z}
local corner010 = {x = minp.x, y = maxp.y, z = minp.z}
local corner011 = {x = minp.x, y = maxp.y, z = maxp.z}
local corner100 = {x = maxp.x, y = minp.y, z = minp.z}
local corner101 = {x = maxp.x, y = minp.y, z = maxp.z}
local corner110 = {x = maxp.x, y = maxp.y, z = minp.z}
-- corner111 = maxp
return { -- min pos, max pos, normal[x, y ,z]
-- The 8 corners
{minp, minp, {-1, -1, -1}},
{corner001, corner001, {-1, -1, 1}},
{corner010, corner010, {-1, 1, -1}},
{corner011, corner011, {-1, 1, 1}},
{corner100, corner100, { 1, -1, -1}},
{corner101, corner101, { 1, -1, 1}},
{corner110, corner110, { 1, 1, -1}},
{maxp, maxp, { 1, 1, 1}},
-- The 8 segments
{{x = minp.x + 1, y = minp.y, z = minp.z}, {x = maxp.x - 1, y = minp.y, z = minp.z}, { 0, -1, -1}},
{{x = minp.x + 1, y = maxp.y, z = minp.z}, {x = maxp.x - 1, y = maxp.y, z = minp.z}, { 0, 1, -1}},
{{x = minp.x, y = minp.y + 1, z = minp.z}, {x = minp.x, y = maxp.y - 1, z = minp.z}, {-1, 0, -1}},
{{x = maxp.x, y = minp.y + 1, z = minp.z}, {x = maxp.x, y = maxp.y - 1, z = minp.z}, { 1, 0, -1}},
{{x = minp.x + 1, y = minp.y, z = maxp.z}, {x = maxp.x - 1, y = minp.y, z = maxp.z}, { 0, -1, 1}},
{{x = minp.x + 1, y = maxp.y, z = maxp.z}, {x = maxp.x - 1, y = maxp.y, z = maxp.z}, { 0, 1, 1}},
{{x = minp.x, y = minp.y + 1, z = maxp.z}, {x = minp.x, y = maxp.y - 1, z = maxp.z}, { -1, 0, 1}},
{{x = maxp.x, y = minp.y + 1, z = maxp.z}, {x = maxp.x, y = maxp.y - 1, z = maxp.z}, { 1, 0, 1}},
-- The 6 faces
{{x = minp.x + 1, y = minp.y, z = minp.z + 1}, {x = maxp.x - 1, y = minp.y, z = maxp.z - 1}, { 0, -1, 0}},
{{x = minp.x + 1, y = maxp.y, z = minp.z + 1}, {x = maxp.x - 1, y = maxp.y, z = maxp.z - 1}, { 0, 1, 0}},
{{x = minp.x, y = minp.y + 1, z = minp.z + 1}, {x = minp.x, y = maxp.y - 1, z = maxp.z - 1}, { -1, 0, 0}},
{{x = maxp.x, y = minp.y + 1, z = minp.z + 1}, {x = maxp.x, y = maxp.y - 1, z = maxp.z - 1}, { 1, 0, 0}},
{{x = minp.x + 1, y = minp.y + 1, z = minp.z}, {x = maxp.x - 1, y = maxp.y - 1, z = minp.z}, { 0, 0, -1}},
{{x = minp.x + 1, y = minp.y + 1, z = maxp.z}, {x = maxp.x - 1, y = maxp.y - 1, z = maxp.z}, { 0, 0, 1}}
}
end
--- Massive shape update with VoxelManip.
-- @param minp Lower boundary of area.
-- @param mapx Higher boundary of area.
-- @param factor Factor for chance (0.1 means 10 times more likely to update)
-- @param skip (optional) Don't parse all nodes, skip randomly skip/2 to skip nodes
-- @param progressive_edges (optional) When true, edges are generated progressively (default)
-- @param type (optional) Transformation type for chance factor.
-- at every loop.
function naturalslopeslib.area_chance_update_shape(minp, maxp, factor, skip, progressive_edges, type)
if not skip then skip = 0 end
if progressive_edges == nil then progressive_edges = true end
-- Run on every block
local vm, emin, emax = minetest.get_voxel_manip()
local e1, e2 = vm:read_from_map(minp, maxp)
local area = VoxelArea:new{MinEdge = e1, MaxEdge = e2}
local data = vm:get_data()
local param2_data = vm:get_param2_data()
local i = area:indexp(e1)
local imax = area:indexp(e2)
if progressive_edges then
local edges = get_edges(minp, maxp)
for _, edge in ipairs(edges) do
naturalslopeslib.register_progressive_area_update(edge[1], edge[2], factor, skip, type, {x = edge[3][1], y = edge[3][2], z = edge[3][3]})
end
end
while i <= imax do
local x = (i-1) % area.ystride
local y = (i-1) % area.zstride
if x == 0 or x == area.ystride - 1
or y == 0 or y == area.zstride - 1 then
-- Skip edges
else
local replacement = naturalslopeslib.get_replacement_id(data[i])
if replacement ~= nil then
local chance_factor = 1
if type == "mapgen" or type == "stomp" or type == "place" or type == "time" then
chance_factor = replacement.chance_factors[type]
end
if math.random() * (replacement.chance * factor * chance_factor) < 1.0 then
local new_data = naturalslopeslib.get_replacement_node(i, data[i], area, data, param2_data)
if new_data then
data[i] = new_data.id
if new_data.param2_data then
param2_data[i] = new_data.param2_data
end
end
end
end
end
i = i + 1 + math.random(skip / 2, skip)
end
vm:set_data(data)
vm:set_param2_data(param2_data)
vm:write_to_map()
end
naturalslopeslib.progressive_area_updates = {}
function naturalslopeslib.register_progressive_area_update(minp, maxp, factor, skip, type, edge_normal)
if edge_normal ~= nil or minp.x == maxp.x or minp.y == maxp.y or minp.z == maxp.z then
-- Explicit edge or ignored
table.insert(naturalslopeslib.progressive_area_updates, {minp = minp, maxp = maxp,
factor = factor, skip = skip, i = 1, edge_normal = edge_normal})
return
end
-- else register the inner cube and all edges
-- The inner cube
table.insert(naturalslopeslib.progressive_area_updates, {
minp = vector.add(minp, 1),
maxp = vector.add(maxp, -1),
factor = factor, skip = skip, i = 1, edge_normal = nil})
local edges = get_edges(minp, maxp)
-- Register
for _, edge in ipairs(edges) do
table.insert(naturalslopeslib.progressive_area_updates, {
minp = edge[1], maxp = edge[2],
factor = factor, type = type, skip = skip, i = 1,
edge_normal = {x = edge[3][1], y = edge[3][2], z = edge[3][3]}
})
end
end
local function check_area_edges(area)
if area.edge_normal == nil then
return true
end
local edge = area.edge_normal
local pos = area.minp
local requirements = math.abs(edge.x) + math.abs(edge.y) + math.abs(edge.z)
local found = 0
if edge.x ~= 0 then
if minetest.get_node_or_nil(vector.add(pos, {x = edge.x, y = 0, z = 0})) ~= nil then
found = found + 1
end
end
if edge.y ~= 0 then
if minetest.get_node_or_nil(vector.add(pos, {x = 0, y = edge.y, z = 0})) ~= nil then
found = found + 1
end
end
if edge.z ~= 0 then
if minetest.get_node_or_nil(vector.add(pos, {x = 0, y = 0, z = edge.z})) ~= nil then
found = found + 1
end
end
return found == requirements
end
local function progressive_area_update(start_time)
if #naturalslopeslib.progressive_area_updates == 0 then
return true
end
if start_time == nil then
start_time = os.clock()
end
-- pick an area around a player at random and process it
local players = minetest.get_connected_players()
local processed_area_index = nil
local alt_processed_area_index = nil
for area_index, area in ipairs(naturalslopeslib.progressive_area_updates) do
for _, p in ipairs(players) do
local minp = area.minp
local maxp = area.maxp
local ppos = p:get_pos()
if ppos.x >= minp.x and ppos.x <= maxp.x and ppos.y >= minp.y and ppos.y <= maxp.y and ppos.z >= minp.z and ppos.z <= maxp.z then
-- Prefer an area in which a player is
if (check_area_edges(area)) then
processed_area_index = area_index
break
end
elseif alt_processed_area_index == nil and ppos.x + 16 >= minp.x and ppos.x - 16 <= maxp.x and ppos.y + 16 >= minp.y and ppos.y - 16 <= maxp.y and ppos.z + 16 >= minp.z and ppos.z - 16 <= maxp.z then
-- Else pick an area near a player
if (check_area_edges(area)) then
alt_processed_area_index = area_index
end
end
end
if processed_area_index ~= nil then
local area = naturalslopeslib.progressive_area_updates[processed_area_index]
end
end
if processed_area_index == nil then
if alt_processed_area_index ~= nil then
processed_area_index = alt_processed_area_index
else
processed_area_index = 1 -- try to reduce the queue as fast as possible
end
end
local area = naturalslopeslib.progressive_area_updates[processed_area_index]
local i = area.i
local y_size = area.maxp.y - area.minp.y + 1
local z_size = area.maxp.z - area.minp.z + 1
local imax = y_size * z_size * (area.maxp.x - area.minp.x + 1)
while i <= imax do
local x = math.floor((i - 1) / (y_size * z_size))
local y = math.floor((i - 1) / z_size) % y_size
local z = (i - 1) % (z_size)
local pos = {x = area.minp.x + x, y = area.minp.y + y, z = area.minp.z + z}
local node = minetest.get_node(pos)
naturalslopeslib.chance_update_shape(pos, node, area.factor, area.type)
i = i + 1 + math.random(area.skip / 2, area.skip)
if (os.clock() - start_time) > 0.1 and i <= imax then
area.i = i
return false
end
end
table.remove(naturalslopeslib.progressive_area_updates, processed_area_index)
if os.clock() - start_time < 0.1 then
progressive_area_update(start_time)
end
return true
end
local generation_dtime = 0
local function generation_globalstep(dtime)
generation_dtime = generation_dtime + dtime
if generation_dtime > 0.1 then
progressive_area_update()
generation_dtime = 0
end
end
minetest.register_globalstep(generation_globalstep)
minetest.register_on_shutdown(function()
if #naturalslopeslib.progressive_area_updates > 0 then
minetest.log("info", "Processing slope generation for queued areas")
for i, area in ipairs(naturalslopeslib.progressive_area_updates) do
minetest.log("info", (#naturalslopeslib.progressive_area_updates - i + 1) .. " remaining area(s)")
naturalslopeslib.area_chance_update_shape(area.minp, area.maxp, area.factor, area.skip, false, area.type)
end
end
end)
--[[
Triggers registration
--]]
-- Stomp function to get the replacement node name
function naturalslopeslib.update_shape_on_walk(player, pos, node, desc, trigger_meta)
return naturalslopeslib.get_replacement_node(pos, node)
end
-- Chat command
minetest.register_chatcommand('updshape', {
func = function(name, param)
local player = minetest.get_player_by_name(name)
if not player then return false, 'Player not found' end
if not minetest.check_player_privs(player, {server=true}) then return false, 'Update shape requires server privileges' end
local pos = player:get_pos()
local node_pos = {['x'] = pos.x, ['y'] = pos.y - 1, ['z'] = pos.z}
local node = minetest.get_node(node_pos)
if naturalslopeslib.update_shape(node_pos, node) then
return true, 'Shape updated.'
end
return false, node.name .. " cannot have it's shape updated."
end,
})
-- On generation big update
local function register_on_generation()
if not naturalslopeslib._register_on_generated then
return
end
if naturalslopeslib.setting_enable_shape_on_generation() then
if naturalslopeslib.setting_generation_method() == "Progressive" then
minetest.register_on_generated(function(minp, maxp, seed)
naturalslopeslib.register_progressive_area_update(minp, maxp, naturalslopeslib.setting_generation_factor(), naturalslopeslib.setting_generation_skip(), "mapgen")
end)
else
minetest.register_on_generated(function(minp, maxp, seed)
naturalslopeslib.area_chance_update_shape(minp, maxp, naturalslopeslib.setting_generation_factor(), naturalslopeslib.setting_generation_skip(), true, "mapgen")
end)
end
end
end
if not naturalslopeslib.setting_revert() then
minetest.register_on_mods_loaded(register_on_generation)
end
--- On place neighbor update
local function on_place_or_dig(pos, force_below)
local function update(pos, x, y, z, factor)
local new_pos = vector.add(pos, vector.new(x, y, z))
naturalslopeslib.chance_update_shape(new_pos, minetest.get_node(new_pos), factor, "place")
end
-- Update 8 neighbors plus above and below
local place_factor = naturalslopeslib.setting_dig_place_factor()
update(pos, 0, 0, 0, place_factor)
update(pos, 1, 0, 0, place_factor)
update(pos, 0, 0, 1, place_factor)
update(pos, -1, 0, 0, place_factor)
update(pos, 0, 0, -1, place_factor)
update(pos, 1, 0, 1, place_factor)
update(pos, 1, 0, -1, place_factor)
update(pos, -1, 0, 1, place_factor)
update(pos, -1, 0, -1, place_factor)
if force_below then update(pos, 0, -1, 0, 0)
else update(pos, 0, -1, 0, place_factor)
end
update(pos, 0, 1, 0, place_factor)
end
if naturalslopeslib.setting_enable_shape_on_dig_place() and not naturalslopeslib.setting_revert() then
minetest.register_on_placenode(function(pos, new_node, placer, old_node, item_stack, pointed_thing)
on_place_or_dig(pos, true)
end)
minetest.register_on_dignode(function(pos, old_node, digger)
on_place_or_dig(pos)
end)
end