mirror of
https://github.com/sbrl/Minetest-WorldEditAdditions.git
synced 2024-11-25 16:43:05 +00:00
Merge branch 'main' into VorTechnix
This commit is contained in:
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 }}">🔗 <!-- 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 }}">🔗 <!-- 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 hi = (z-pos1.z)*heightmap_size.x + (x-pos1.x)
|
||||
|
||||
local is_air = worldeditadditions.is_airlike(data[i])
|
||||
local is_ignore = data[i] == node_id_ignore
|
||||
-- print("DEBUG hi", hi, "x", x, "z", z, "slope", slopemap[hi], "as deg", math.deg(slopemap[hi]))
|
||||
|
||||
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
|
||||
-- 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
|
||||
|
|
37
worldeditadditions/utils/inspect.lua
Normal file
37
worldeditadditions/utils/inspect.lua
Normal file
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue