From 1833e68ec5b54ee4ae16410ced946c8b51bb685c Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Wed, 4 Aug 2021 00:51:16 +0100 Subject: [PATCH 01/17] terrain.lua: add wea.calculate_slope() --- worldeditadditions/utils/terrain.lua | 39 +++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/worldeditadditions/utils/terrain.lua b/worldeditadditions/utils/terrain.lua index 1a9bb44..6d7e84d 100644 --- a/worldeditadditions/utils/terrain.lua +++ b/worldeditadditions/utils/terrain.lua @@ -1,3 +1,4 @@ +local wea = worldeditadditions --- Given a manip object and associates, generates a 2D x/z heightmap. -- Note that pos1 and pos2 should have already been pushed through @@ -20,7 +21,7 @@ function worldeditadditions.make_heightmap(pos1, pos2, manip, area, data) -- Scan each column top to bottom for y = pos2.y+1, pos1.y, -1 do local i = area:index(x, y, z) - if not (worldeditadditions.is_airlike(data[i]) or worldeditadditions.is_liquidlike(data[i])) then + if not (wea.is_airlike(data[i]) or wea.is_liquidlike(data[i])) then -- It's the first non-airlike node in this column -- Start heightmap values from 1 (i.e. there's at least 1 node in the column) heightmap[hi] = (y - pos1.y) + 1 @@ -72,18 +73,42 @@ function worldeditadditions.calculate_normals(heightmap, heightmap_size) -- print("[normals] LEFT | index", z*heightmap_size.x + (x-1), "x", x, "x-1", x - 1, "left", left, "limit", 0) -- print("[normals] RIGHT | index", z*heightmap_size.x + (x+1), "x", x, "x+1", x + 1, "right", right, "limit", heightmap_size.x-1) - result[hi] = worldeditadditions.vector.normalize({ - x = left - right, - y = 2, -- Z & Y are flipped - z = down - up - }) + result[hi] = wea.vector3.new( + left - right, -- x + 2, -- y - Z & Y are flipped + down - up -- z + ):normalise() - -- print("[normals] at "..hi.." ("..x..", "..z..") normal "..worldeditadditions.vector.tostring(result[hi])) + -- print("[normals] at "..hi.." ("..x..", "..z..") normal "..result[hi]) end end return result end +--- Converts a 2d heightmap into slope values in radians. +-- Convert a radians to degrees by doing (radians*math.pi) / 180 for display, +-- but it is STRONGLY recommended to keep all internal calculations in radians. +-- @param heightmap table A ZERO indexed flat heightmap. See worldeditadditions.make_heightmap(). +-- @param heightmap_size int[] The size of the heightmap in the form [ z, x ] +-- @return Vector[] The calculated slope map, in the same form as the input heightmap. Each element of the array is a (floating-point) number representing the slope in that cell in radians. +function worldeditadditions.calculate_slope(heightmap, heightmap_size) + local normals = worldeditadditions.calculate_normals(heightmap, heightmap_size) + local slopes = { } + + local up = wea.vector3(0, 1, 0) -- Z & Y are flipped + + for z = heightmap_size.z-1, 0, -1 do + for x = heightmap_size.x-1, 0, -1 do + local hi = z*heightmap_size.x + x + + -- Ref https://stackoverflow.com/a/16669463/1460422 + slopes[hi] = wea.vector3.dot_product(normals[hi], up) + end + end + + return slopes +end + --- Applies changes to a heightmap to a Voxel Manipulator data block. -- @param pos1 vector Position 1 of the defined region -- @param pos2 vector Position 2 of the defined region From 902d5ddc8ba81446810d3f638f4ce33cae1f28ac Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Wed, 4 Aug 2021 00:53:47 +0100 Subject: [PATCH 02/17] wea.calculate_normals: update comment --- worldeditadditions/utils/terrain.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worldeditadditions/utils/terrain.lua b/worldeditadditions/utils/terrain.lua index 6d7e84d..014c0b3 100644 --- a/worldeditadditions/utils/terrain.lua +++ b/worldeditadditions/utils/terrain.lua @@ -49,7 +49,7 @@ end -- will have the z and y values swapped. -- @param heightmap table A ZERO indexed flat heightmap. See worldeditadditions.make_heightmap(). -- @param heightmap_size int[] The size of the heightmap in the form [ z, x ] --- @return Vector[] The calculated normal map, in the same form as the input heightmap. Each element of the array is a 3D Vector (i.e. { x, y, z }) representing a normal. +-- @return Vector[] The calculated normal map, in the same form as the input heightmap. Each element of the array is a Vector3 instance representing a normal. function worldeditadditions.calculate_normals(heightmap, heightmap_size) -- print("heightmap_size: "..heightmap_size.x.."x"..heightmap_size.z) local result = {} From db830c66333dc7cd8a8dc02fba5dde86f1b16150 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Wed, 4 Aug 2021 01:41:51 +0100 Subject: [PATCH 03/17] Implement slope constraint for //layers, but it isn't tested yet --- CHANGELOG.md | 1 + worldeditadditions/lib/layers.lua | 72 +++++++++++++------ worldeditadditions/utils/terrain.lua | 2 +- worldeditadditions/utils/vector3.lua | 3 +- .../commands/layers.lua | 60 +++++++++++++--- 5 files changed, 105 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8839b..7979d1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Note to self: See the bottom of this file for the release template text. - Add `//airapply` for applying commands only to air nodes in the defined region - Use [luacheck](https://github.com/mpeterv/luacheck) to find and fix a large number of bugs and other issues - Multiple commands: Allow using quotes (`"thing"`, `'thing'`) to quote values when splitting + - Add optional slope constraint to `//layers` (inspired by [WorldPainter](https://worldpainter.net/)) ## v1.12: The selection tools update (26th June 2021) diff --git a/worldeditadditions/lib/layers.lua b/worldeditadditions/lib/layers.lua index 78572b4..e370df3 100644 --- a/worldeditadditions/lib/layers.lua +++ b/worldeditadditions/lib/layers.lua @@ -1,8 +1,21 @@ ---- Overlap command. Places a specified node on top of each column. --- @module worldeditadditions.layers +-- ██ █████ ██ ██ ███████ ██████ ███████ +-- ██ ██ ██ ██ ██ ██ ██ ██ ██ +-- ██ ███████ ████ █████ ██████ ███████ +-- ██ ██ ██ ██ ██ ██ ██ ██ +-- ███████ ██ ██ ██ ███████ ██ ██ ███████ -function worldeditadditions.layers(pos1, pos2, node_weights) - pos1, pos2 = worldedit.sort_pos(pos1, pos2) +local wea = worldeditadditions + +--- Replaces the non-air nodes in each column with a list of nodes from top to bottom. +-- @param pos1 Vector Position 1 of the region to operate on +-- @param pos2 Vector Position 2 of the region to operate on +-- @param node_weights string[] +-- @param min_slope number? +-- @param max_slope number? +function worldeditadditions.layers(pos1, pos2, node_weights, min_slope, max_slope) + pos1, pos2 = wea.vector3.sort(pos1, pos2) + if not min_slope then min_slope = math.rad(0) end + if not max_slope then max_slope = math.rad(180) end -- pos2 will always have the highest co-ordinates now -- Fetch the nodes in the specified area @@ -11,38 +24,53 @@ function worldeditadditions.layers(pos1, pos2, node_weights) local node_id_ignore = minetest.get_content_id("ignore") - local node_ids, node_ids_count = worldeditadditions.unwind_node_list(node_weights) + local node_ids, node_ids_count = wea.unwind_node_list(node_weights) - -- minetest.log("action", "pos1: " .. worldeditadditions.vector.tostring(pos1)) - -- minetest.log("action", "pos2: " .. worldeditadditions.vector.tostring(pos2)) + local heightmap, heightmap_size = wea.make_heightmap( + pos1, pos2, + manip, area, data + ) + local slopemap = wea.calculate_slopes(heightmap, heightmap_size) + --luacheck:ignore 311 + heightmap = nil -- Just in case Lua wants to garbage collect it + + -- minetest.log("action", "pos1: " .. wea.vector.tostring(pos1)) + -- minetest.log("action", "pos2: " .. wea.vector.tostring(pos2)) -- for i,v in ipairs(node_ids) do -- print("[layer] i", i, "node id", v) -- end -- z y x is the preferred loop order, but that isn't really possible here - local changes = { replaced = 0, skipped_columns = 0 } + local changes = { replaced = 0, skipped_columns = 0, skipped_columns_slope = 0 } for z = pos2.z, pos1.z, -1 do for x = pos2.x, pos1.x, -1 do local next_index = 1 -- We use table.insert() in make_weighted local placed_node = false - for y = pos2.y, pos1.y, -1 do - local i = area:index(x, y, z) - - local is_air = worldeditadditions.is_airlike(data[i]) - local is_ignore = data[i] == node_id_ignore - - if not is_air and not is_ignore then - -- It's not an airlike node or something else odd - data[i] = node_ids[next_index] - next_index = next_index + 1 - changes.replaced = changes.replaced + 1 + local hi = z*heightmap_size.x + x + + -- Again, Lua 5.1 doesn't have a continue statement :-/ + if slopemap[hi] >= min_slope and slopemap[hi] <= max_slope then + for y = pos2.y, pos1.y, -1 do + local i = area:index(x, y, z) - -- If we're done replacing nodes in this column, move to the next one - if next_index > #node_ids then - break + local is_air = wea.is_airlike(data[i]) + local is_ignore = data[i] == node_id_ignore + + if not is_air and not is_ignore then + -- It's not an airlike node or something else odd + data[i] = node_ids[next_index] + next_index = next_index + 1 + changes.replaced = changes.replaced + 1 + + -- If we're done replacing nodes in this column, move to the next one + if next_index > #node_ids then + break + end end end + else + changes.skipped_columns_slope = changes.skipped_columns_slope + 1 end if not placed_node then diff --git a/worldeditadditions/utils/terrain.lua b/worldeditadditions/utils/terrain.lua index 014c0b3..49aa9d1 100644 --- a/worldeditadditions/utils/terrain.lua +++ b/worldeditadditions/utils/terrain.lua @@ -91,7 +91,7 @@ end -- @param heightmap table A ZERO indexed flat heightmap. See worldeditadditions.make_heightmap(). -- @param heightmap_size int[] The size of the heightmap in the form [ z, x ] -- @return Vector[] The calculated slope map, in the same form as the input heightmap. Each element of the array is a (floating-point) number representing the slope in that cell in radians. -function worldeditadditions.calculate_slope(heightmap, heightmap_size) +function worldeditadditions.calculate_slopes(heightmap, heightmap_size) local normals = worldeditadditions.calculate_normals(heightmap, heightmap_size) local slopes = { } diff --git a/worldeditadditions/utils/vector3.lua b/worldeditadditions/utils/vector3.lua index 734aabb..6ec8811 100644 --- a/worldeditadditions/utils/vector3.lua +++ b/worldeditadditions/utils/vector3.lua @@ -256,7 +256,8 @@ end --- Sorts the components of the given vectors. -- pos1 will contain the minimum values, and pos2 the maximum values. -- Returns 2 new vectors. --- Note that the vectors provided do not *have* to be instances of Vector3. +-- Note that for this specific function +-- the vectors provided do not *have* to be instances of Vector3. -- It is only required that they have the keys x, y, and z. -- Vector3 instances are always returned. -- This enables convenient ingesting of positions from outside. diff --git a/worldeditadditions_commands/commands/layers.lua b/worldeditadditions_commands/commands/layers.lua index ae09158..9f7a204 100644 --- a/worldeditadditions_commands/commands/layers.lua +++ b/worldeditadditions_commands/commands/layers.lua @@ -1,11 +1,34 @@ +local function parse_slope_range(text) + if string.match(text, "%.%.") then + -- It's in the form a..b + local parts = worldeditadditions.split(text, "..", true) + if not parts then return nil end + if #parts ~= 2 then return false, "Error: Exactly 2 numbers may be separated by a double dot '..' (e.g. 10..45)" end + local min_slope = tonumber(parts[1]) + local max_slope = tonumber(parts[2]) + if not min_slope then return false, "Error: Failed to parse the specified min_slope '"..tostring(min_slope).."' value as a number." end + if not max_slope then return false, "Error: Failed to parse the specified max_slope '"..tostring(max_slope).."' value as a number." end + + -- math.rad converts degrees to radians + return true, math.rad(min_slope), math.rad(max_slope) + else + -- It's a single value + local max_slope = tonumber(text) + if not max_slope then return nil end + + return true, 0, math.rad(max_slope) + end +end + + -- ██████ ██ ██ ███████ ██████ ██ █████ ██ ██ -- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -- ██ ██ ██ ██ █████ ██████ ██ ███████ ████ -- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -- ██████ ████ ███████ ██ ██ ███████ ██ ██ ██ worldedit.register_command("layers", { - params = "[ []] [ []] ...", - description = "Replaces the topmost non-airlike nodes with layers of the given nodes from top to bottom. Like WorldEdit for MC's //naturalize command. Default: dirt_with_grass dirt 3", + params = "[] [ []] [ []] ...", + description = "Replaces the topmost non-airlike nodes with layers of the given nodes from top to bottom. Like WorldEdit for MC's //naturalize command. Optionally takes a maximum or minimum and maximum slope value. If a column's slope value falls outside the defined range, then it's skipped. Default: dirt_with_grass dirt 3", privs = { worldedit = true }, require_pos = 2, parse = function(params_text) @@ -13,21 +36,40 @@ worldedit.register_command("layers", { params_text = "dirt_with_grass dirt 3" end - local success, node_list = worldeditadditions.parse.weighted_nodes( - worldeditadditions.split_shell(params_text), + local parts = worldeditadditions.split_shell(params_text) + local success, min_slope, max_slope + + if #parts > 0 then + success, min_slope, max_slope = parse_slope_range(parts[1]) + if success then + table.remove(parts, 1) -- Automatically shifts other values down + end + end + + if not min_slope then min_slope = 0 end + if not max_slope then max_slope = 180 end + + + local node_list + success, node_list = worldeditadditions.parse.weighted_nodes( + parts, true ) - return success, node_list + return success, node_list, min_slope, max_slope end, nodes_needed = function(name) return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name]) end, - func = function(name, node_list) + func = function(name, node_list, min_slope, max_slope) local start_time = worldeditadditions.get_ms_time() - local changes = worldeditadditions.layers(worldedit.pos1[name], worldedit.pos2[name], node_list) + local changes = worldeditadditions.layers( + worldedit.pos1[name], worldedit.pos2[name], + node_list, + min_slope, max_slope + ) local time_taken = worldeditadditions.get_ms_time() - start_time - minetest.log("action", name .. " used //layers at " .. worldeditadditions.vector.tostring(worldedit.pos1[name]) .. ", replacing " .. changes.replaced .. " nodes and skipping " .. changes.skipped_columns .. " columns in " .. time_taken .. "s") - return true, changes.replaced .. " nodes replaced and " .. changes.skipped_columns .. " columns skipped in " .. worldeditadditions.format.human_time(time_taken) + minetest.log("action", name .. " used //layers at " .. worldeditadditions.vector.tostring(worldedit.pos1[name]) .. ", replacing " .. changes.replaced .. " nodes and skipping " .. changes.skipped_columns .. " columns ("..changes.skipped_columns_slope.." due to slope constraints) in " .. time_taken .. "s") + return true, changes.replaced .. " nodes replaced and " .. changes.skipped_columns .. " columns skipped ("..changes.skipped_columns_slope.." due to slope constraints) in " .. worldeditadditions.format.human_time(time_taken) end }) From ef678e6a05354c50218ce04ae42e3b492cdb501b Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Wed, 4 Aug 2021 12:17:39 +0100 Subject: [PATCH 04/17] //layers: fix --- worldeditadditions/lib/layers.lua | 18 ++++++++++++++++-- worldeditadditions/utils/terrain.lua | 7 ++++--- .../commands/layers.lua | 4 +++- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/worldeditadditions/lib/layers.lua b/worldeditadditions/lib/layers.lua index e370df3..58b12cd 100644 --- a/worldeditadditions/lib/layers.lua +++ b/worldeditadditions/lib/layers.lua @@ -6,6 +6,15 @@ local wea = worldeditadditions +local function print_slopes(slopemap, width) + local copy = wea.table.shallowcopy(slopemap) + for key,value in pairs(copy) do + copy[key] = wea.round(math.deg(value), 2) + end + + worldeditadditions.format.array_2d(copy, width) +end + --- Replaces the non-air nodes in each column with a list of nodes from top to bottom. -- @param pos1 Vector Position 1 of the region to operate on -- @param pos2 Vector Position 2 of the region to operate on @@ -13,7 +22,7 @@ local wea = worldeditadditions -- @param min_slope number? -- @param max_slope number? function worldeditadditions.layers(pos1, pos2, node_weights, min_slope, max_slope) - pos1, pos2 = wea.vector3.sort(pos1, pos2) + pos1, pos2 = wea.Vector3.sort(pos1, pos2) if not min_slope then min_slope = math.rad(0) end if not max_slope then max_slope = math.rad(180) end -- pos2 will always have the highest co-ordinates now @@ -31,9 +40,12 @@ function worldeditadditions.layers(pos1, pos2, node_weights, min_slope, max_slop manip, area, data ) local slopemap = wea.calculate_slopes(heightmap, heightmap_size) + worldeditadditions.format.array_2d(heightmap, heightmap_size.x) + print_slopes(slopemap, heightmap_size.x) --luacheck:ignore 311 heightmap = nil -- Just in case Lua wants to garbage collect it + -- minetest.log("action", "pos1: " .. wea.vector.tostring(pos1)) -- minetest.log("action", "pos2: " .. wea.vector.tostring(pos2)) -- for i,v in ipairs(node_ids) do @@ -47,7 +59,9 @@ function worldeditadditions.layers(pos1, pos2, node_weights, min_slope, max_slop local next_index = 1 -- We use table.insert() in make_weighted local placed_node = false - local hi = z*heightmap_size.x + x + local hi = (z-pos1.z)*heightmap_size.x + (x-pos1.x) + + -- print("DEBUG hi", hi, "x", x, "z", z, "slope", slopemap[hi], "as deg", math.deg(slopemap[hi])) -- Again, Lua 5.1 doesn't have a continue statement :-/ if slopemap[hi] >= min_slope and slopemap[hi] <= max_slope then diff --git a/worldeditadditions/utils/terrain.lua b/worldeditadditions/utils/terrain.lua index 49aa9d1..920496f 100644 --- a/worldeditadditions/utils/terrain.lua +++ b/worldeditadditions/utils/terrain.lua @@ -73,7 +73,7 @@ function worldeditadditions.calculate_normals(heightmap, heightmap_size) -- print("[normals] LEFT | index", z*heightmap_size.x + (x-1), "x", x, "x-1", x - 1, "left", left, "limit", 0) -- print("[normals] RIGHT | index", z*heightmap_size.x + (x+1), "x", x, "x+1", x + 1, "right", right, "limit", heightmap_size.x-1) - result[hi] = wea.vector3.new( + result[hi] = wea.Vector3.new( left - right, -- x 2, -- y - Z & Y are flipped down - up -- z @@ -95,14 +95,15 @@ function worldeditadditions.calculate_slopes(heightmap, heightmap_size) local normals = worldeditadditions.calculate_normals(heightmap, heightmap_size) local slopes = { } - local up = wea.vector3(0, 1, 0) -- Z & Y are flipped + local up = wea.Vector3.new(0, 1, 0) -- Z & Y are flipped for z = heightmap_size.z-1, 0, -1 do for x = heightmap_size.x-1, 0, -1 do local hi = z*heightmap_size.x + x -- Ref https://stackoverflow.com/a/16669463/1460422 - slopes[hi] = wea.vector3.dot_product(normals[hi], up) + -- slopes[hi] = wea.Vector3.dot_product(normals[hi], up) + slopes[hi] = math.acos(normals[hi].y) end end diff --git a/worldeditadditions_commands/commands/layers.lua b/worldeditadditions_commands/commands/layers.lua index 9f7a204..6423af2 100644 --- a/worldeditadditions_commands/commands/layers.lua +++ b/worldeditadditions_commands/commands/layers.lua @@ -49,7 +49,6 @@ worldedit.register_command("layers", { if not min_slope then min_slope = 0 end if not max_slope then max_slope = 180 end - local node_list success, node_list = worldeditadditions.parse.weighted_nodes( parts, @@ -69,6 +68,9 @@ worldedit.register_command("layers", { ) local time_taken = worldeditadditions.get_ms_time() - start_time + print("DEBUG min_slope", min_slope, "max_slope", max_slope) + print("DEBUG min_slope", math.deg(min_slope), "max_slope", math.deg(max_slope)) + minetest.log("action", name .. " used //layers at " .. worldeditadditions.vector.tostring(worldedit.pos1[name]) .. ", replacing " .. changes.replaced .. " nodes and skipping " .. changes.skipped_columns .. " columns ("..changes.skipped_columns_slope.." due to slope constraints) in " .. time_taken .. "s") return true, changes.replaced .. " nodes replaced and " .. changes.skipped_columns .. " columns skipped ("..changes.skipped_columns_slope.." due to slope constraints) in " .. worldeditadditions.format.human_time(time_taken) end From b7c3f2113507c61cc8868c5d558436fea4af3643 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Wed, 4 Aug 2021 12:20:32 +0100 Subject: [PATCH 05/17] reference: add quick todo --- Chat-Command-Reference.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Chat-Command-Reference.md b/Chat-Command-Reference.md index 142343e..6cf3013 100644 --- a/Chat-Command-Reference.md +++ b/Chat-Command-Reference.md @@ -40,10 +40,12 @@ Note also that columns without any air nodes in them at all are also skipped, so //overlay dirt 90% stone 10% ``` -## `//layers [ []] [ []] ...` +## `//layers [] [ []] [ []] ...` Finds the first non-air node in each column and works downwards, replacing non-air nodes with a defined list of nodes in sequence. Like WorldEdit for Minecraft's `//naturalize` command, and also similar to [`we_env`'s `//populate`](https://github.com/sfan5/we_env). Speaking of, this command has `//naturalise` and `//naturalize` as aliases. Defaults to 1 layer of grass followed by 3 layers of dirt. -The list of nodes has a form similar to that of a chance list you might find in `//replacemix`, `//overlay`, or `//mix` - see the examples below. If the numberr of layers isn't specified, `1` is assumed (i.e. a single layer). +The list of nodes has a form similar to that of a chance list you might find in `//replacemix`, `//overlay`, or `//mix` - see the examples below. If the number of layers isn't specified, `1` is assumed (i.e. a single layer). + +TODO UPDATE DOCS HERE ```weacmd //layers dirt_with_grass dirt 3 From 7c7abf4509913de61012462a5a6c0abaffffe222 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Wed, 4 Aug 2021 21:11:47 +0100 Subject: [PATCH 06/17] Queue: document with comments --- worldeditadditions/utils/queue.lua | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/worldeditadditions/utils/queue.lua b/worldeditadditions/utils/queue.lua index af1817f..a00f1e5 100644 --- a/worldeditadditions/utils/queue.lua +++ b/worldeditadditions/utils/queue.lua @@ -1,16 +1,22 @@ ------------------------------------------------------------------------------- ---- A Queue implementation, taken & adapted from https://www.lua.org/pil/11.4.html +--- A Queue implementation +-- Taken & adapted from https://www.lua.org/pil/11.4.html -- @submodule worldeditadditions.utils.queue - +-- @class local Queue = {} Queue.__index = Queue +--- Creates a new queue instance. +-- @returns Queue function Queue.new() local result = { first = 0, last = -1, items = {} } setmetatable(result, Queue) return result end +--- Adds a new value to the end of the queue. +-- @param value any The new value to add to the end of the queue. +-- @returns number The index of the value that was added to the queue. function Queue:enqueue(value) local new_last = self.last + 1 self.last = new_last @@ -18,6 +24,9 @@ function Queue:enqueue(value) return new_last end +--- Determines whether a given value is present in this queue or not. +-- @param value any The value to check. +-- @returns bool Whether the given value exists in the queue or not. function Queue:contains(value) for i=self.first,self.last do if self.items[i] == value then @@ -27,14 +36,22 @@ function Queue:contains(value) return false end +--- Returns whether the queue is empty or not. +-- @returns bool Whether the queue is empty or not. function Queue:is_empty() return self.first > self.last end +--- Removes the item with the given index from the queue. +-- Item indexes do not change as the items in a queue are added and removed. +-- @param number The index of the item to remove from the queue. +-- @returns nil function Queue:remove_index(index) self.items[index] = nil end +--- Dequeues an item from the front of the queue. +-- @returns any|nil Returns the item at the front of the queue, or nil if no items are currently enqueued. function Queue:dequeue() if Queue.is_empty(self) then error("Error: The self is empty!") @@ -53,4 +70,5 @@ function Queue:dequeue() return value end + return Queue From c48e9f2ab81c02fe4850e79413f024aee2ff20c5 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Wed, 4 Aug 2021 21:23:01 +0100 Subject: [PATCH 07/17] Vector3.min, Vector3.max: allow arguments to be numbers --- .tests/Vector3/max.test.lua | 18 ++++++++++++++++++ .tests/Vector3/min.test.lua | 18 ++++++++++++++++++ worldeditadditions/utils/vector3.lua | 20 ++++++++++++++++---- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/.tests/Vector3/max.test.lua b/.tests/Vector3/max.test.lua index f8d3d2a..04f180c 100644 --- a/.tests/Vector3/max.test.lua +++ b/.tests/Vector3/max.test.lua @@ -19,6 +19,24 @@ describe("Vector3.max", function() Vector3.max(a, b) ) end) + it("should work with scalar numbers", function() + local a = Vector3.new(16, 1, 16) + local b = 2 + + assert.are.same( + Vector3.new(16, 2, 16), + Vector3.max(a, b) + ) + end) + it("should work with scalar numbers both ways around", function() + local a = Vector3.new(16, 1, 16) + local b = 2 + + assert.are.same( + Vector3.new(16, 2, 16), + Vector3.max(b, a) + ) + end) it("should work with negative vectors", function() local a = Vector3.new(-9, -16, -25) local b = Vector3.new(-3, -6, -2) diff --git a/.tests/Vector3/min.test.lua b/.tests/Vector3/min.test.lua index 332d74d..307ed79 100644 --- a/.tests/Vector3/min.test.lua +++ b/.tests/Vector3/min.test.lua @@ -19,6 +19,24 @@ describe("Vector3.min", function() Vector3.min(a, b) ) end) + it("should work with scalar numbers", function() + local a = Vector3.new(16, 1, 16) + local b = 2 + + assert.are.same( + Vector3.new(2, 1, 2), + Vector3.min(a, b) + ) + end) + it("should work with scalar numbers both ways around", function() + local a = Vector3.new(16, 1, 16) + local b = 2 + + assert.are.same( + Vector3.new(2, 1, 2), + Vector3.min(b, a) + ) + end) it("should work with negative vectors", function() local a = Vector3.new(-9, -16, -25) local b = Vector3.new(-3, -6, -2) diff --git a/worldeditadditions/utils/vector3.lua b/worldeditadditions/utils/vector3.lua index 6ec8811..5733279 100644 --- a/worldeditadditions/utils/vector3.lua +++ b/worldeditadditions/utils/vector3.lua @@ -317,8 +317,8 @@ end --- Returns the mean (average) of 2 positions. -- In other words, returns the centre of 2 points. --- @param pos1 Vector3 pos1 of the defined region. --- @param pos2 Vector3 pos2 of the defined region. +-- @param pos1 Vector3|number pos1 of the defined region. +-- @param pos2 Vector3|number pos2 of the defined region. -- @param target Vector3 Centre coordinates. function Vector3.mean(pos1, pos2) return (pos1 + pos2) / 2 @@ -326,10 +326,16 @@ end --- Returns a vector of the min components of 2 vectors. --- @param pos1 Vector3 The first vector to operate on. --- @param pos2 Vector3 The second vector to operate on. +-- @param pos1 Vector3|number The first vector to operate on. +-- @param pos2 Vector3|number The second vector to operate on. -- @return Vector3 The minimum values from the input vectors function Vector3.min(pos1, pos2) + if type(pos1) == "number" then + pos1 = Vector3.new(pos1, pos1, pos1) + end + if type(pos2) == "number" then + pos2 = Vector3.new(pos2, pos2, pos2) + end return Vector3.new( math.min(pos1.x, pos2.x), math.min(pos1.y, pos2.y), @@ -342,6 +348,12 @@ end -- @param pos2 Vector3 The second vector to operate on. -- @return Vector3 The maximum values from the input vectors. function Vector3.max(pos1, pos2) + if type(pos1) == "number" then + pos1 = Vector3.new(pos1, pos1, pos1) + end + if type(pos2) == "number" then + pos2 = Vector3.new(pos2, pos2, pos2) + end return Vector3.new( math.max(pos1.x, pos2.x), math.max(pos1.y, pos2.y), From 5d27ba9ccb3f0099a5a49d18f332293d21d536a4 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Wed, 4 Aug 2021 21:24:36 +0100 Subject: [PATCH 08/17] Vector3.new: add test to make sure Vector3.new() returns (0, 0, 0) --- .tests/Vector3/new.test.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.tests/Vector3/new.test.lua b/.tests/Vector3/new.test.lua index 54a5c72..c92e9d5 100644 --- a/.tests/Vector3/new.test.lua +++ b/.tests/Vector3/new.test.lua @@ -7,6 +7,12 @@ describe("Vector3.add", function() Vector3.new(3, 4, 5) ) end) + it("should default to (0, 0, 0)", function() + assert.are.same( + { x = 0, y = 0, z = 0 }, + Vector3.new() + ) + end) it("should not throw an error on invalid x", function() assert.has_no.errors(function() Vector3.new("cheese", 4, 5) From 3338a3fddf7083a408afd64a4ae8c819e1044de2 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Thu, 5 Aug 2021 00:57:22 +0100 Subject: [PATCH 09/17] Reference: Document new extension to //layers --- Chat-Command-Reference.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Chat-Command-Reference.md b/Chat-Command-Reference.md index 6cf3013..cd8da59 100644 --- a/Chat-Command-Reference.md +++ b/Chat-Command-Reference.md @@ -43,15 +43,18 @@ Note also that columns without any air nodes in them at all are also skipped, so ## `//layers [] [ []] [ []] ...` Finds the first non-air node in each column and works downwards, replacing non-air nodes with a defined list of nodes in sequence. Like WorldEdit for Minecraft's `//naturalize` command, and also similar to [`we_env`'s `//populate`](https://github.com/sfan5/we_env). Speaking of, this command has `//naturalise` and `//naturalize` as aliases. Defaults to 1 layer of grass followed by 3 layers of dirt. +A maximum and minimum slope is optional, and constrains the columns in the defined region that `//layers` will operate on. For example, specifying a value of `20` would mean that only columns with a slop less than or equal to 20° (degrees, not radians) will be operated on. A value of `45..60` would mean that only columns with a slope between 45° and 60° will be operated on. + The list of nodes has a form similar to that of a chance list you might find in `//replacemix`, `//overlay`, or `//mix` - see the examples below. If the number of layers isn't specified, `1` is assumed (i.e. a single layer). -TODO UPDATE DOCS HERE ```weacmd //layers dirt_with_grass dirt 3 //layers sand 5 sandstone 4 desert_stone 2 //layers brick stone 3 //layers cobble 2 dirt +//layers 45..60 dirt_with_snow dirt 2 +//layers 30 snowblock dirt_with_snow dirt 2 ``` ## `//forest [] [] [] [ []] ...` From 87f84e248281279d0cf6d5c3222cc233f844c9a8 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Thu, 5 Aug 2021 01:17:43 +0100 Subject: [PATCH 10/17] //bonmeal: add optional node list constraint --- CHANGELOG.md | 3 +- Chat-Command-Reference.md | 7 +++- worldeditadditions/lib/bonemeal.lua | 20 +++++++++-- .../commands/bonemeal.lua | 33 +++++++++++++------ 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7979d1f..d5307ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ Note to self: See the bottom of this file for the release template text. - Add `//airapply` for applying commands only to air nodes in the defined region - Use [luacheck](https://github.com/mpeterv/luacheck) to find and fix a large number of bugs and other issues - Multiple commands: Allow using quotes (`"thing"`, `'thing'`) to quote values when splitting - - Add optional slope constraint to `//layers` (inspired by [WorldPainter](https://worldpainter.net/)) + - `//layers`: Add optional slope constraint (inspired by [WorldPainter](https://worldpainter.net/)) + - `//bonemeal`: Add optional node list contraint ## v1.12: The selection tools update (26th June 2021) diff --git a/Chat-Command-Reference.md b/Chat-Command-Reference.md index cd8da59..2c7f8fb 100644 --- a/Chat-Command-Reference.md +++ b/Chat-Command-Reference.md @@ -225,7 +225,7 @@ Additional examples: //maze3d stone 6 3 3 54321 ``` -## `//bonemeal [ []]` +## `//bonemeal [ [ [ [ ...]]]]` Requires the [`bonemeal`](https://content.minetest.net/packages/TenPlus1/bonemeal/) ([repo](https://notabug.org/TenPlus1/bonemeal/)) mod (otherwise _WorldEditAdditions_ will not register this command and output a message to the server log). Alias: `//flora`. Bonemeals all eligible nodes in the current region. An eligible node is one that has an air node directly above it - note that just because a node is eligible doesn't mean to say that something will actually happen when the `bonemeal` mod bonemeals it. @@ -240,6 +240,9 @@ For example, a chance number of 2 would mean a 50% chance that any given eligibl Since WorldEditAdditions v1.12, a percentage chance is also supported. This is denoted by suffixing a number with a percent sign (e.g. `//bonemeal 1 25%`). +Since WorldEditAdditions v1.13, a list of node names is also optionally supported. This will constrain bonemeal operations to be performed only on the node names listed. + + ```weacmd //bonemeal //bonemeal 3 25 @@ -247,6 +250,8 @@ Since WorldEditAdditions v1.12, a percentage chance is also supported. This is d //bonemeal 1 10 //bonemeal 2 15 //bonemeal 2 10% +//bonemeal 2 10% dirt +//bonemeal 4 50 ethereal:grove_dirt ``` ## `//walls ` diff --git a/worldeditadditions/lib/bonemeal.lua b/worldeditadditions/lib/bonemeal.lua index 1b7fc7c..31e4682 100644 --- a/worldeditadditions/lib/bonemeal.lua +++ b/worldeditadditions/lib/bonemeal.lua @@ -4,7 +4,8 @@ -- strength The strength to apply - see bonemeal:on_use -- chance Positive integer that represents the chance bonemealing will occur -function worldeditadditions.bonemeal(pos1, pos2, strength, chance) +function worldeditadditions.bonemeal(pos1, pos2, strength, chance, nodename_list) + if not nodename_list then nodename_list = {} end pos1, pos2 = worldedit.sort_pos(pos1, pos2) -- pos2 will always have the highest co-ordinates now @@ -14,6 +15,12 @@ function worldeditadditions.bonemeal(pos1, pos2, strength, chance) return false, "Bonemeal mod not loaded" end + local node_list = worldeditadditions.table.map(nodename_list, function(nodename) + return minetest.get_content_id(nodename) + end) + local node_list_count = #nodename_list + + -- Fetch the nodes in the specified area local manip, area = worldedit.manip_helpers.init(pos1, pos2) local data = manip:get_data() @@ -26,10 +33,17 @@ function worldeditadditions.bonemeal(pos1, pos2, strength, chance) for z = pos2.z, pos1.z, -1 do for x = pos2.x, pos1.x, -1 do for y = pos2.y, pos1.y, -1 do - if not worldeditadditions.is_airlike(data[area:index(x, y, z)]) then + local i = area:index(x, y, z) + if not worldeditadditions.is_airlike(data[i]) then + local should_bonemeal = true + if node_list_count > 0 and not worldeditadditions.table.contains(node_list, data[i]) then + should_bonemeal = false + end + + -- It's not an air node, so let's try to bonemeal it - if math.random(0, chance - 1) == 0 then + if should_bonemeal and math.random(0, chance - 1) == 0 then bonemeal:on_use( { x = x, y = y, z = z }, strength, diff --git a/worldeditadditions_commands/commands/bonemeal.lua b/worldeditadditions_commands/commands/bonemeal.lua index 505ae99..651aaab 100644 --- a/worldeditadditions_commands/commands/bonemeal.lua +++ b/worldeditadditions_commands/commands/bonemeal.lua @@ -6,7 +6,7 @@ local we_c = worldeditadditions_commands -- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -- ██████ ██████ ██ ████ ███████ ██ ██ ███████ ██ ██ ███████ worldedit.register_command("bonemeal", { - params = "[ []]", + params = "[ [ [ [ ...]]]]", description = "Bonemeals everything that's bonemeal-able that has an air node directly above it. Optionally takes a strength value to use (default: 1, maximum: 4), and a chance to actually bonemeal an eligible node (positive integer; nodes have a 1-in- chance to be bonemealed; higher values mean a lower chance; default: 1 - 100% chance).", privs = { worldedit = true }, require_pos = 2, @@ -19,15 +19,16 @@ worldedit.register_command("bonemeal", { local strength = 1 local chance = 1 + local node_names = {} -- An empty table means all nodes if #parts >= 1 then - strength = tonumber(parts[1]) + strength = tonumber(table.remove(parts, 1)) if not strength then return false, "Invalid strength value (value must be an integer)" end end if #parts >= 2 then - chance = worldeditadditions.parse.chance(parts[2]) + chance = worldeditadditions.parse.chance(table.remove(parts, 1)) if not chance then return false, "Invalid chance value (must be a positive integer)" end @@ -37,21 +38,33 @@ worldedit.register_command("bonemeal", { return false, "Error: strength value out of bounds (value must be an integer between 1 and 4 inclusive)" end + + if #parts > 0 then + for _,nodename in pairs(parts) do + local normalised = worldedit.normalize_nodename(nodename) + if not normalised then return false, "Error: Unknown node name '"..nodename.."'." end + table.insert(node_names, normalised) + end + end + -- We unconditionally math.floor here because when we tried to test for it directly it was unreliable - return true, math.floor(strength), math.floor(chance) + return true, math.floor(strength), math.floor(chance), node_names end, nodes_needed = function(name) -- strength, chance -- Since every node has to have an air block, in the best-case scenario -- edit only half the nodes in the selected area return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name]) / 2 end, - func = function(name, strength, chance) + func = function(name, strength, chance, node_names) local start_time = worldeditadditions.get_ms_time() - local success, nodes_bonemealed, candidates = worldeditadditions.bonemeal(worldedit.pos1[name], worldedit.pos2[name], strength, chance) - if not success then - -- nodes_bonemealed is an error message here because success == false - return success, nodes_bonemealed - end + local success, nodes_bonemealed, candidates = worldeditadditions.bonemeal( + worldedit.pos1[name], worldedit.pos2[name], + strength, chance, + node_names + ) + -- nodes_bonemealed is an error message here if success == false + if not success then return success, nodes_bonemealed end + local percentage = worldeditadditions.round((nodes_bonemealed / candidates)*100, 2) local time_taken = worldeditadditions.get_ms_time() - start_time -- Avoid nan% - since if there aren't any candidates then nodes_bonemealed will be 0 too From 7e008df540a2dab37f74dccba1f96191a90574a5 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Thu, 5 Aug 2021 01:18:27 +0100 Subject: [PATCH 11/17] Reference: tweak //layers explanation --- Chat-Command-Reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Chat-Command-Reference.md b/Chat-Command-Reference.md index 2c7f8fb..4a57f26 100644 --- a/Chat-Command-Reference.md +++ b/Chat-Command-Reference.md @@ -43,7 +43,7 @@ Note also that columns without any air nodes in them at all are also skipped, so ## `//layers [] [ []] [ []] ...` Finds the first non-air node in each column and works downwards, replacing non-air nodes with a defined list of nodes in sequence. Like WorldEdit for Minecraft's `//naturalize` command, and also similar to [`we_env`'s `//populate`](https://github.com/sfan5/we_env). Speaking of, this command has `//naturalise` and `//naturalize` as aliases. Defaults to 1 layer of grass followed by 3 layers of dirt. -A maximum and minimum slope is optional, and constrains the columns in the defined region that `//layers` will operate on. For example, specifying a value of `20` would mean that only columns with a slop less than or equal to 20° (degrees, not radians) will be operated on. A value of `45..60` would mean that only columns with a slope between 45° and 60° will be operated on. +Since WorldEditAdditions v1.13, a maximum and minimum slope is optionally accepted, and constrains the columns in the defined region that `//layers` will operate on. For example, specifying a value of `20` would mean that only columns with a slop less than or equal to 20° (degrees, not radians) will be operated on. A value of `45..60` would mean that only columns with a slope between 45° and 60° will be operated on. The list of nodes has a form similar to that of a chance list you might find in `//replacemix`, `//overlay`, or `//mix` - see the examples below. If the number of layers isn't specified, `1` is assumed (i.e. a single layer). From c2702a8fdec8fc2e120945e688d9d0081d68c7fe Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Thu, 5 Aug 2021 01:32:44 +0100 Subject: [PATCH 12/17] Website/reference: Update search to also serch command list --- .docs/Reference.html | 46 ++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/.docs/Reference.html b/.docs/Reference.html index e9004a1..6d3dd9a 100644 --- a/.docs/Reference.html +++ b/.docs/Reference.html @@ -11,21 +11,6 @@

After the contents, there is a filter box for filtering the detailed explanations to quickly find the one you're after.

-
-

- 🔗 - Contents -

-

TODO: Group commands here by category (*especially* the meta commands)

- -
-
@@ -37,6 +22,21 @@
+
+

+ 🔗 + Contents +

+

TODO: Group commands here by category (*especially* the meta commands)

+ +
+