1443 lines
47 KiB
Lua
1443 lines
47 KiB
Lua
-- 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 = 16
|
||
-- 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 = {x=pos.x, y=pos.y, z=pos.z}
|
||
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>, <bad_wavelength>
|
||
-- 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
|
||
-- bad_wavelength = true if any wavelength is lower than 1
|
||
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
|
||
-- Add offset and scale to min/max value (final step)
|
||
local min_value = np.offset + np.scale * o_min
|
||
local max_value = np.offset + np.scale * o_max
|
||
|
||
-- Bring the 2 values in the correct order
|
||
-- (min_value might be bigger for negative scale)
|
||
if min_value > max_value then
|
||
min_value, max_value = max_value, min_value
|
||
end
|
||
|
||
local bad_wavelength = false
|
||
-- 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
|
||
if wave < 1 then
|
||
bad_wavelength = true
|
||
end
|
||
table.insert(waves[w], wave)
|
||
wave = wave * (1 / np.lacunarity)
|
||
end
|
||
end
|
||
return min_value, max_value, waves, bad_wavelength
|
||
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
|
||
error("[perlin_explorer] Unknown/invalid number of Perlin noise dimensions!")
|
||
return
|
||
end
|
||
local msg
|
||
local color_node = minetest.get_color_escape_sequence("#FFD47CFF")
|
||
local color_player = minetest.get_color_escape_sequence("#87FF87FF")
|
||
local color_end = minetest.get_color_escape_sequence("#FFFFFFFF")
|
||
if ptype == "node" then
|
||
msg = S("Value at @1node@2 pos @3: @4", color_node, color_end, minetest.pos_to_string(pos, precision), val)
|
||
elseif ptype == "player" then
|
||
msg = S("Value at @1player@2 pos @3: @4", color_player, color_end, 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 time1 = minetest.get_us_time()
|
||
|
||
local size_v = {
|
||
x = current_perlin.size,
|
||
y = current_perlin.size,
|
||
z = 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
|
||
size_v.z = nil
|
||
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
|
||
|
||
-- Initialize Perlin map
|
||
local perlin_map_object = PerlinNoiseMap(current_perlin.noiseparams, size_v)
|
||
local perlin_map
|
||
if current_perlin.dimensions == 2 then
|
||
perlin_map = perlin_map_object:get_2d_map({x=startpos.x, y=startpos.z})
|
||
else
|
||
perlin_map = perlin_map_object:get_3d_map(startpos)
|
||
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
|
||
-- Note: This section has been optimized. Don’t introduce stuff like
|
||
-- vectors.new() here.
|
||
local relpos = {x=x,y=y,z=z}
|
||
local abspos = {
|
||
x=startpos.x+relpos.x,
|
||
y=startpos.y+relpos.y,
|
||
z=startpos.z+relpos.z,
|
||
}
|
||
-- Apply sidelen transformation (pixelize)
|
||
local abspos_get = sidelen_pos(abspos, current_perlin.sidelen)
|
||
local indexpos = {
|
||
x = abspos_get.x - startpos.x + 1,
|
||
y = abspos_get.y - startpos.y + 1,
|
||
z = abspos_get.z - startpos.z + 1,
|
||
}
|
||
|
||
local perlin_value
|
||
if current_perlin.dimensions == 2 then
|
||
if indexpos.x < 1 or indexpos.z < 1 then
|
||
-- The pixelization can move indexpos below 1, in this case
|
||
-- we get the perlin value directly because it is outside
|
||
-- the precalculated map. Performance impact is hopefully
|
||
-- not too bad because this will only occur at the low
|
||
-- edges.
|
||
-- Ideally, for cleaner code, the precalculated map would
|
||
-- take this into account as well but this has not
|
||
-- been done yet.
|
||
perlin_value = current_perlin.noise:get_2d({x=abspos_get.x, y=abspos_get.z})
|
||
else
|
||
-- Normal case: Get value from perlin map
|
||
perlin_value = perlin_map[indexpos.z][indexpos.x]
|
||
end
|
||
elseif current_perlin.dimensions == 3 then
|
||
if indexpos.x < 1 or indexpos.y < 1 or indexpos.z < 1 then
|
||
-- See above
|
||
perlin_value = current_perlin.noise:get_3d(abspos_get)
|
||
else
|
||
-- See above
|
||
perlin_value = perlin_map[indexpos.z][indexpos.y][indexpos.x]
|
||
end
|
||
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
|
||
|
||
if set_nodes then
|
||
-- 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
|
||
|
||
-- 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
|
||
|
||
local time2 = minetest.get_us_time()
|
||
local timediff = time2 - time1
|
||
minetest.log("verbose", "[perlin_explorer] Noisechunk calculated/generated in "..timediff.." µs")
|
||
|
||
|
||
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_error_formspec = function(player, message)
|
||
local form = [[
|
||
formspec_version[4]size[10,3.5]
|
||
container[0.25,0.25]
|
||
box[0,0;9.5,2;]]..FORMSPEC_BOX_COLOR..[[]
|
||
box[0,0;9.5,0.4;]]..FORMSPEC_HEADER_COLOR..[[]
|
||
label[0.25,0.2;]]..F(S("Error"))..[[]
|
||
container[0.25,0.5]
|
||
textarea[0,0;9,1.4;;]]..F(message)..[[;]
|
||
container_end[]
|
||
container_end[]
|
||
button[1.5,2.5;3,0.75;done;]]..F(S("Return"))..[[]
|
||
button[5.5,2.5;3,0.75;analyze;]]..F(S("Analyze now"))..[[]
|
||
]]
|
||
minetest.show_formspec(player:get_player_name(), "perlin_explorer:error", form)
|
||
end
|
||
|
||
local show_histogram_loading_formspec = function(player)
|
||
local form = [[
|
||
formspec_version[4]size[11,2]
|
||
container[0.25,0.25]
|
||
box[0,0;10.5,1.5;]]..FORMSPEC_BOX_COLOR..[[]
|
||
box[0,0;10.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 histogram
|
||
histogram = "vertlabel[0.1,0.1;"..F(S("Relative frequency")).."]"..
|
||
"label[1.1,8.15;"..F(S("Noise values")).."]"..
|
||
"label[0,7.0;"..F(S("Max.")).."\n"..F(S("Min.")).."]"..
|
||
"tooltip[0,6.8;0.7,1;"..F(S("Upper (Max.) and lower (Min.) bounds of the noise value data bucketing")).."]"..
|
||
"box[0.75,0;0.025,8.3;#FFFFFF80]"..
|
||
"box[0,6.65;10.0,0.025;#FFFFFF80]"..
|
||
"box[0,7.8;10.0,0.025;#FFFFFF80]"
|
||
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.."]"
|
||
|
||
histogram = histogram .. 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 = "textarea[0,0;10,2;;;"..
|
||
F(S("Values were picked between coordinates @1 and @2.", vmin, vmax)).."\n"..
|
||
F(S("Values calculated: @1", stats.value_count)).."\n"..
|
||
F(S("Minimum calculated value: @1", stats.min)).."\n"..
|
||
F(S("Maximum calculated value: @1", stats.max)).."\n"..
|
||
F(S("Average calculated value: @1", stats.avg)).."]\n"
|
||
|
||
local form = [[
|
||
formspec_version[4]size[11,12.5]
|
||
container[0.25,0.25]
|
||
box[0,0;10.5,2.5;]]..FORMSPEC_BOX_COLOR..[[]
|
||
box[0,0;10.5,0.4;]]..FORMSPEC_HEADER_COLOR..[[]
|
||
label[0.25,0.2;]]..F(S("Noise Value Statistics"))..[[]
|
||
container[0.25,0.5]
|
||
]]..labels..[[
|
||
container_end[]
|
||
container[0,2.75]
|
||
box[0,0;10.5,9.0;]]..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.6]
|
||
]]..histogram..[[
|
||
container_end[]
|
||
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,0.75;]]..F(S("Minimum possible value: @1", min))..[[]
|
||
label[0.25,1.25;]]..F(S("Maximum possible value: @1", max))..[[]
|
||
|
||
label[0.25,1.75;]]..F(S("X wavelengths: @1", print_waves(waves.x)))..[[]
|
||
label[0.25,2.25;]]..F(S("Y wavelengths: @1", print_waves(waves.y)))..[[]
|
||
label[0.25,2.75;]]..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" or formname == "perlin_explorer:error" then
|
||
if fields.done then
|
||
local noiseparams = formspec_states[player:get_player_name()].noiseparams
|
||
show_noise_formspec(player, noiseparams)
|
||
elseif fields.analyze then
|
||
local noiseparams = formspec_states[player:get_player_name()].noiseparams
|
||
analyze_noiseparams_and_show_formspec(player, noiseparams)
|
||
end
|
||
return
|
||
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
|
||
local _, _, _, badwaves = analyze_noiseparams(noiseparams)
|
||
if badwaves then
|
||
formspec_states[player:get_player_name()].noiseparams = noiseparams
|
||
show_error_formspec(player, S("Bad noise parameters given. At least one of the resulting wave lengths got below 1, which is not allowed. Decrease the octaves or lacunarity to reach valid wave lengths. Analyze the noise parameters to see the current wave lengths."))
|
||
return
|
||
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)
|
||
-- Check of position is still within the valid map
|
||
local node = minetest.get_node_or_nil(npos)
|
||
if node ~= nil then
|
||
local hash = minetest.hash_node_position(npos)
|
||
if not loaded_areas[hash] then
|
||
table.insert(noisechunks, {npos, hash})
|
||
end
|
||
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)
|