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>
</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">
<div class="form-item bigsearch">
<label for="input-filter">Filter:</label>
@ -37,6 +22,21 @@
</div>
</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>
function search_text(query, text) {
return text.toLocaleLowerCase().includes(query);
@ -45,19 +45,27 @@
function do_filter() {
let el_search = document.querySelector("#input-filter");
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 mode = el_searchall.checked ? "all" : "header";
console.log(`SEARCH | mode`, mode, `query`, query);
for(let i = 0; i < els_sections.length; i++) {
let el_next = els_sections[i];
for(let i = 0; i < els_filterable.length; 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;
if(query.length > 0) {
switch(mode) {
switch(mode_this) {
case "all":
show = search_text(query,
el_next.textContent

@ -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)

@ -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)

@ -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)

@ -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.
- 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
- `//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)

@ -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%
```
## `//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.
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
//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 [<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
```
## `//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`.
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.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
@ -242,15 +250,19 @@ 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 <replace_node>`
Creates vertical walls of `<replace_node>` around the inside edges of the defined region.
## `//walls <replace_node> [<thickness=1>]`
Creates vertical walls of `<replace_node>` around the inside edges of the defined region, optionally specifying the thickness thereof.
```weacmd
//walls dirt
//walls stone
//walls goldblock
//walls sandstone 2
//walls glass 4
```
## `//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.LRU = dofile(wea.modpath.."/utils/lru.lua")
wea.inspect = dofile(wea.modpath.."/utils/inspect.lua")
wea.bit = dofile(wea.modpath.."/utils/bit.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,

@ -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)
pos1, pos2 = worldedit.sort_pos(pos1, pos2)
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
-- @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 +33,58 @@ 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)
-- 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
-- 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-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
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

@ -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)
if not thickness then thickness = 1 end
-- pos2 will always have the highest co-ordinates now
-- 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 y = pos2.y, pos1.y, -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
count_replaced = count_replaced + 1
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
-- @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

@ -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
@ -48,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 = {}
@ -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] 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_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.
-- @param pos1 vector Position 1 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.
-- 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.
@ -316,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
@ -325,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),
@ -341,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),

@ -6,7 +6,7 @@ local we_c = worldeditadditions_commands
-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
-- ██████ ██████ ██ ████ ███████ ██ ██ ███████ ██ ██ ███████
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).",
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

@ -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 = "[<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",
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. 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,42 @@ 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)
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
})

@ -4,28 +4,48 @@
-- ██ ███ ██ ██ ██ ██ ██ ██
-- ███ ███ ██ ██ ███████ ███████ ███████
worldedit.register_command("walls", {
params = "<replace_node>",
description = "Creates vertical walls of <replace_node> around the inside edges of the defined region.",
params = "<replace_node> [<thickness=1>]",
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 },
require_pos = 2,
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
return false, "Error: Invalid node name"
return false, "Error: Invalid node name '"..target_node_raw.."'."
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,
nodes_needed = function(name)
nodes_needed = function(name, target_node, thickness)
-- //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 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)
end,
func = function(name, target_node)
func = function(name, target_node, thickness)
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
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")