2022-04-17 01:29:43 +02:00

1327 lines
43 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- If true, the Perlin test nodes will support color
-- (set to false in case of performance problems)
local COLORIZE_NODES = true
-- The number of available test node colors is divided by this number.
-- A number between 1 to 256
-- * 1 = Full 256 palette.
-- * 2 = 128 colors.
-- * 4 = 64 colors.
-- etc.
-- Higher values lead to less colors but increased performance.
-- This value is only used for performance reason, because Minetest
-- has a sharp performance drop when there are many different node colors on screen.
-- Consider removing this one when Minetest's performance problem has been solved.
local COLOR_PRECISION = 4
-- Time to wait in seconds before checking and generating new nodes in autobuild mode.
local AUTOBUILD_UPDATE_TIME = 0.1
-- x/y/z size of "noisechunks" (like chunks in the Minetest mapgen, but specific to this
-- mod) to generate in autobuild mode.
local AUTOBUILD_SIZE = 24
-- Amount of noisechunks to generate around player
local AUTOBUILD_CHUNKDIST = 2
-- Color of the formspec box[] element
local FORMSPEC_BOX_COLOR = "#00000080"
-- Color for the section titles in the formspec
local FORMSPEC_HEADER_COLOR = "#000000FF"
-- Buckets to use for the histogram
local HISTOGRAM_BUCKETS = 10
-- Length of side (XZ) of the square to calculate in the Deep Analyze mode.
-- Number of values calculated is this to the power of 2.
local DEEP_ANALYSIS_SIZE_2D = 2048
-- Length of side (XZZ) of the cube to calculate in the Deep Analyze mode.
-- Number of values calculated is this to the power of 3.
local DEEP_ANALYSIS_SIZE_3D = 162 -- 163^3 is roughly equal to 2048
local S = minetest.get_translator("perlin_explorer")
local F = minetest.formspec_escape
-- Per-player formspec states (mostly for remembering checkbox states)
local formspec_states = {}
-- List of noise parameters profiles
local np_profiles = {}
local default_noiseparams = {
offset = 0.0,
scale = 1.0,
spread = vector.new(10, 10, 10),
seed = 0,
octaves = 2,
persistence = 0.5,
lacunarity = 2.0,
flags = "noeased,noabsvalue",
}
-- Holds the currently used Perlin noise
local current_perlin = {}
-- holds the current PerlinNoise object
current_perlin.noise = nil
current_perlin.noiseparams = table.copy(default_noiseparams)
local noise_settings = dofile(minetest.get_modpath(minetest.get_current_modname()).."/noise_settings_list.lua")
for n=1, #noise_settings do
local np = minetest.get_mapgen_setting_noiseparams(noise_settings[n])
-- TODO/FIXME: Make sure that ALL noise settings are gettable (not just those of the active mapgen)
if np then
table.insert(np_profiles, {noiseparams=np, name=noise_settings[n], can_delete=false})
end
end
table.insert(np_profiles, {noiseparams=current_perlin.noiseparams})
-- 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
current_perlin.nodetype = 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
-- Place position of current perlin (relevant for single placing)
current_perlin.pos = nil
-- If enabled, automatically generate nodes around player
current_perlin.autogen = false
-- Remember which areas have been loaded by the autogen so far
-- Index: Hash of node position, value: true if loaded
local 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
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
-- 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
-- Test nodes to generate a map based on a perlin noise
local paramtype2
if COLORIZE_NODES then
paramtype2 = "color"
end
-- Register list of node types for test mapgen
local nodetypes = {
-- { Entry name for sformspec, positive node, negative node, supports color? }
{ S("Solid Nodes"), "perlin_explorer:node", "perlin_explorer:node_negative", true },
{ S("Grid Nodes"), "perlin_explorer:grid", "perlin_explorer:grid_negative", true },
{ S("Minibox Nodes"), "perlin_explorer:mini", "perlin_explorer:mini_negative", true },
}
-- Analyze the given noiseparams for interesting properties.
-- Returns: <min>, <max>, <waves>
-- min = minimum possible value
-- max = maximum possible value
-- waves = table with x/y/z indices, each containing a list of effective "wavelengths" for each of the axes
local analyze_noiseparams = function(noiseparams)
local np = noiseparams
local flags = parse_flags_string(noiseparams.flags)
local is_absolute = flags.absvalue == true
-- Calculate min. and max. possible values
-- Octaves
local o_min, o_max = 0, 0
for o=1, np.octaves do
local exp = o-1
o_max = o_max + (1 * np.persistence ^ exp)
if not is_absolute then
o_min = o_min + (- 1 * np.persistence ^ exp)
-- Note: If absvalue flag is set, the sum of the octaves
-- is always 0, so we don't need to calculate it
end
end
-- Offset and scale
local min_value = np.offset + np.scale * o_min
local max_value = np.offset + np.scale * o_max
-- Calculate "wavelengths"
local axes = { "x", "y", "z" }
local waves = {}
for a=1, #axes do
local w = axes[a]
waves[w] = {}
local wave = np.spread[w]
for o=1, np.octaves do
table.insert(waves[w], wave)
wave = wave * (1 / np.lacunarity)
end
end
return min_value, max_value, waves
end
-- Add stone node to nodetypes, if present
minetest.register_on_mods_loaded(function()
local stone = minetest.registered_aliases["mapgen_stone"]
if stone then
local desc = minetest.registered_nodes[stone].description
if not desc then
desc = stone
end
table.insert(nodetypes, { desc, stone, "air", false})
end
end)
minetest.register_node("perlin_explorer:node", {
description = S("Solid 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("Solid 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_node("perlin_explorer:grid", {
description = S("Grid Perlin Test Node"),
paramtype = "light",
drawtype = "allfaces",
use_texture_alpha = "clip",
sunlight_propagates = true,
paramtype2 = paramtype2,
tiles = {"perlin_explorer_grid.png"},
palette = "perlin_explorer_node_palette.png",
groups = { dig_immediate = 3 },
drop = "perlin_explorer:grid",
})
minetest.register_node("perlin_explorer:grid_negative", {
description = S("Grid Negative Perlin Test Node"),
paramtype = "light",
sunlight_propagates = true,
paramtype2 = paramtype2,
tiles = {"perlin_explorer_grid_neg.png"},
use_texture_alpha = "clip",
palette = "perlin_explorer_node_palette_neg.png",
groups = { dig_immediate = 3 },
drop = "perlin_explorer:grid_negative",
})
minetest.register_node("perlin_explorer:mini", {
description = S("Minibox Perlin Test Node"),
paramtype = "light",
drawtype = "nodebox",
climbable = true,
walkable = false,
node_box = {
type = "fixed",
fixed = { -2/16, -2/16, -2/16, 2/16, 2/16, 2/16 },
},
use_texture_alpha = "clip",
sunlight_propagates = true,
paramtype2 = paramtype2,
tiles = {"perlin_explorer_mini.png"},
palette = "perlin_explorer_node_palette.png",
groups = { dig_immediate = 3 },
drop = "perlin_explorer:mini",
})
minetest.register_node("perlin_explorer:mini_negative", {
description = S("Minibox Negative Perlin Test Node"),
paramtype = "light",
drawtype = "nodebox",
climbable = true,
walkable = false,
node_box = {
type = "fixed",
fixed = { -2/16, -2/16, -2/16, 2/16, 2/16, 2/16 },
},
sunlight_propagates = true,
paramtype2 = paramtype2,
tiles = {"perlin_explorer_mini_neg.png"},
use_texture_alpha = "clip",
palette = "perlin_explorer_node_palette_neg.png",
groups = { dig_immediate = 3 },
drop = "perlin_explorer:mini_negative",
})
local print_value = function(pos, user, precision, ptype)
local val
local getpos = sidelen_pos(pos, current_perlin.sidelen)
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
local msg
if ptype == "node" then
msg = S("node pos=@1, value=@2", minetest.pos_to_string(pos, precision), val)
elseif ptype == "player" then
msg = S("player pos=@1, value=@2", minetest.pos_to_string(pos, precision), val)
else
error("[perlin_explorer] Invalid ptype in print_value()!")
end
minetest.chat_send_player(user:get_player_name(), msg)
end
-- Get Perlin value of player pos
local use_getter = 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
local pos = user:get_pos()
local ctrl = user:get_player_control()
local precision = 1
if not ctrl.sneak then
pos = vector.round(pos)
precision = 0
end
print_value(pos, user, precision, "player")
else
local msg = S("No Perlin noise set. Set one first!")
minetest.chat_send_player(user:get_player_name(), msg)
end
end
-- Get Perlin value of pointed node
local place_getter = 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
print_value(pos, user, 0, "node")
else
local msg = S("No Perlin noise set. Set one first!")
minetest.chat_send_player(user:get_player_name(), msg)
end
end
minetest.register_tool("perlin_explorer:getter", {
description = S("Perlin Value Getter"),
_tt_help = S("Place: Display Perlin noise value of the pointed node position").."\n"..
S("Punch: Display Perlin noise value of player position (+Sneak: precise position)"),
inventory_image = "perlin_explorer_getter.png",
wield_image = "perlin_explorer_getter.png",
groups = { disable_repair = 1 },
on_use = use_getter,
on_place = place_getter,
})
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
local content_test_node = minetest.get_content_id(nodetypes[current_perlin.nodetype][2])
local content_test_node_negative = minetest.get_content_id(nodetypes[current_perlin.nodetype][3])
local needs_color = nodetypes[current_perlin.nodetype][4]
stats = {}
stats.avg = 0
local sum_of_values = 0
stats.value_count = 0
stats.histogram = {}
local min_possible, max_possible = analyze_noiseparams(current_perlin.noiseparams)
local cutoff_points = {}
for d=1,HISTOGRAM_BUCKETS do
cutoff_points[d] = min_possible + ((max_possible-min_possible) / HISTOGRAM_BUCKETS) * d
stats.histogram[d] = 0
end
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
-- Histogram
for c=1, HISTOGRAM_BUCKETS do
if perlin_value < cutoff_points[c] or c >= HISTOGRAM_BUCKETS then
stats.histogram[c] = stats.histogram[c] + 1
break
end
end
sum_of_values = sum_of_values + perlin_value
stats.value_count = stats.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 = 0
if needs_color then
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 node_param2 < 255 then
node_param2 = node_param2 - (node_param2 % COLOR_PRECISION)
end
end
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 / stats.value_count
stats.histogram_points = cutoff_points
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 cpos = table.copy(pos)
local mpos = vector.round(cpos)
current_perlin.pos = mpos
local set_nodes = options.set_nodes ~= false
local stats = update_map(mpos, set_nodes)
if set_nodes then
-- Show a particle in the center of the newly generated area
local center = vector.new()
center.x = mpos.x + options.size/2
if current_perlin.dimensions == 2 then
center.y = mpos.y + 3
else
center.y = mpos.y + options.size/2
end
center.z = mpos.z + options.size/2
minetest.add_particle({
pos = center,
expirationtime = 4,
size = 16,
texture = "perlin_explorer_new_noisechunk.png",
glow = minetest.LIGHT_MAX,
})
end
if stats then
minetest.log("info", "[perlin_explorer] Perlin noise generated at %s! Stats: min. value=%.3f, max. value=%.3f, avg. value=%.3f", minetest.pos_to_string(mpos), stats.min, stats.max, stats.avg)
return S("Perlin noise generated at @1!", minetest.pos_to_string(mpos)), stats
else
minetest.log("error", "[perlin_explorer] Could not get stats!")
return false
end
end
local seeder_reseed = function(player, regen)
local msg
if regen and (not current_perlin.autogen and current_perlin.pos) then
msg = S("New random seed set, starting to regenerate nodes ...")
minetest.chat_send_player(player:get_player_name(), msg)
msg = create_perlin(current_perlin.pos, {dimensions = current_perlin.dimensions, size = current_perlin.size})
if msg ~= false then
minetest.chat_send_player(player:get_player_name(), msg)
end
else
msg = S("New random seed set!")
minetest.chat_send_player(player:get_player_name(), msg)
end
end
local function seeder_use(reseed)
return 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
local noiseparams = table.copy(current_perlin.noiseparams)
noiseparams.seed = math.random(0, 2^32-1)
set_perlin_noise(noiseparams)
loaded_areas = {}
seeder_reseed(user, reseed)
else
local msg = S("No Perlin noise set. Set one first!")
minetest.chat_send_player(user:get_player_name(), msg)
end
end
end
minetest.register_tool("perlin_explorer:seeder", {
description = S("Random Perlin seed setter"),
_tt_help = S("Punch: Set a random seed for the current Perlin noise params").."\n"..
S("Place: Set a random seed and regenerate nodes (if applicable)"),
inventory_image = "perlin_explorer_seeder.png",
wield_image = "perlin_explorer_seeder.png",
groups = { disable_repair = 1 },
on_use = seeder_use(false),
on_secondary_use = seeder_use(true),
on_place = seeder_use(true),
})
minetest.register_chatcommand("perlin_set_options", {
privs = { server = true },
description = S("Set Perlin map generation 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
loaded_areas = {}
return true, S("Perlin map generation options set!")
end,
})
minetest.register_chatcommand("perlin_set_noise", {
privs = { server = true },
description = S("Set active Perlin noise parameters"),
params = S("<offset> <scale> <seed> <spread_x> <spread_y> <spread_z> <octaves> <persistence> <lacunarity> [<flags>]"),
func = function(name, param)
local offset, scale, seed, sx, sy, sz, octaves, persistence, lacunarity, flags = string.match(param, string.rep("([0-9.-]+) ", 9) .. "([a-zA-Z, ]+)")
if not offset then
offset, scale, seed, sx, sy, sz, octaves, persistence, lacunarity = string.match(param, string.rep("([0-9.-]+) ", 8) .. "([0-9.-]+)")
if not offset then
return false
end
end
if not flags then
flags = ""
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
local noiseparams = {
octaves = octaves,
offset = offset,
scale = scale,
spread = { x = sx, y = sy, z = sz },
persistence = persistence,
lacunarity = lacunarity,
seed = seed,
flags = flags,
}
noiseparams = fix_noiseparams(noiseparams)
set_perlin_noise(noiseparams)
loaded_areas = {}
return true, S("Active Perlin noise parameters 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 first!")
end
return true, msg
end,
})
local show_histogram_loading_formspec = function(player)
local form = [[
formspec_version[4]size[10,2]
container[0.25,0.25]
box[0,0;9.5,1.5;]]..FORMSPEC_BOX_COLOR..[[]
box[0,0;9.5,0.4;]]..FORMSPEC_HEADER_COLOR..[[]
label[0.25,0.2;]]..F(S("Statistical analysis in progress"))..[[]
container[0.25,0.8]
label[0,0;]]..F(S("Collecting data, please wait …"))..[[]
container_end[]
container_end[]
]]
minetest.show_formspec(player:get_player_name(), "perlin_explorer:histogram_loading", form)
end
local show_histogram_formspec = function(player, stats)
local txt = ""
local maxh = 6.0
local boxes = ""
boxes = boxes .. "label[0,7.0;"..F(S("Max.")).."\n"..F(S("Min.")).."]"
local hstart = 1
-- Special case: If bucket sizes are equal, only show the last bucket
-- (can happen if scale=0)
if HISTOGRAM_BUCKETS > 1 and stats.histogram_points[1] == stats.histogram_points[2] then
hstart = HISTOGRAM_BUCKETS
end
-- Drawn histogram bars, tooltips and labels
for h=hstart, HISTOGRAM_BUCKETS do
local count = stats.histogram[h]
local ratio = (stats.histogram[h] / stats.value_count)
local perc = ratio * 100
local perc_f = string.format("%.1f", perc)
local x = h * 0.9
local height = maxh * ratio
local coords = x..","..maxh-height..";0.8,"..height
local box = ""
if count > 0 then
box = box .. "box["..coords..";#00FF00FF]"
box = box .. "tooltip["..coords..";"..count.."]"
end
box = box .. "label["..x..",6.4;"..F(S("@1%", perc_f)).."]"
box = box .. "tooltip["..x..",6.2;0.9,0.3;"..count.."]"
local min, max, min_v, max_v
if h <= 1 then
min = ""
else
min = F(string.format("%.1f", stats.histogram_points[h-1]))
end
if h >= HISTOGRAM_BUCKETS then
max = ""
else
max = F(string.format("%.1f", stats.histogram_points[h]))
end
box = box .. "label["..x..",7.0;"..max.."\n"..min.."]"
local tt
if h == 1 then
tt = F(S("value < @1", stats.histogram_points[h]))
elseif h == HISTOGRAM_BUCKETS then
tt = F(S("@1 <= value", stats.histogram_points[h-1]))
else
tt = F(S("@1 <= value < @2", stats.histogram_points[h-1], stats.histogram_points[h]))
end
box = box .. "tooltip["..x..",6.8;0.9,1;"..tt.."]"
boxes = boxes .. box
end
local vmin, vmax
if current_perlin.dimensions == 2 then
vmin = S("(@1,@2)", 0,0)
vmax = S("(@1,@2)", DEEP_ANALYSIS_SIZE_2D, DEEP_ANALYSIS_SIZE_2D)
else
vmin = S("(@1,@2,@3)", 0,0,0)
vmax = S("(@1,@2,@3)", DEEP_ANALYSIS_SIZE_3D, DEEP_ANALYSIS_SIZE_3D, DEEP_ANALYSIS_SIZE_3D)
end
local labels = "label[0,0;"..F(S("Values calculated: @1", stats.value_count)).."\n"..
F(S("Tested noise coordinates: @1 to @2", vmin, vmax)).."]"
local form = [[
formspec_version[4]size[11,10]
container[0.25,0.25]
box[0,0;10.5,9.5;]]..FORMSPEC_BOX_COLOR..[[]
box[0,0;10.5,0.4;]]..FORMSPEC_HEADER_COLOR..[[]
label[0.25,0.2;]]..F(S("Noise Value Histogram"))..[[]
container[0.25,0.8]
]]..labels..[[
]]..boxes..[[
container_end[]
container_end[]
]]
minetest.show_formspec(player:get_player_name(), "perlin_explorer:histogram", form)
end
-- Analyzes the given noise params and shows the result in a pretty-printed formspec to player
local analyze_noiseparams_and_show_formspec = function(player, noiseparams)
local min, max, waves = analyze_noiseparams(noiseparams)
local print_waves = function(waves_a)
local stringified_waves = {}
for w=1, #waves_a do
local strwave
local is_bad = false
if minetest.is_nan(waves_a[w]) or waves_a[w] == math.huge or waves_a[w] == -math.huge then
strwave = minetest.colorize("#FF0000FF", waves_a[w])
elseif waves_a[w] < 1 then
strwave = minetest.colorize("#FF0000FF", "0")
else
strwave = string.format("%.0f", waves_a[w])
end
table.insert(stringified_waves, strwave)
end
return table.concat(stringified_waves, ", ")
end
local form = [[
formspec_version[4]size[10,5]
container[0.25,0.25]
box[0,0;9.5,3.5;]]..FORMSPEC_BOX_COLOR..[[]
box[0,0;9.5,0.4;]]..FORMSPEC_HEADER_COLOR..[[]
label[0.25,0.2;]]..F(S("Noise Parameters Analysis"))..[[]
label[0.25,1;]]..F(S("Minimum possible value: @1", min))..[[]
label[0.25,1.5;]]..F(S("Maximum possible value: @1", max))..[[]
label[0.25,2;]]..F(S("X wavelengths: @1", print_waves(waves.x)))..[[]
label[0.25,2.5;]]..F(S("Y wavelengths: @1", print_waves(waves.y)))..[[]
label[0.25,3;]]..F(S("Z wavelengths: @1", print_waves(waves.z)))..[[]
button[3.5,3.75;3,0.75;done;]]..F(S("Done"))..[[]
container_end[]
--]]
minetest.show_formspec(player:get_player_name(), "perlin_explorer:analyze", form)
end
local show_noise_formspec = function(player, noiseparams, profile_id)
local np
if noiseparams then
np = noiseparams
else
np = current_perlin.noiseparams
end
if not profile_id then
profile_id = 1
end
local offset = tostring(np.offset or "")
local scale = tostring(np.scale or "")
local seed = tostring(np.seed or "")
local sx, sy, sz = "", "", ""
if np.spread then
sx = tostring(np.spread.x or "")
sy = tostring(np.spread.y or "")
sz = tostring(np.spread.z or "")
end
local octaves = tostring(np.octaves or "")
local persistence = tostring(np.persistence or "")
local lacunarity = tostring(np.lacunarity or "")
local size = tostring(current_perlin.size or "")
local sidelen = tostring(current_perlin.sidelen or "")
local pos_x, pos_y, pos_z = "", "", ""
if current_perlin.pos then
pos_x = tostring(current_perlin.pos.x or "")
pos_y = tostring(current_perlin.pos.y or "")
pos_z = tostring(current_perlin.pos.z or "")
end
local value_min = tostring(current_perlin.min or "")
local value_max = tostring(current_perlin.max or "")
local flags = np.flags
local flags_table = parse_flags_string(flags)
local eased = tostring(flags_table.eased)
local absvalue = tostring(flags_table.absvalue)
local noiseparams_list = {}
local counter = 1
for i=1, #np_profiles do
local npp = np_profiles[i]
local name = npp.name
if not name then
name = S("Profile @1", counter)
counter = counter + 1
end
table.insert(noiseparams_list, F(name))
end
local noiseparams_list_str = table.concat(noiseparams_list, ",")
local dimensions_index = (current_perlin.dimensions or 2) - 1
local nodetype_index = (current_perlin.nodetype)
local nodetypes_list = {}
for i=1, #nodetypes do
table.insert(nodetypes_list, F(nodetypes[i][1]))
end
local nodetypes_list_str = table.concat(nodetypes_list, ",")
local delete_btn = ""
if #np_profiles > 1 then
delete_btn = "button[7.25,0;2.0,0.5;delete_np_profile;"..F(S("Delete")).."]"
end
local autogen_label
local create_btn = ""
local xyzsize = ""
if current_perlin.autogen then
autogen_label = S("Disable mapgen")
else
autogen_label = S("Enable mapgen")
create_btn = "button[3.5,0;3,1;create;"..F(S("Apply and create")).."]"
xyzsize = [[
field[0.25,1.95;2,0.75;pos_x;]]..F(S("X"))..[[;]]..pos_x..[[]
field[2.35,1.95;2,0.75;pos_y;]]..F(S("Y"))..[[;]]..pos_y..[[]
field[4.45,1.95;2,0.75;pos_z;]]..F(S("Z"))..[[;]]..pos_z..[[]
field[6.55,1.95;2,0.75;size;]]..F(S("Size"))..[[;]]..size..[[]
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]
]]
end
local form = [[
formspec_version[4]size[10,12.5]
container[0.25,0.25]
box[0,0;9.5,5.5;]]..FORMSPEC_BOX_COLOR..[[]
box[0,0;9.5,0.4;]]..FORMSPEC_HEADER_COLOR..[[]
label[0.15,0.2;]]..F(S("Noise parameters"))..[[]
container[0.0,0.5]
dropdown[0.25,0;3,0.5;np_profiles;]]..noiseparams_list_str..[[;]]..profile_id..[[;true]
button[3.25,0;2.0,0.5;add_np_profile;]]..F(S("Add"))..[[]
button[5.25,0;2.0,0.5;load_np_profile;]]..F(S("Load"))..[[]
]]..delete_btn..[[
container_end[]
container[0.0,1.5]
field[0.25,0;2,0.75;offset;]]..F(S("Offset"))..[[;]]..offset..[[]
field[3.25,0;2,0.75;scale;]]..F(S("Scale"))..[[;]]..scale..[[]
field[6.25,0;2,0.75;seed;]]..F(S("Seed"))..[[;]]..seed..[[]
image_button[8.35,0.0;0.75,0.75;perlin_explorer_seeder.png;set_random_seed;]
field[0.25,1.2;2,0.75;spread_x;]]..F(S("X Spread"))..[[;]]..sx..[[]
field[3.25,1.2;2,0.75;spread_y;]]..F(S("Y Spread"))..[[;]]..sy..[[]
field[6.25,1.2;2,0.75;spread_z;]]..F(S("Z Spread"))..[[;]]..sz..[[]
field[0.25,2.4;2,0.75;octaves;]]..F(S("Octaves"))..[[;]]..octaves..[[]
field[3.25,2.4;2,0.75;persistence;]]..F(S("Persistence"))..[[;]]..persistence..[[]
field[6.25,2.4;2,0.75;lacunarity;]]..F(S("Lacunarity"))..[[;]]..lacunarity..[[]
checkbox[0.25,3.55;eased;]]..F(S("eased"))..[[;]]..eased..[[]
checkbox[3.25,3.55;absvalue;]]..F(S("absvalue"))..[[;]]..absvalue..[[]
button[6.25,3.35;2.0,0.5;analyze;]]..F(S("Analyze"))..[[]
container_end[]
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]
tooltip[set_random_seed;]]..F(S("Random seed"))..[[]
container_end[]
container[0.25,6.0]
box[0,0;9.5,1.6;]]..FORMSPEC_BOX_COLOR..[[]
box[0,0;9.5,0.4;]]..FORMSPEC_HEADER_COLOR..[[]
label[0.15,0.2;]]..F(S("Noise options"))..[[]
dropdown[0.25,0.7;1,0.75;dimensions;]]..F(S("2D"))..[[,]]..F(S("3D"))..[[;]]..dimensions_index..[[;true]
field[2.25,0.7;2,0.75;sidelen;]]..F(S("Pixelization"))..[[;]]..sidelen..[[]
button[6.25,0.7;2.0,0.6;deep_analyze;]]..F(S("Statistics"))..[[]
tooltip[sidelen;]]..F(S("If higher than 1, Perlin values will be repeated along all axes every x nodes, for a pixelized effect."))..[[]
container_end[]
container[0.25,7.85]
box[0,0;9.5,2.9;]]..FORMSPEC_BOX_COLOR..[[]
box[0,0;9.5,0.4;]]..FORMSPEC_HEADER_COLOR..[[]
label[0.15,0.2;]]..F(S("Node generation"))..[[]
field[0.25,0.75;2,0.75;value_min;]]..F(S("Min. color at"))..[[;]]..value_min..[[]
field[2.35,0.75;2,0.75;value_max;]]..F(S("Max. color at"))..[[;]]..value_max..[[]
dropdown[6.55,0.75;2,0.75;nodetype;]]..nodetypes_list_str..[[;]]..nodetype_index..[[;true]
]]..xyzsize..[[
tooltip[value_min;]]..F(S("The Perlin value at which the node color gradient begins. Must be lower than 0."))..[[]
tooltip[value_max;]]..F(S("The Perlin value at which the node color gradient ends. Must be higher than 0."))..[[]
container_end[]
container[0,10.95]
button[0.5,0;3,1;apply;]]..F(S("Apply"))..[[]
]]..create_btn..[[
button[6.5,0;3,1;toggle_autogen;]]..F(autogen_label)..[[]
container_end[]
]]
minetest.show_formspec(player:get_player_name(), "perlin_explorer:creator", form)
end
-- Fix some errors in the noiseparams
local fix_noiseparams = function(noiseparams)
noiseparams.octaves = math.floor(math.max(1, noiseparams.octaves))
noiseparams.lacunarity = math.max(1.0, noiseparams.lacunarity)
noiseparams.spread.x = math.floor(math.max(1, noiseparams.spread.x))
noiseparams.spread.y = math.floor(math.max(1, noiseparams.spread.y))
noiseparams.spread.z = math.floor(math.max(1, noiseparams.spread.z))
return noiseparams
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
-- Require 'server' priv
local privs = minetest.get_player_privs(player:get_player_name())
if not privs.server then
return
end
-- Analysis window
if formname == "perlin_explorer:analyze" then
if fields.done then
local noiseparams = formspec_states[player:get_player_name()].noiseparams
show_noise_formspec(player, noiseparams)
return
end
end
-- Creator window
if formname ~= "perlin_explorer:creator" then
return
end
-- Start deep analysis
if fields.deep_analyze then
local pos = vector.zero()
local size
if current_perlin.dimensions == 2 then
size = DEEP_ANALYSIS_SIZE_2D
else -- 3 dimensions
size = DEEP_ANALYSIS_SIZE_3D
end
-- Show a loading formspec
show_histogram_loading_formspec(player)
-- This takes long
local _, stats = create_perlin(pos, {dimensions=current_perlin.dimensions, size=size, set_nodes=false})
if stats then
-- Update the formspec to show the result
show_histogram_formspec(player, stats)
else
minetest.log("error", "[perlin_explorer] Error while creating stats from Perlin noise!")
end
return
end
-- Handle checkboxes
local name = player:get_player_name()
local flags_touched = false
if fields.eased == "true" then
formspec_states[name].eased = true
return
elseif fields.eased == "false" then
formspec_states[name].eased = false
return
end
if fields.absvalue == "true" then
formspec_states[name].absvalue = true
return
elseif fields.absvalue == "false" then
formspec_states[name].absvalue = false
return
end
-- Deleting a profile does not require any other field
if fields.delete_np_profile then
if #np_profiles <= 1 then
return
end
local profile_to_delete = tonumber(fields.np_profiles)
if np_profiles[profile_to_delete].can_delete == false then
return
end
table.remove(np_profiles, profile_to_delete)
local new_id = math.max(1, profile_to_delete - 1)
show_noise_formspec(player, default_noiseparams, new_id)
return
end
-- Handle other fields
local enter_pressed = fields.key_enter_field ~= nil
local do_apply = fields.apply ~= nil or fields.toggle_autogen ~= nil
local do_create = fields.create ~= nil
if current_perlin.autogen then
do_apply = do_apply or enter_pressed
else
do_create = do_create or enter_pressed
end
local do_analyze = fields.analyze ~= nil
if (do_create or do_apply or do_analyze or fields.add_np_profile or fields.np_profiles) 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
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)
local nodetype = tonumber(fields.nodetype)
if (offset and scale and spread and octaves and persistence) then
local eased = formspec_states[name].eased
local absvalue = formspec_states[name].absvalue
local noiseparams = {
offset = offset,
scale = scale,
seed = seed,
spread = spread,
octaves = octaves,
persistence = persistence,
lacunarity = lacunarity,
flags = build_flags_string(eased, absvalue),
}
noiseparams = fix_noiseparams(noiseparams)
-- Open analyze window
if do_analyze then
formspec_states[player:get_player_name()].noiseparams = noiseparams
analyze_noiseparams_and_show_formspec(player, noiseparams)
return
-- Change NP profile selection
elseif fields.load_np_profile and fields.np_profiles then
local profile = tonumber(fields.np_profiles)
local loaded_np = np_profiles[profile].noiseparams
-- Load new profile
show_noise_formspec(player, loaded_np, profile)
minetest.log("action", "[perlin_explorer] Loaded perlin noise profile "..profile)
return
-- Add new profile and save current noiseparams to it
elseif fields.add_np_profile then
table.insert(np_profiles, noiseparams)
local new_profile = #np_profiles
minetest.log("action", "[perlin_explorer] Perlin noise profile "..new_profile.." added!")
show_noise_formspec(player, noiseparams, new_profile)
return
elseif fields.set_random_seed then
-- Randomize seed
local profile = tonumber(fields.np_profiles)
noiseparams.seed = math.random(0, 2^32-1)
show_noise_formspec(player, noiseparams, profile)
return
end
if not (dimensions and sidelen and value_min and value_max and nodetype) then
return
end
-- 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
set_perlin_noise(noiseparams)
minetest.log("action", "[perlin_explorer] Perlin noise set!")
current_perlin.dimensions = dimensions
current_perlin.sidelen = sidelen
current_perlin.min = value_min
current_perlin.max = value_max
current_perlin.nodetype = nodetype
if fields.toggle_autogen then
current_perlin.autogen = not current_perlin.autogen
if current_perlin.autogen then
loaded_areas = {}
end
local profile = tonumber(fields.np_profiles)
show_noise_formspec(player, noiseparams, profile)
minetest.log("action", "[perlin_explorer] Autogen state is now: "..tostring(current_perlin.autogen))
elseif do_create then
if not px or not py or not pz or not size then
return
end
if current_perlin.autogen then
loaded_areas = {}
end
current_perlin.size = size
local place_pos = vector.new(px, py, pz)
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
elseif do_apply and current_perlin.autogen then
loaded_areas = {}
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_noise_formspec(user)
end,
})
local timer = 0
minetest.register_globalstep(function(dtime)
timer = timer + dtime
if timer < AUTOBUILD_UPDATE_TIME then
return
end
timer = 0
if current_perlin.noise and current_perlin.autogen then
local player = minetest.get_player_by_name("singleplayer")
if not player then
return
end
local build = function(pos, pos_hash, player_name)
if not pos or not pos.x or not pos.y or not pos.z then
minetest.log("error", "[perlin_explorer] build(): Invalid pos!")
return
end
if not pos_hash then
minetest.log("error", "[perlin_explorer] build(): Invalid pos_hash!")
return
end
if not loaded_areas[pos_hash] then
create_perlin(pos, {
dimensions = current_perlin.dimensions,
size = AUTOBUILD_SIZE,
})
loaded_areas[pos_hash] = true
end
end
local pos = vector.round(player:get_pos())
pos = sidelen_pos(pos, AUTOBUILD_SIZE)
local neighbors = { vector.new(0, 0, 0) }
local c = AUTOBUILD_CHUNKDIST
local cc = c
if current_perlin.dimensions == 2 then
cc = 0
end
for cx=-c, c do
for cy=-cc, cc do
for cz=-c, c do
table.insert(neighbors, vector.new(cx, cy, cz))
end
end
end
local noisechunks = {}
for n=1, #neighbors do
local offset = vector.multiply(neighbors[n], AUTOBUILD_SIZE)
local npos = vector.add(pos, offset)
local hash = minetest.hash_node_position(npos)
if not loaded_areas[hash] then
table.insert(noisechunks, {npos, hash})
end
end
if #noisechunks > 0 then
minetest.log("verbose", "[perlin_explorer] Started building "..#noisechunks.." noisechunk(s)")
end
for c=1, #noisechunks do
local npos = noisechunks[c][1]
local nhash = noisechunks[c][2]
build(npos, nhash, player:get_player_name())
end
if #noisechunks > 0 then
minetest.log("verbose", "[perlin_explorer] Done building "..#noisechunks.." noisechunk(s)")
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)