Merge branch 'main' into VorTechnix

This commit is contained in:
VorTechnix 2021-08-05 10:43:00 -07:00
commit cff58792bf
17 changed files with 397 additions and 96 deletions

@ -11,21 +11,6 @@
<p>After the contents, there is a <a href="#filter">filter box</a> for filtering the detailed explanations to quickly find the one you're after.</p> <p>After the contents, there is a <a href="#filter">filter box</a> for filtering the detailed explanations to quickly find the one you're after.</p>
</section> </section>
<section class="panel-generic">
<h2 id="contents" class="linked-section-heading">
<a class="section-link" href="#{{ section.slug }}">&#x1f517; <!-- Link Symbol --></a>
<span>Contents</span>
</h2>
<p>TODO: Group commands here by category (*especially* the meta commands)</p>
<ul class="command-list">
{% for section in sections_help %}
<li><a href="#{{ section.slug }}">
<code>{{ section.title }}</code>
</a></li>
{% endfor %}
</ul>
</section>
<section id="filter" class="panel-generic"> <section id="filter" class="panel-generic">
<div class="form-item bigsearch"> <div class="form-item bigsearch">
<label for="input-filter">Filter:</label> <label for="input-filter">Filter:</label>
@ -37,6 +22,21 @@
</div> </div>
</section> </section>
<section class="panel-generic">
<h2 id="contents" class="linked-section-heading">
<a class="section-link" href="#{{ section.slug }}">&#x1f517; <!-- Link Symbol --></a>
<span>Contents</span>
</h2>
<p>TODO: Group commands here by category (*especially* the meta commands)</p>
<ul class="command-list">
{% for section in sections_help %}
<li data-filtermode-force="all"><a href="#{{ section.slug }}">
<code>{{ section.title }}</code>
</a></li>
{% endfor %}
</ul>
</section>
<script> <script>
function search_text(query, text) { function search_text(query, text) {
return text.toLocaleLowerCase().includes(query); return text.toLocaleLowerCase().includes(query);
@ -45,19 +45,27 @@
function do_filter() { function do_filter() {
let el_search = document.querySelector("#input-filter"); let el_search = document.querySelector("#input-filter");
let el_searchall = document.querySelector("#input-searchall"); let el_searchall = document.querySelector("#input-searchall");
let els_sections = document.querySelectorAll("section.filterable"); /* Filterable items
- Sections
- Commands in the command list
*/
let els_filterable = document.querySelectorAll("section.filterable, .command-list > li");
let query = el_search.value.toLocaleLowerCase(); let query = el_search.value.toLocaleLowerCase();
let mode = el_searchall.checked ? "all" : "header"; let mode = el_searchall.checked ? "all" : "header";
console.log(`SEARCH | mode`, mode, `query`, query); console.log(`SEARCH | mode`, mode, `query`, query);
for(let i = 0; i < els_sections.length; i++) { for(let i = 0; i < els_filterable.length; i++) {
let el_next = els_sections[i]; let el_next = els_filterable[i];
let mode_this = mode;
if(typeof el_next.dataset.filtermodeForce == "string")
mode_this = el_next.dataset.filtermodeForce;
let show = true; let show = true;
if(query.length > 0) { if(query.length > 0) {
switch(mode) { switch(mode_this) {
case "all": case "all":
show = search_text(query, show = search_text(query,
el_next.textContent el_next.textContent

@ -19,6 +19,24 @@ describe("Vector3.max", function()
Vector3.max(a, b) Vector3.max(a, b)
) )
end) 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() it("should work with negative vectors", function()
local a = Vector3.new(-9, -16, -25) local a = Vector3.new(-9, -16, -25)
local b = Vector3.new(-3, -6, -2) local b = Vector3.new(-3, -6, -2)

@ -19,6 +19,24 @@ describe("Vector3.min", function()
Vector3.min(a, b) Vector3.min(a, b)
) )
end) 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() it("should work with negative vectors", function()
local a = Vector3.new(-9, -16, -25) local a = Vector3.new(-9, -16, -25)
local b = Vector3.new(-3, -6, -2) local b = Vector3.new(-3, -6, -2)

@ -7,6 +7,12 @@ describe("Vector3.add", function()
Vector3.new(3, 4, 5) Vector3.new(3, 4, 5)
) )
end) 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() it("should not throw an error on invalid x", function()
assert.has_no.errors(function() assert.has_no.errors(function()
Vector3.new("cheese", 4, 5) Vector3.new("cheese", 4, 5)

@ -12,6 +12,9 @@ Note to self: See the bottom of this file for the release template text.
- Add `//sshift` (_selection shift_) - WorldEdit cuboid manipulator replacements implemented by @VorTechnix. - Add `//sshift` (_selection shift_) - WorldEdit cuboid manipulator replacements implemented by @VorTechnix.
- Use [luacheck](https://github.com/mpeterv/luacheck) to find and fix a large number of bugs and other issues - 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 - Multiple commands: Allow using quotes (`"thing"`, `'thing'`) to quote values when splitting
- `//layers`: Add optional slope constraint (inspired by [WorldPainter](https://worldpainter.net/))
- `//bonemeal`: Add optional node list constraint
- `//walls`: Add optional thickness argument
## v1.12: The selection tools update (26th June 2021) ## v1.12: The selection tools update (26th June 2021)

@ -40,16 +40,21 @@ Note also that columns without any air nodes in them at all are also skipped, so
//overlay dirt 90% stone 10% //overlay dirt 90% stone 10%
``` ```
## `//layers [<node_name_1> [<layer_count_1>]] [<node_name_2> [<layer_count_2>]] ...` ## `//layers [<max_slope|min_slope..max_slope>] [<node_name_1> [<layer_count_1>]] [<node_name_2> [<layer_count_2>]] ...`
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. 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). 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).
```weacmd ```weacmd
//layers dirt_with_grass dirt 3 //layers dirt_with_grass dirt 3
//layers sand 5 sandstone 4 desert_stone 2 //layers sand 5 sandstone 4 desert_stone 2
//layers brick stone 3 //layers brick stone 3
//layers cobble 2 dirt //layers cobble 2 dirt
//layers 45..60 dirt_with_snow dirt 2
//layers 30 snowblock dirt_with_snow dirt 2
``` ```
## `//forest [<density>] <sapling_a> [<chance_a>] <sapling_b> [<chance_b>] [<sapling_N> [<chance_N>]] ...` ## `//forest [<density>] <sapling_a> [<chance_a>] <sapling_b> [<chance_b>] [<sapling_N> [<chance_N>]] ...`
@ -220,7 +225,7 @@ Additional examples:
//maze3d stone 6 3 3 54321 //maze3d stone 6 3 3 54321
``` ```
## `//bonemeal [<strength> [<chance>]]` ## `//bonemeal [<strength> [<chance> [<node_name> [<node_name> ...]]]]`
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`. 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. 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.
@ -235,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.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 ```weacmd
//bonemeal //bonemeal
//bonemeal 3 25 //bonemeal 3 25
@ -242,15 +250,19 @@ Since WorldEditAdditions v1.12, a percentage chance is also supported. This is d
//bonemeal 1 10 //bonemeal 1 10
//bonemeal 2 15 //bonemeal 2 15
//bonemeal 2 10% //bonemeal 2 10%
//bonemeal 2 10% dirt
//bonemeal 4 50 ethereal:grove_dirt
``` ```
## `//walls <replace_node>` ## `//walls <replace_node> [<thickness=1>]`
Creates vertical walls of `<replace_node>` around the inside edges of the defined region. Creates vertical walls of `<replace_node>` around the inside edges of the defined region, optionally specifying the thickness thereof.
```weacmd ```weacmd
//walls dirt //walls dirt
//walls stone //walls stone
//walls goldblock //walls goldblock
//walls sandstone 2
//walls glass 4
``` ```
## `//wbox <replace_node>` ## `//wbox <replace_node>`

@ -16,6 +16,7 @@ wea.Mesh, wea.Face = dofile(wea.modpath.."/utils/mesh.lua")
wea.Queue = dofile(wea.modpath.."/utils/queue.lua") wea.Queue = dofile(wea.modpath.."/utils/queue.lua")
wea.LRU = dofile(wea.modpath.."/utils/lru.lua") wea.LRU = dofile(wea.modpath.."/utils/lru.lua")
wea.inspect = dofile(wea.modpath.."/utils/inspect.lua")
wea.bit = dofile(wea.modpath.."/utils/bit.lua") wea.bit = dofile(wea.modpath.."/utils/bit.lua")

@ -4,7 +4,8 @@
-- strength The strength to apply - see bonemeal:on_use -- strength The strength to apply - see bonemeal:on_use
-- chance Positive integer that represents the chance bonemealing will occur -- 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) pos1, pos2 = worldedit.sort_pos(pos1, pos2)
-- pos2 will always have the highest co-ordinates now -- 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" return false, "Bonemeal mod not loaded"
end 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 -- Fetch the nodes in the specified area
local manip, area = worldedit.manip_helpers.init(pos1, pos2) local manip, area = worldedit.manip_helpers.init(pos1, pos2)
local data = manip:get_data() 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 z = pos2.z, pos1.z, -1 do
for x = pos2.x, pos1.x, -1 do for x = pos2.x, pos1.x, -1 do
for y = pos2.y, pos1.y, -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 -- 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( bonemeal:on_use(
{ x = x, y = y, z = z }, { x = x, y = y, z = z },
strength, strength,

@ -1,8 +1,30 @@
--- Overlap command. Places a specified node on top of each column. -- ██ █████ ██ ██ ███████ ██████ ███████
-- @module worldeditadditions.layers -- ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ██ ███████ ████ █████ ██████ ███████
-- ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██ ██ ██ ███████ ██ ██ ███████
function worldeditadditions.layers(pos1, pos2, node_weights) local wea = worldeditadditions
pos1, pos2 = worldedit.sort_pos(pos1, pos2)
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
-- @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 -- pos2 will always have the highest co-ordinates now
-- Fetch the nodes in the specified area -- Fetch the nodes in the specified area
@ -11,38 +33,58 @@ function worldeditadditions.layers(pos1, pos2, node_weights)
local node_id_ignore = minetest.get_content_id("ignore") 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)) local heightmap, heightmap_size = wea.make_heightmap(
-- minetest.log("action", "pos2: " .. worldeditadditions.vector.tostring(pos2)) pos1, pos2,
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 -- for i,v in ipairs(node_ids) do
-- print("[layer] i", i, "node id", v) -- print("[layer] i", i, "node id", v)
-- end -- end
-- z y x is the preferred loop order, but that isn't really possible here -- 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 z = pos2.z, pos1.z, -1 do
for x = pos2.x, pos1.x, -1 do for x = pos2.x, pos1.x, -1 do
local next_index = 1 -- We use table.insert() in make_weighted local next_index = 1 -- We use table.insert() in make_weighted
local placed_node = false local placed_node = false
for y = pos2.y, pos1.y, -1 do local hi = (z-pos1.z)*heightmap_size.x + (x-pos1.x)
local i = area:index(x, y, z)
-- print("DEBUG hi", hi, "x", x, "z", z, "slope", slopemap[hi], "as deg", math.deg(slopemap[hi]))
local is_air = worldeditadditions.is_airlike(data[i])
local is_ignore = data[i] == node_id_ignore -- Again, Lua 5.1 doesn't have a continue statement :-/
if slopemap[hi] >= min_slope and slopemap[hi] <= max_slope then
if not is_air and not is_ignore then for y = pos2.y, pos1.y, -1 do
-- It's not an airlike node or something else odd local i = area:index(x, y, z)
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 local is_air = wea.is_airlike(data[i])
if next_index > #node_ids then local is_ignore = data[i] == node_id_ignore
break
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
end end
else
changes.skipped_columns_slope = changes.skipped_columns_slope + 1
end end
if not placed_node then if not placed_node then

@ -1,5 +1,3 @@
--- Creates vertical walls on the inside of the defined region.
-- @module worldeditadditions.walls
-- ██ ██ █████ ██ ██ ███████ -- ██ ██ █████ ██ ██ ███████
-- ██ ██ ██ ██ ██ ██ ██ -- ██ ██ ██ ██ ██ ██ ██
@ -7,8 +5,15 @@
-- ██ ███ ██ ██ ██ ██ ██ ██ -- ██ ███ ██ ██ ██ ██ ██ ██
-- ███ ███ ██ ██ ███████ ███████ ███████ -- ███ ███ ██ ██ ███████ ███████ ███████
function worldeditadditions.walls(pos1, pos2, node_name) --- Creates vertical walls on the inside of the defined region.
-- @apipath worldeditadditions.walls
-- @param pos1 Vector Position 1 of the defined region,
-- @param pos2 Vector Position 2 of the defined region.
-- @param node_name string The name of the node to use to create the walls with.
-- @param thickness number? The thickness of the walls to create. Default: 1
function worldeditadditions.walls(pos1, pos2, node_name, thickness)
pos1, pos2 = worldedit.sort_pos(pos1, pos2) pos1, pos2 = worldedit.sort_pos(pos1, pos2)
if not thickness then thickness = 1 end
-- pos2 will always have the highest co-ordinates now -- pos2 will always have the highest co-ordinates now
-- Fetch the nodes in the specified area -- Fetch the nodes in the specified area
@ -22,7 +27,10 @@ function worldeditadditions.walls(pos1, pos2, node_name)
for z = pos2.z, pos1.z, -1 do for z = pos2.z, pos1.z, -1 do
for y = pos2.y, pos1.y, -1 do for y = pos2.y, pos1.y, -1 do
for x = pos2.x, pos1.x, -1 do for x = pos2.x, pos1.x, -1 do
if x == pos1.x or x == pos2.x or z == pos1.z or z == pos2.z then if math.abs(x - pos1.x) < thickness
or math.abs(x - pos2.x) < thickness
or math.abs(z - pos1.z) < thickness
or math.abs(z - pos2.z) < thickness then
data[area:index(x, y, z)] = node_id data[area:index(x, y, z)] = node_id
count_replaced = count_replaced + 1 count_replaced = count_replaced + 1
end end

@ -0,0 +1,37 @@
--- Serialises an arbitrary value to a string.
-- Note that although the resulting table *looks* like valid Lua, it isn't.
-- @param item any Input item to serialise.
-- @param sep string key value seperator
-- @param new_line string key value pair delimiter
-- @return string concatenated table pairs
local function inspect(item, maxdepth)
if not maxdepth then maxdepth = 3 end
if type(item) ~= "table" then
if type(item) == "string" then return "\""..item.."\"" end
return tostring(item)
end
if maxdepth < 1 then return "[truncated]" end
local result = { "{\n" }
for key,value in pairs(item) do
local value_text = inspect(value, maxdepth - 1)
:gsub("\n", "\n\t")
table.insert(result, "\t"..tostring(key).." = ".."("..type(value)..") "..value_text.."\n")
end
table.insert(result, "}")
return table.concat(result,"")
end
-- local test = {
-- a = { x = 5, y = 7, z = -6 },
-- http = {
-- port = 80,
-- protocol = "http"
-- },
-- mode = "do_stuff",
-- apple = false,
-- deepa = { deepb = { deepc = { yay = "Happy Birthday!" } }}
-- }
-- print(inspect(test))
return inspect

@ -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 -- @submodule worldeditadditions.utils.queue
-- @class
local Queue = {} local Queue = {}
Queue.__index = Queue Queue.__index = Queue
--- Creates a new queue instance.
-- @returns Queue
function Queue.new() function Queue.new()
local result = { first = 0, last = -1, items = {} } local result = { first = 0, last = -1, items = {} }
setmetatable(result, Queue) setmetatable(result, Queue)
return result return result
end 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) function Queue:enqueue(value)
local new_last = self.last + 1 local new_last = self.last + 1
self.last = new_last self.last = new_last
@ -18,6 +24,9 @@ function Queue:enqueue(value)
return new_last return new_last
end 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) function Queue:contains(value)
for i=self.first,self.last do for i=self.first,self.last do
if self.items[i] == value then if self.items[i] == value then
@ -27,14 +36,22 @@ function Queue:contains(value)
return false return false
end end
--- Returns whether the queue is empty or not.
-- @returns bool Whether the queue is empty or not.
function Queue:is_empty() function Queue:is_empty()
return self.first > self.last return self.first > self.last
end 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) function Queue:remove_index(index)
self.items[index] = nil self.items[index] = nil
end 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() function Queue:dequeue()
if Queue.is_empty(self) then if Queue.is_empty(self) then
error("Error: The self is empty!") error("Error: The self is empty!")
@ -53,4 +70,5 @@ function Queue:dequeue()
return value return value
end end
return Queue return Queue

@ -1,3 +1,4 @@
local wea = worldeditadditions
--- Given a manip object and associates, generates a 2D x/z heightmap. --- Given a manip object and associates, generates a 2D x/z heightmap.
-- Note that pos1 and pos2 should have already been pushed through -- 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 -- Scan each column top to bottom
for y = pos2.y+1, pos1.y, -1 do for y = pos2.y+1, pos1.y, -1 do
local i = area:index(x, y, z) 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 -- 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) -- Start heightmap values from 1 (i.e. there's at least 1 node in the column)
heightmap[hi] = (y - pos1.y) + 1 heightmap[hi] = (y - pos1.y) + 1
@ -48,7 +49,7 @@ end
-- will have the z and y values swapped. -- will have the z and y values swapped.
-- @param heightmap table A ZERO indexed flat heightmap. See worldeditadditions.make_heightmap(). -- @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 ] -- @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) function worldeditadditions.calculate_normals(heightmap, heightmap_size)
-- print("heightmap_size: "..heightmap_size.x.."x"..heightmap_size.z) -- print("heightmap_size: "..heightmap_size.x.."x"..heightmap_size.z)
local result = {} local result = {}
@ -72,18 +73,43 @@ 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] 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) -- 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({ result[hi] = wea.Vector3.new(
x = left - right, left - right, -- x
y = 2, -- Z & Y are flipped 2, -- y - Z & Y are flipped
z = down - up 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
end end
return result return result
end 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_slopes(heightmap, heightmap_size)
local normals = worldeditadditions.calculate_normals(heightmap, heightmap_size)
local slopes = { }
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] = math.acos(normals[hi].y)
end
end
return slopes
end
--- Applies changes to a heightmap to a Voxel Manipulator data block. --- Applies changes to a heightmap to a Voxel Manipulator data block.
-- @param pos1 vector Position 1 of the defined region -- @param pos1 vector Position 1 of the defined region
-- @param pos2 vector Position 2 of the defined region -- @param pos2 vector Position 2 of the defined region

@ -256,7 +256,8 @@ end
--- Sorts the components of the given vectors. --- Sorts the components of the given vectors.
-- pos1 will contain the minimum values, and pos2 the maximum values. -- pos1 will contain the minimum values, and pos2 the maximum values.
-- Returns 2 new vectors. -- 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. -- It is only required that they have the keys x, y, and z.
-- Vector3 instances are always returned. -- Vector3 instances are always returned.
-- This enables convenient ingesting of positions from outside. -- This enables convenient ingesting of positions from outside.
@ -316,8 +317,8 @@ end
--- Returns the mean (average) of 2 positions. --- Returns the mean (average) of 2 positions.
-- In other words, returns the centre of 2 points. -- In other words, returns the centre of 2 points.
-- @param pos1 Vector3 pos1 of the defined region. -- @param pos1 Vector3|number pos1 of the defined region.
-- @param pos2 Vector3 pos2 of the defined region. -- @param pos2 Vector3|number pos2 of the defined region.
-- @param target Vector3 Centre coordinates. -- @param target Vector3 Centre coordinates.
function Vector3.mean(pos1, pos2) function Vector3.mean(pos1, pos2)
return (pos1 + pos2) / 2 return (pos1 + pos2) / 2
@ -325,10 +326,16 @@ end
--- Returns a vector of the min components of 2 vectors. --- Returns a vector of the min components of 2 vectors.
-- @param pos1 Vector3 The first vector to operate on. -- @param pos1 Vector3|number The first vector to operate on.
-- @param pos2 Vector3 The second vector to operate on. -- @param pos2 Vector3|number The second vector to operate on.
-- @return Vector3 The minimum values from the input vectors -- @return Vector3 The minimum values from the input vectors
function Vector3.min(pos1, pos2) 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( return Vector3.new(
math.min(pos1.x, pos2.x), math.min(pos1.x, pos2.x),
math.min(pos1.y, pos2.y), math.min(pos1.y, pos2.y),
@ -341,6 +348,12 @@ end
-- @param pos2 Vector3 The second vector to operate on. -- @param pos2 Vector3 The second vector to operate on.
-- @return Vector3 The maximum values from the input vectors. -- @return Vector3 The maximum values from the input vectors.
function Vector3.max(pos1, pos2) 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( return Vector3.new(
math.max(pos1.x, pos2.x), math.max(pos1.x, pos2.x),
math.max(pos1.y, pos2.y), math.max(pos1.y, pos2.y),

@ -6,7 +6,7 @@ local we_c = worldeditadditions_commands
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ██████ ██████ ██ ████ ███████ ██ ██ ███████ ██ ██ ███████ -- ██████ ██████ ██ ████ ███████ ██ ██ ███████ ██ ██ ███████
worldedit.register_command("bonemeal", { worldedit.register_command("bonemeal", {
params = "[<strength> [<chance>]]", params = "[<strength> [<chance> [<node_name> [<node_name> ...]]]]",
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> chance to be bonemealed; higher values mean a lower chance; default: 1 - 100% chance).", 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> chance to be bonemealed; higher values mean a lower chance; default: 1 - 100% chance).",
privs = { worldedit = true }, privs = { worldedit = true },
require_pos = 2, require_pos = 2,
@ -19,15 +19,16 @@ worldedit.register_command("bonemeal", {
local strength = 1 local strength = 1
local chance = 1 local chance = 1
local node_names = {} -- An empty table means all nodes
if #parts >= 1 then if #parts >= 1 then
strength = tonumber(parts[1]) strength = tonumber(table.remove(parts, 1))
if not strength then if not strength then
return false, "Invalid strength value (value must be an integer)" return false, "Invalid strength value (value must be an integer)"
end end
end end
if #parts >= 2 then if #parts >= 2 then
chance = worldeditadditions.parse.chance(parts[2]) chance = worldeditadditions.parse.chance(table.remove(parts, 1))
if not chance then if not chance then
return false, "Invalid chance value (must be a positive integer)" return false, "Invalid chance value (must be a positive integer)"
end 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)" return false, "Error: strength value out of bounds (value must be an integer between 1 and 4 inclusive)"
end 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 -- 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, end,
nodes_needed = function(name) -- strength, chance nodes_needed = function(name) -- strength, chance
-- Since every node has to have an air block, in the best-case scenario -- Since every node has to have an air block, in the best-case scenario
-- edit only half the nodes in the selected area -- edit only half the nodes in the selected area
return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name]) / 2 return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name]) / 2
end, end,
func = function(name, strength, chance) func = function(name, strength, chance, node_names)
local start_time = worldeditadditions.get_ms_time() local start_time = worldeditadditions.get_ms_time()
local success, nodes_bonemealed, candidates = worldeditadditions.bonemeal(worldedit.pos1[name], worldedit.pos2[name], strength, chance) local success, nodes_bonemealed, candidates = worldeditadditions.bonemeal(
if not success then worldedit.pos1[name], worldedit.pos2[name],
-- nodes_bonemealed is an error message here because success == false strength, chance,
return success, nodes_bonemealed node_names
end )
-- 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 percentage = worldeditadditions.round((nodes_bonemealed / candidates)*100, 2)
local time_taken = worldeditadditions.get_ms_time() - start_time 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 -- Avoid nan% - since if there aren't any candidates then nodes_bonemealed will be 0 too

@ -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", { worldedit.register_command("layers", {
params = "[<node_name_1> [<layer_count_1>]] [<node_name_2> [<layer_count_2>]] ...", params = "[<max_slope|min_slope..max_slope>] [<node_name_1> [<layer_count_1>]] [<node_name_2> [<layer_count_2>]] ...",
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", 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 }, privs = { worldedit = true },
require_pos = 2, require_pos = 2,
parse = function(params_text) parse = function(params_text)
@ -13,21 +36,42 @@ worldedit.register_command("layers", {
params_text = "dirt_with_grass dirt 3" params_text = "dirt_with_grass dirt 3"
end end
local success, node_list = worldeditadditions.parse.weighted_nodes( local parts = worldeditadditions.split_shell(params_text)
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 true
) )
return success, node_list return success, node_list, min_slope, max_slope
end, end,
nodes_needed = function(name) nodes_needed = function(name)
return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name]) return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name])
end, end,
func = function(name, node_list) func = function(name, node_list, min_slope, max_slope)
local start_time = worldeditadditions.get_ms_time() 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 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") print("DEBUG min_slope", min_slope, "max_slope", max_slope)
return true, changes.replaced .. " nodes replaced and " .. changes.skipped_columns .. " columns skipped in " .. worldeditadditions.format.human_time(time_taken) 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 end
}) })

@ -4,28 +4,48 @@
-- ██ ███ ██ ██ ██ ██ ██ ██ -- ██ ███ ██ ██ ██ ██ ██ ██
-- ███ ███ ██ ██ ███████ ███████ ███████ -- ███ ███ ██ ██ ███████ ███████ ███████
worldedit.register_command("walls", { worldedit.register_command("walls", {
params = "<replace_node>", params = "<replace_node> [<thickness=1>]",
description = "Creates vertical walls of <replace_node> around the inside edges of the defined region.", description = "Creates vertical walls of <replace_node> around the inside edges of the defined region. Optionally specifies a thickness for the walls to be created (defaults to 1)",
privs = { worldedit = true }, privs = { worldedit = true },
require_pos = 2, require_pos = 2,
parse = function(params_text) parse = function(params_text)
local target_node = worldedit.normalize_nodename(params_text) local parts = worldeditadditions.split_shell(params_text)
local target_node
local thickness = 1
local target_node_raw = table.remove(parts, 1)
target_node = worldedit.normalize_nodename(target_node_raw)
if not target_node then if not target_node then
return false, "Error: Invalid node name" return false, "Error: Invalid node name '"..target_node_raw.."'."
end end
return true, target_node
if #parts > 0 then
local thickness_raw = table.remove(parts, 1)
thickness = tonumber(thickness_raw)
if not thickness then return false, "Error: Invalid thickness value '"..thickness_raw.."'. The thickness value must be a positive integer greater than or equal to 0." end
if thickness < 1 then return false, "Error: That thickness value '"..thickness_raw.."' is out of range. The thickness value must be a positive integer greater than or equal to 0." end
end
return true, target_node, math.floor(thickness)
end, end,
nodes_needed = function(name) nodes_needed = function(name, target_node, thickness)
-- //overlay only modifies up to 1 node per column in the selected region -- //overlay only modifies up to 1 node per column in the selected region
local pos1, pos2 = worldedit.sort_pos(worldedit.pos1[name], worldedit.pos2[name]) local pos1, pos2 = worldedit.sort_pos(worldedit.pos1[name], worldedit.pos2[name])
local pos3 = { x = pos2.x - 2, z = pos2.z - 2, y = pos2.y } local pos3 = {
x = pos2.x - thickness*2,
z = pos2.z - thickness*2,
y = pos2.y }
return worldedit.volume(pos1, pos2) - worldedit.volume(pos1, pos3) return worldedit.volume(pos1, pos2) - worldedit.volume(pos1, pos3)
end, end,
func = function(name, target_node) func = function(name, target_node, thickness)
local start_time = worldeditadditions.get_ms_time() local start_time = worldeditadditions.get_ms_time()
local success, replaced = worldeditadditions.walls(worldedit.pos1[name], worldedit.pos2[name], target_node) local success, replaced = worldeditadditions.walls(
worldedit.pos1[name], worldedit.pos2[name],
target_node, thickness
)
local time_taken = worldeditadditions.get_ms_time() - start_time local time_taken = worldeditadditions.get_ms_time() - start_time
minetest.log("action", name .. " used //walls from "..worldeditadditions.vector.tostring(worldedit.pos1[name]).." to "..worldeditadditions.vector.tostring(worldedit.pos1[name])..", replacing " .. replaced .. " nodes in " .. time_taken .. "s") minetest.log("action", name .. " used //walls from "..worldeditadditions.vector.tostring(worldedit.pos1[name]).." to "..worldeditadditions.vector.tostring(worldedit.pos1[name])..", replacing " .. replaced .. " nodes in " .. time_taken .. "s")