2022-04-14 04:47:05 +02:00

655 lines
21 KiB
Lua

-- If true, the Perlin test nodes will support color
-- (set to false in case of performance problems)
local COLORIZE_NODES = true
local S = minetest.get_translator("perlin_explorer")
local F = minetest.formspec_escape
local formspec_states = {}
-- Holds the currently used Perlin noise
local current_perlin = {}
-- holds the current PerlinNoise object
current_perlin.noise = nil
current_perlin.noiseparams = {
offset = 0.0,
scale = 1.0,
spread = vector.new(10, 10, 10),
seed = 0,
octaves = 2,
persistence = 0.7,
lacunarity = 2.0,
flags = "noeased,noabsvalue",
}
-- Side length of calculated perlin area
current_perlin.size = 64
-- Theoretical min and max values for Perlin noise (for colorization)
current_perlin.min = -1
current_perlin.max = 1
-- dimensions of current Perlin noise (2 or 3)
current_perlin.dimensions = 2
-- If greater than 1, the Perlin noise values are "pixelized". Noise values at
-- coordinates not divisible by sidelen will be set equal to the noise value
-- of the nearest number (counting downwards) that is divisible by sidelen.
-- This is (kind of) analogous to the "sidelen" parameter of mapgen decorations.
current_perlin.sidelen = 1
current_perlin.pos = {}
current_perlin.auto_build = true
current_perlin.loaded_areas = {}
------------
-- Helper functions
-- Reduce the pos coordinates down to the closest numbers divisible by sidelen
local sidelen_pos = function(pos, sidelen)
local newpos = table.copy(pos)
if sidelen <= 1 then
return newpos
end
newpos.x = newpos.x - newpos.x % sidelen
newpos.y = newpos.y - newpos.y % sidelen
newpos.z = newpos.z - newpos.z % sidelen
return newpos
end
-- Test nodes to generate a map based on a perlin noise
local paramtype2
if COLORIZE_NODES then
paramtype2 = "color"
end
minetest.register_node("perlin_explorer:node", {
description = S("Perlin Test Node"),
paramtype = "light",
sunlight_propagates = true,
paramtype2 = paramtype2,
tiles = {"perlin_explorer_node.png"},
palette = "perlin_explorer_node_palette.png",
groups = { dig_immediate = 3 },
-- Force-drop without metadata to avoid spamming the inventory
drop = "perlin_explorer:node",
})
minetest.register_node("perlin_explorer:node_negative", {
description = S("Negative Perlin Test Node"),
paramtype = "light",
sunlight_propagates = true,
paramtype2 = paramtype2,
tiles = {"perlin_explorer_node_neg.png"},
palette = "perlin_explorer_node_palette_neg.png",
groups = { dig_immediate = 3 },
-- Force-drop without metadata to avoid spamming the inventory
drop = "perlin_explorer:node_negative",
})
minetest.register_tool("perlin_explorer:getter", {
description = S("Perlin Value Getter"),
_tt_help = S("Punch a node to display the Perlin noise value at this position"),
inventory_image = "perlin_explorer_getter.png",
wield_image = "perlin_explorer_getter.png",
groups = { disable_repair = 1 },
on_use = function(itemstack, user, pointed_thing)
if not user then
return
end
local privs = minetest.get_player_privs(user:get_player_name())
if not privs.server then
minetest.chat_send_player(user:get_player_name(), S("Insufficient privileges! You need the @1 privilege to use this tool.", "server"))
return
end
if current_perlin.noise then
if pointed_thing.type ~= "node" then
-- No-op for non-nodes
return
end
local pos = pointed_thing.under
local getpos = sidelen_pos(pos, current_perlin.sidelen)
local val
if current_perlin.dimensions == 2 then
val = current_perlin.noise:get_2d({x=getpos.x, y=getpos.z})
elseif current_perlin.dimensions == 3 then
val = current_perlin.noise:get_3d(getpos)
else
minetest.chat_send_player(user:get_player_name(), S("Unknown/invalid number of Perlin noise dimensions. Use /generate_perlin first!"))
end
minetest.chat_send_player(user:get_player_name(), S("pos=@1, value=@2", minetest.pos_to_string(pos), val))
else
local msg = S("No Perlin noise set. Set one with /set_perlin_noise!")
minetest.chat_send_player(user:get_player_name(), msg)
end
end,
})
local CONTENT_TEST_NODE = minetest.get_content_id("perlin_explorer:node")
local CONTENT_TEST_NODE_NEGATIVE = minetest.get_content_id("perlin_explorer:node_negative")
local update_map = function(pos, set_nodes)
local stats
if not current_perlin.noise then
return
end
local size_v = vector.new(current_perlin.size, current_perlin.size, current_perlin.size)
local startpos = pos
local endpos = vector.add(startpos, current_perlin.size-1)
local y_max = endpos.y - startpos.y
if current_perlin.dimensions == 2 then
y_max = 0
startpos.y = pos.y
endpos.y = pos.y
end
local vmanip, emin, emax, vdata, vdata2, varea
if set_nodes then
vmanip = VoxelManip(startpos, endpos)
emin, emax = vmanip:get_emerged_area()
vdata = vmanip:get_data()
vdata2 = vmanip:get_param2_data()
varea = VoxelArea:new({MinEdge = emin, MaxEdge = emax})
end
stats = {}
stats.avg = 0
local sum_of_values = 0
local value_count = 0
for x=0, endpos.x - startpos.x do
for y=0, y_max do
for z=0, endpos.z - startpos.z do
-- Get Perlin value at current pos
local relpos = vector.new(x,y,z)
local abspos = vector.add(startpos, relpos)
local abspos_get = sidelen_pos(abspos, current_perlin.sidelen)
local perlin_value
if current_perlin.dimensions == 2 then
perlin_value = current_perlin.noise:get_2d({x=abspos_get.x, y=abspos_get.z})
elseif current_perlin.dimensions == 3 then
perlin_value = current_perlin.noise:get_3d(abspos_get)
else
error("[perlin_explorer] Unknown/invalid number of Perlin noise dimensions!")
return
end
-- Statistics
if not stats.min then
stats.min = perlin_value
elseif perlin_value < stats.min then
stats.min = perlin_value
end
if not stats.max then
stats.max = perlin_value
elseif perlin_value > stats.max then
stats.max = perlin_value
end
sum_of_values = sum_of_values + perlin_value
value_count = value_count + 1
-- Calculate color (param2) for node
local zeropoint = 0
local min_size = zeropoint - current_perlin.min
local max_size = current_perlin.max - zeropoint
local node_param2
if perlin_value >= zeropoint then
node_param2 = (math.abs(perlin_value) / max_size) * 255
else
node_param2 = (math.abs(perlin_value) / min_size) * 255
end
node_param2 = math.floor(math.abs(node_param2))
node_param2 = math.max(0, math.min(255, node_param2))
if set_nodes then
-- Get vmanip index
local index = varea:indexp(abspos)
if not index then
return
end
-- Set node and param2
if perlin_value >= zeropoint then
vdata[index] = CONTENT_TEST_NODE
vdata2[index] = node_param2
else
if current_perlin.show_negative == true then
vdata[index] = CONTENT_TEST_NODE_NEGATIVE
vdata2[index] = node_param2
else
vdata[index] = minetest.CONTENT_AIR
vdata2[index] = 0
end
end
end
end
end
end
stats.avg = sum_of_values / value_count
if set_nodes then
-- Set vmanip, return stats
vmanip:set_data(vdata)
vmanip:set_param2_data(vdata2)
vmanip:write_to_map()
end
return stats
end
-- Creates and demonstrates a Perlin noise.
-- * pos: Where the Perlin noise starts
-- * options: table with:
-- * dimensions: number of Perlin noise dimensions (2 or 3)
-- * size: side length of area/volume to calculate)
-- * show_negative: if true, places nodes for negative Perlin values (default: true for 2 dimensions and false for 3 dimensions)
-- * set_nodes: if true, will set nodes, otherwise it's a "dry run"
local create_perlin = function(pos, options)
if not current_perlin.noise then
return false
end
current_perlin.dimensions = options.dimensions
current_perlin.size = options.size
current_perlin.show_negative = options.show_negative
if current_perlin.show_negative == nil then
if current_perlin.dimensions == 2 then
current_perlin.show_negative = true
elseif current_perlin.dimensions == 3 then
current_perlin.show_negative = false
end
end
local mpos = vector.round(pos)
current_perlin.pos = mpos
local set_nodes = options.set_nodes ~= false
local stats = update_map(mpos, set_nodes)
if stats then
return string.format("Perlin noise created! Stats: min. value=%.3f, max. value=%.3f, avg. value=%.3f", stats.min, stats.max, stats.avg)
end
end
-- Sets the currently active Perlin noise.
-- * noiseparams: NoiseParams table (see Minetest's Lua API documentation)
local set_perlin_noise = function(noiseparams)
current_perlin.noise = PerlinNoise(noiseparams)
current_perlin.noiseparams = noiseparams
end
minetest.register_chatcommand("perlin_set_options", {
privs = { server = true },
description = S("Set Perlin options"),
params = S("<dimensions> <sidelen> <minval> <maxval>"),
func = function(name, param)
local dimensions, sidelen, min, max = string.match(param, "([23]) ([0-9]+) ([0-9.-]+) ([0-9.-]+)")
if not dimensions then
return false
end
dimensions = tonumber(dimensions)
sidelen = tonumber(sidelen)
min = tonumber(min)
max = tonumber(max)
if not dimensions or not sidelen or not min or not max then
return false, S("Invalid parameter type.")
end
current_perlin.dimensions = dimensions
current_perlin.sidelen = sidelen
current_perlin.min = min
current_perlin.max = max
current_perlin.loaded_areas = {}
return true, S("Perlin options set!")
end,
})
minetest.register_chatcommand("perlin_set_noise", {
privs = { server = true },
description = S("Set Perlin noise parameters"),
params = S("<octaves> <offset> <scale> <spread_x> <spread_y> <spread_z> <persistence> <lacunarity> <seed>"),
func = function(name, param)
local octaves, offset, scale, sx, sy, sz, persistence, lacunarity, seed = string.match(param, string.rep("([0-9.-]+) ", 8) .. "([0-9.-]+)")
if not octaves then
return false
end
octaves = tonumber(octaves)
offset = tonumber(offset)
sx = tonumber(sx)
sy = tonumber(sy)
sz = tonumber(sz)
persistence = tonumber(persistence)
lacunarity = tonumber(lacunarity)
seed = tonumber(seed)
if not octaves or not offset or not sx or not sy or not sz or not persistence or not lacunarity or not seed then
return false, S("Invalid parameter type.")
end
set_perlin_noise({
octaves = octaves,
offset = offset,
scale = scale,
spread = { x = sx, y = sy, z = sz },
persistence = persistence,
lacunarity = lacunarity,
seed = seed,
})
current_perlin.loaded_areas = {}
return true, S("Perlin noise set!")
end,
})
minetest.register_chatcommand("perlin_generate", {
privs = { server = true },
description = S("Generate Perlin noise"),
params = S("<pos> <size>"),
func = function(name, param)
local x, y, z, size = string.match(param, "([0-9.-]+) ([0-9.-]+) ([0-9.-]+) ([0-9]+)")
if not x then
return false
end
x = tonumber(x)
y = tonumber(y)
z = tonumber(z)
size = tonumber(size)
if not x or not y or not z or not size then
return false
end
if not x or not y or not z or not size then
return false, S("Invalid parameter type.")
end
local pos = vector.new(x, y, z)
minetest.chat_send_player(name, S("Creating Perlin noise, please wait …"))
local msg = create_perlin(pos, {dimensions=current_perlin.dimensions, size=size})
if msg == false then
return false, S("No Perlin noise set. Set one with '/perlin_set_noise' first!")
end
return true, msg
end,
})
local build_flags_string = function(eased, absvalue)
local flags = ""
if eased then
flags = "eased"
else
flags = "noeased"
end
if absvalue then
flags = flags .. ",absvalue"
else
flags = flags .. ",noabsvalue"
end
return flags
end
local parse_flags_string = function(flags)
local ftable = string.split(flags, ",")
local eased, absvalue = false, false
for f=1, #ftable do
local s = string.trim(ftable[f])
if s == "eased" then
eased = true
elseif s == "absvalue" then
absvalue = true
end
end
return { eased = eased, absvalue = absvalue }
end
local show_formspec = function(player)
local offset = tostring(current_perlin.noiseparams.offset or "")
local scale = tostring(current_perlin.noiseparams.scale or "")
local seed = tostring(current_perlin.noiseparams.seed or "")
local sx, sy, sz = "", "", ""
if current_perlin.noiseparams.spread then
sx = tostring(current_perlin.noiseparams.spread.x or "")
sy = tostring(current_perlin.noiseparams.spread.y or "")
sz = tostring(current_perlin.noiseparams.spread.z or "")
end
local octaves = tostring(current_perlin.noiseparams.octaves or "")
local persistence = tostring(current_perlin.noiseparams.persistence or "")
local lacunarity = tostring(current_perlin.noiseparams.lacunarity or "")
local size = tostring(current_perlin.size or "")
local sidelen = tostring(current_perlin.sidelen or "")
local pos_x = tostring(current_perlin.pos.x or "")
local pos_y = tostring(current_perlin.pos.y or "")
local pos_z = tostring(current_perlin.pos.z or "")
local value_min = tostring(current_perlin.min or "")
local value_max = tostring(current_perlin.max or "")
local flags = current_perlin.noiseparams.flags
local flags_table = parse_flags_string(flags)
local eased = tostring(flags_table.eased)
local absvalue = tostring(flags_table.absvalue)
-- TODO: Add noise options
local dimensions_index = (current_perlin.dimensions or 2) - 1
local form = [[
formspec_version[4]size[10,11]
field[0.5,0.5;3,0.75;offset;]]..F(S("Offset"))..[[;]]..offset..[[]
field[3.5,0.5;3,0.75;scale;]]..F(S("Scale"))..[[;]]..scale..[[]
field[6.5,0.5;3,0.75;seed;]]..F(S("Seed"))..[[;]]..seed..[[]
field[0.5,1.75;3,0.75;spread_x;]]..F(S("X Spread"))..[[;]]..sx..[[]
field[3.5,1.75;3,0.75;spread_y;]]..F(S("Y Spread"))..[[;]]..sy..[[]
field[6.5,1.75;3,0.75;spread_z;]]..F(S("Z Spread"))..[[;]]..sz..[[]
field[0.5,3;3,0.75;octaves;]]..F(S("Octaves"))..[[;]]..octaves..[[]
field[3.5,3;3,0.75;persistence;]]..F(S("Persistence"))..[[;]]..persistence..[[]
field[6.5,3;3,0.75;lacunarity;]]..F(S("Lacunarity"))..[[;]]..lacunarity..[[]
checkbox[0.5,4;eased;]]..F(S("eased"))..[[;]]..eased..[[]
checkbox[3.5,4;absvalue;]]..F(S("absvalue"))..[[;]]..absvalue..[[]
label[0.5,5.0;]]..F(S("Dimensions"))..[[]
dropdown[0.5,5.2;1;dimensions;]]..F(S("2D"))..[[,]]..F(S("3D"))..[[;]]..dimensions_index..[[;true]
field[6.5,5.5;2,0.75;sidelen;]]..F(S("Sidelen"))..[[;]]..sidelen..[[]
field[0.5,6.7;2,0.75;pos_x;]]..F(S("X"))..[[;]]..pos_x..[[]
field[2.5,6.7;2,0.75;pos_y;]]..F(S("Y"))..[[;]]..pos_y..[[]
field[4.5,6.7;2,0.75;pos_z;]]..F(S("Z"))..[[;]]..pos_z..[[]
field[0.5,7.7;1,0.75;size;]]..F(S("Size"))..[[;]]..size..[[]
field[0.5,8.7;2,0.75;value_min;]]..F(S("Min."))..[[;]]..value_min..[[]
field[2.5,8.7;2,0.75;value_max;]]..F(S("Max."))..[[;]]..value_max..[[]
field_close_on_enter[offset;false]
field_close_on_enter[scale;false]
field_close_on_enter[seed;false]
field_close_on_enter[spread_x;false]
field_close_on_enter[spread_y;false]
field_close_on_enter[spread_z;false]
field_close_on_enter[octaves;false]
field_close_on_enter[persistence;false]
field_close_on_enter[lacunarity;false]
field_close_on_enter[sidelen;false]
field_close_on_enter[pos_x;false]
field_close_on_enter[pos_y;false]
field_close_on_enter[pos_z;false]
field_close_on_enter[value_min;false]
field_close_on_enter[value_max;false]
button[0.5,9.5;3,1;apply;]]..F(S("Apply"))..[[]
button[3.5,9.5;3,1;create;]]..F(S("Apply and create"))..[[]
button_exit[6.5,9.5;3,1;close;]]..F(S("Close"))..[[]
]]
minetest.show_formspec(player:get_player_name(), "perlin_explorer:creator", form)
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "perlin_explorer:creator" then
return
end
-- Require 'server' priv
local privs = minetest.get_player_privs(player:get_player_name())
if not privs.server then
return
end
-- Handle checkboxes
local name = player:get_player_name()
if fields.eased == "true" then
formspec_states[name].eased = true
elseif fields.eased == "false" then
formspec_states[name].eased = false
end
if fields.absvalue == "true" then
formspec_states[name].absvalue = true
elseif fields.absvalue == "false" then
formspec_states[name].absvalue = false
end
-- Handle other fields
local do_apply = fields.apply ~= nil
local do_create = fields.create ~= nil or fields.key_enter_field ~= nil
if (do_create or do_apply) then
if fields.offset and fields.scale and fields.seed and fields.spread_x and fields.spread_y and fields.spread_z and fields.octaves and fields.persistence and fields.lacunarity then
current_perlin.loaded_areas = {}
local offset = tonumber(fields.offset)
local scale = tonumber(fields.scale)
local seed = tonumber(fields.seed)
local sx = tonumber(fields.spread_x)
local sy = tonumber(fields.spread_y)
local sz = tonumber(fields.spread_z)
if not sx or not sy or not sz then
return
end
local spread = vector.new(sx, sy, sz)
local octaves = tonumber(fields.octaves)
local persistence = tonumber(fields.persistence)
local lacunarity = tonumber(fields.lacunarity)
local dimensions = tonumber(fields.dimensions)
local sidelen = tonumber(fields.sidelen)
local px = tonumber(fields.pos_x)
local py = tonumber(fields.pos_y)
local pz = tonumber(fields.pos_z)
local size = tonumber(fields.size)
local value_min = tonumber(fields.value_min)
local value_max = tonumber(fields.value_max)
if not px or not py or not pz then
return
end
local place_pos = vector.new(px, py, pz)
if (offset and scale and spread and octaves and persistence and dimensions and sidelen and size and value_min and value_max) then
-- Convert dropdown index to actual dimensions number
dimensions = dimensions + 1
-- Spread is used differently in 2D
if dimensions == 2 then
spread.y = spread.z
end
local eased = formspec_states[name].eased
local absvalue = formspec_states[name].absvalue
set_perlin_noise({
offset = offset,
scale = scale,
seed = seed,
spread = spread,
octaves = octaves,
persistence = persistence,
lacunarity = lacunarity,
flags = build_flags_string(eased, absvalue),
})
current_perlin.dimensions = dimensions
current_perlin.size = size
current_perlin.sidelen = sidelen
current_perlin.min = value_min
current_perlin.max = value_max
if do_create then
local msg = S("Creating Perlin noise, please wait …")
minetest.chat_send_player(name, msg)
msg = create_perlin(place_pos, {dimensions=dimensions, size=size})
if msg then
minetest.chat_send_player(name, msg)
elseif msg == false then
minetest.log("error", "[perlin_explorer] Error generating Perlin noise nodes!")
end
end
end
end
end
end)
minetest.register_tool("perlin_explorer:creator", {
description = S("Perlin Noise Creator"),
_tt_help = S("Punch to open the Perlin noise creation menu"),
inventory_image = "perlin_explorer_creator.png",
wield_image = "perlin_explorer_creator.png",
groups = { disable_repair = 1 },
on_use = function(itemstack, user, pointed_thing)
if not user then
return
end
local privs = minetest.get_player_privs(user:get_player_name())
if not privs.server then
minetest.chat_send_player(user:get_player_name(), S("Insufficient privileges! You need the @1 privilege to use this tool.", "server"))
return
end
show_formspec(user)
end,
})
local TIME_UPDATE = 1
local AUTOBUILD_SIZE = 16
local timer = 0
minetest.register_globalstep(function(dtime)
timer = timer + dtime
if timer < TIME_UPDATE then
return
end
timer = 0
if current_perlin.noise and current_perlin.auto_build then
local player = minetest.get_player_by_name("singleplayer")
if not player then
return
end
local build = function(pos, player_name)
local hash = minetest.hash_node_position(pos)
if not current_perlin.loaded_areas[hash] then
local msg = create_perlin(pos, {
dimensions = current_perlin.dimensions,
size = AUTOBUILD_SIZE,
})
minetest.chat_send_player(player_name, msg)
current_perlin.loaded_areas[hash] = true
end
end
local pos = vector.round(player:get_pos())
pos = sidelen_pos(pos, AUTOBUILD_SIZE)
local neighbors = {
vector.new(0, 0, 0),
vector.new(0, 0, -1),
vector.new(0, 0, 1),
vector.new(0, -1, -1),
vector.new(0, -1, 0),
vector.new(0, -1, 1),
vector.new(0, 1, -1),
vector.new(0, 1, 0),
vector.new(0, 1, 1),
vector.new(-1, -1, -1),
vector.new(-1, -1, 0),
vector.new(-1, -1, 1),
vector.new(-1, 0, -1),
vector.new(-1, 0, 0),
vector.new(-1, 0, 1),
vector.new(-1, 1, -1),
vector.new(-1, 1, 0),
vector.new(-1, 1, 1),
vector.new(1, -1, -1),
vector.new(1, -1, 0),
vector.new(1, -1, 1),
vector.new(1, 0, -1),
vector.new(1, 0, 0),
vector.new(1, 0, 1),
vector.new(1, 1, -1),
vector.new(1, 1, 0),
vector.new(1, 1, 1),
}
for n=1, #neighbors do
local offset = vector.multiply(neighbors[n], 16)
local npos = vector.add(pos, offset)
build(npos, player:get_player_name())
end
end
end)
minetest.register_on_joinplayer(function(player)
local name = player:get_player_name()
formspec_states[name] = {
eased = false,
}
end)
minetest.register_on_leaveplayer(function(player)
local name = player:get_player_name()
formspec_states[name] = nil
end)