Merge branch 'main' into vortechnix

This commit is contained in:
VorTechnix 2021-05-30 09:35:41 -07:00
commit 8884c938e0
17 changed files with 452 additions and 39 deletions

@ -5,8 +5,6 @@ Note to self: See the bottom of this file for the release template text.
## v1.12: The selection tools update (unreleased) ## v1.12: The selection tools update (unreleased)
- Add `//spush`, `//spop`, and `//sstack` - Add `//spush`, `//spop`, and `//sstack`
- `//overlay`: Don't place nodes above water
- `//multi`: Improve resilience by handling some edge cases
- Add `//srect` (_select rectangle_), `//scol` (_select column_), `//scube` (_select cube_) - thanks, @VorTechnix! - Add `//srect` (_select rectangle_), `//scol` (_select column_), `//scube` (_select cube_) - thanks, @VorTechnix!
- Add `//scloud` (_select point cloud_), `//scentre` (_select centre node(s)_), `//srel` (_select relative_) - thanks, @VorTechnix! - Add `//scloud` (_select point cloud_), `//scentre` (_select centre node(s)_), `//srel` (_select relative_) - thanks, @VorTechnix!
- Significantly refactored backend utility functions (more to come in future updates) - Significantly refactored backend utility functions (more to come in future updates)
@ -16,7 +14,14 @@ Note to self: See the bottom of this file for the release template text.
- Caveat: Percentages are converted to a 1-in-N chance, but additionally that number is rounded down in some places - Caveat: Percentages are converted to a 1-in-N chance, but additionally that number is rounded down in some places
- `//torus`, `//hollowtorus`: Add optional new axes - `//torus`, `//hollowtorus`: Add optional new axes
- `//torus`: Add optional hollow keyword - `//torus`: Add optional hollow keyword
- `//layers`: Fix crash duee to outdated debug code - `//multi`: Add curly brace syntax for nesting command calls ([more information](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#multi-command_a-command_b-command_c-))
- `//erode`: Add new `river` erosion algorithm for filling in potholes and removing pillars
### Bugfixes
- `//overlay`: Don't place nodes above water
- `//multi`: Improve resilience by handling some edge cases
- `//layers`: Fix crash due to outdated debug code
- `//erode`/snowballs: Fix assignment to undeclared variable
## v1.11: The big data update (25th January 2021) ## v1.11: The big data update (25th January 2021)
@ -33,7 +38,7 @@ Note to self: See the bottom of this file for the release template text.
- Bugfix: Fix obscure crash in calls to `human_size` ("unknown" will now be returned if passed junk) - Bugfix: Fix obscure crash in calls to `human_size` ("unknown" will now be returned if passed junk)
- `//many` can now be used with commands with no arguments. - `//many` can now be used with commands with no arguments.
- `//conv`, `//erode`, `//fillcaves`: Treat liquids as air - `//conv`, `//erode`, `//fillcaves`: Treat liquids as air
- Add new [cloud wand](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/master/Chat-Command-Reference.md#cloud-wand) - Add new [cloud wand](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#cloud-wand)
- `//conv`, `//erode`: Minor refactoring to improve code clarity - `//conv`, `//erode`: Minor refactoring to improve code clarity
@ -89,7 +94,7 @@ Updating depends on how you installed WorldEditAdditions.
- UI in Minetest: There should be an update button for you to click in the mod menu - UI in Minetest: There should be an update button for you to click in the mod menu
- ContentDB: Download the latest update from [here](https://content.minetest.net/packages/Starbeamrainbowlabs/worldeditadditions/) - ContentDB: Download the latest update from [here](https://content.minetest.net/packages/Starbeamrainbowlabs/worldeditadditions/)
- Git: `cd` to the WorldEditAdditions directory and run `git pull` - Git: `cd` to the WorldEditAdditions directory and run `git pull` (**Important:** Recently, WorldEditAdditions changed the default branch from `master` to `main`. If you're updating from before then, you'll need to re-clone the mod or else do some git-fu)
After installing the update, don't forget to restart your client and / or server. After installing the update, don't forget to restart your client and / or server.

@ -408,14 +408,17 @@ The sigma value is only applicable to the `gaussian` kernel, and can be thought
//convolve gaussian 5 0.2 //convolve gaussian 5 0.2
``` ```
## `//erode [<snowballs|...> [<key_1> [<value_1>]] [<key_2> [<value_2>]] ...]` ## `//erode [<snowballs|river> [<key_1> [<value_1>]] [<key_2> [<value_2>]] ...]`
Runs an erosion algorithm over the defined region, optionally passing a number of key - value pairs representing parameters that are passed to the chosen algorithm. This command is **experimental**, as the author is currently on-the-fence about the effects it produces. Runs an erosion algorithm over the defined region, optionally passing a number of key - value pairs representing parameters that are passed to the chosen algorithm. This command is **experimental**, as the author is currently on-the-fence about the effects it produces.
Works best if you run `//fillcaves` first, or otherwise have no air nodes below the top non-air node in each column.
Currently implemented algorithms: Currently implemented algorithms:
Algorithm | Mode | Description Algorithm | Mode | Description
------------|-------|------------------- ------------|-------|-------------------
`snowballs` | 2D | The default - based on [this blog post](https://jobtalle.com/simulating_hydraulic_erosion.html). Simulates snowballs rolling across the terrain, eroding & depositing material. Then runs a 3x3 gaussian kernel over the result (i.e. like the `//conv` / `//smoothadv` command). `snowballs` | 2D | The default - based on [this blog post](https://jobtalle.com/simulating_hydraulic_erosion.html). Simulates snowballs rolling across the terrain, eroding & depositing material. Then runs a 3x3 gaussian kernel over the result (i.e. like the `//conv` / `//smoothadv` command).
`river` | 2D | Fills in potholes and lowers pillars using a cellular automata-like algorithm that analyses the height of neighouring columns.
Usage examples: Usage examples:
@ -431,7 +434,7 @@ Each of the algorithms above have 1 or more parameters that they support. These
Based on the algorithm detailed in [this blog post](https://jobtalle.com/simulating_hydraulic_erosion.html) ([direct link to the source code](https://github.com/jobtalle/HydraulicErosion/blob/master/js/archipelago/island/terrain/erosionHydraulic.js)), devised by [Job Talle](https://jobtalle.com/). Based on the algorithm detailed in [this blog post](https://jobtalle.com/simulating_hydraulic_erosion.html) ([direct link to the source code](https://github.com/jobtalle/HydraulicErosion/blob/master/js/archipelago/island/terrain/erosionHydraulic.js)), devised by [Job Talle](https://jobtalle.com/).
Parameter | Type | Default Value | Description Parameter | Type | Default Value | Description
--------------------|-----------|-------------------|------------------------ --------------------|-----------|-------------------|--------------------------
rate_deposit | `float` | 0.03 | The rate at which snowballs will deposit material rate_deposit | `float` | 0.03 | The rate at which snowballs will deposit material
rate_erosion | `float` | 0.04 | The rate at which snowballs will erode material rate_erosion | `float` | 0.04 | The rate at which snowballs will erode material
friction | `float` | 0.07 | More friction slows snowballs down more. friction | `float` | 0.07 | More friction slows snowballs down more.
@ -444,8 +447,34 @@ maxdiff | `float` | 0.4 | The maximum difference in height (between 0 and
count | `float` | 25000 | The number of snowballs to simulate. count | `float` | 25000 | The number of snowballs to simulate.
noconv | any | n/a | When set to any value, disables to automatic 3x3 gaussian convolution. noconv | any | n/a | When set to any value, disables to automatic 3x3 gaussian convolution.
Example invocations:
```
//erode
//erode snowballs
//erode snowballs count 50000
```
If you find any good combinations of these parameters, please [open an issue](https://github.com/sbrl/Minetest-WorldEditAdditions/issues/new) (or a PR!) and let me know! I'll include good combinations here, and possibly add a presets feature too. If you find any good combinations of these parameters, please [open an issue](https://github.com/sbrl/Minetest-WorldEditAdditions/issues/new) (or a PR!) and let me know! I'll include good combinations here, and possibly add a presets feature too.
### Algorithm: `river`
Ever been annoyed by small 1 wide holes or thin pillars all over the place? This command is for you! Does not operate on the very edge of the defined region, because the algorithm can't see the neighbours of those columns.
Parameter | Type | Default Value | Description
--------------------|-----------|-------------------|--------------------------
steps | `integer` | 1 | The number of rounds or passes of the algorithm to run. Useful if you have a 1x3 hole for instance, it will take at least 2 steps to fill it in - and more if it's deeper than 1 node.
lower_sides | `string` | 4,3 | Comma separated list of numbers. Columns with this many sides lower than it will be lowered in height by 1 node.
raise_sides | `string` | 4,3 | Comma separated list of numbers. Columns with this many sides higher than it will be raised in height by 1 node.
doraise | `boolean` | true | Whether to raise columns in height. If false, then no columns will be raised in height even if they are eligible to be so according to `raise_sides`.
dolower | `boolean` | true | Whether to lower columns in height. If false, then no columns will be lowered in height even if they are eligible to be so according to `lower_sides`.
Example invocations:
```
//erode river
//erode river steps 10
```
## `//count` ## `//count`
Counts all the nodes in the defined region and returns the result along with calculated percentages (note that if the chat window used a monospace font, the returned result would be a perfect table. If someone has a ~~hack~~ solution to make the columns line up neatly, please [open an issue](https://github.com/sbrl/Minetest-WorldEditAdditions/issues/new) :D) Counts all the nodes in the defined region and returns the result along with calculated percentages (note that if the chat window used a monospace font, the returned result would be a perfect table. If someone has a ~~hack~~ solution to make the columns line up neatly, please [open an issue](https://github.com/sbrl/Minetest-WorldEditAdditions/issues/new) :D)
@ -475,6 +504,20 @@ While other server commands can be executed while a `//subdivide` is running, `/
## `//multi <command_a> <command_b> <command_c> .....` ## `//multi <command_a> <command_b> <command_c> .....`
Executes multi chat commands in sequence. Intended for _WorldEdit_ commands, but does work with others too. Don't forget a space between commands! Executes multi chat commands in sequence. Intended for _WorldEdit_ commands, but does work with others too. Don't forget a space between commands!
```
//multi //set dirt //shift x 10 //set glass
```
Since WorldEditAdditions v1.12, curly brace syntax has also been introduced to allow nesting of commands:
```
//multi //fixlight {//many 5 //bonemeal 3 100}
```
This syntax can also be nested arbitrarily in arbitrarily complex combinations, and can also be used multiple separate times in a single `//multi` invocation (if you find a bug, please [open an issue](https://github.com/sbrl/Minetest-WorldEditAdditions/issues/new)), though do remember that only `//multi` supports parsing out this syntax (e.g. if you want to nest multiple commands in a `//many` that's inside a `//multi`, you'll need a sub `//multi` there).
In addition, this also allows for including a double forward slash in the argument list for a command, should you need to do so (e.g. `//multi //example {//bar //baz} //example` will be executed as 3 commands: `/example`, then `/bar` with an argument of `//baz`, then finally `/example`).
``` ```
//multi //1 //2 //shift z -10 //sphere 5 sand //shift z 20 //ellipsoid 5 3 5 ice //multi //1 //2 //shift z -10 //sphere 5 sand //shift z 20 //ellipsoid 5 3 5 ice
//multi //1 //hollowtorus 30 5 stone //hollowtorus 20 3 dirt //torus 10 2 dirt_with_grass //multi //1 //hollowtorus 30 5 stone //hollowtorus 20 3 dirt //torus 10 2 dirt_with_grass

@ -48,7 +48,7 @@ The detailed explanations have moved! Check them out [here](https://github.com/s
- [`//layers [<node_name_1> [<layer_count_1>]] [<node_name_2> [<layer_count_2>]] ...`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#layers-node_name_1-layer_count_1-node_name_2-layer_count_2-) - [`//layers [<node_name_1> [<layer_count_1>]] [<node_name_2> [<layer_count_2>]] ...`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#layers-node_name_1-layer_count_1-node_name_2-layer_count_2-)
- [`//fillcaves [<node_name>]`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#fillcaves-node_name) - [`//fillcaves [<node_name>]`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#fillcaves-node_name)
- [`//convolve <kernel> [<width>[,<height>]] [<sigma>]`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#convolve-kernel-widthheight-sigma) - [`//convolve <kernel> [<width>[,<height>]] [<sigma>]`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#convolve-kernel-widthheight-sigma)
- [`//erode [<snowballs|...> [<key_1> [<value_1>]] [<key_2> [<value_2>]] ...]`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#erode-snowballs-key_1-value_1-key_2-value_2-) **experimental** - [`//erode [<snowballs|river> [<key_1> [<value_1>]] [<key_2> [<value_2>]] ...]`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#erode-snowballsriver-key_1-value_1-key_2-value_2-) **experimental**
### Flora ### Flora
- [`//bonemeal [<strength> [<chance>]]`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#bonemeal-strength-chance) - [`//bonemeal [<strength> [<chance>]]`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#bonemeal-strength-chance)

BIN
screenshot2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

@ -1,6 +1,7 @@
worldeditadditions.erode = {} worldeditadditions.erode = {}
dofile(worldeditadditions.modpath.."/lib/erode/snowballs.lua") dofile(worldeditadditions.modpath.."/lib/erode/snowballs.lua")
dofile(worldeditadditions.modpath.."/lib/erode/river.lua")
function worldeditadditions.erode.run(pos1, pos2, algorithm, params) function worldeditadditions.erode.run(pos1, pos2, algorithm, params)
@ -24,10 +25,27 @@ function worldeditadditions.erode.run(pos1, pos2, algorithm, params)
-- worldeditadditions.format.array_2d(heightmap, heightmap_size.x) -- worldeditadditions.format.array_2d(heightmap, heightmap_size.x)
local success, msg, stats local success, msg, stats
if algorithm == "snowballs" then if algorithm == "snowballs" then
success, msg = worldeditadditions.erode.snowballs(heightmap, heightmap_eroded, heightmap_size, region_height, params) success, msg = worldeditadditions.erode.snowballs(
heightmap, heightmap_eroded,
heightmap_size,
region_height,
params
)
if not success then return success, msg end
elseif algorithm == "river" then
success, msg = worldeditadditions.erode.river(
heightmap, heightmap_eroded,
heightmap_size,
region_height,
params
)
if not success then return success, msg end if not success then return success, msg end
else else
return false, "Error: Unknown algorithm '"..algorithm.."'. Currently implemented algorithms: snowballs (2d; hydraulic-like). Ideas for algorithms to implement are welcome!" -- FUTURE: Add a new "river" algorithm here that:
-- * Fills in blocks that are surrounded on more than 3 horizontal sides
-- * Destroys blocks that have no horizontal neighbours
-- A bit like cellular automata actually.
return false, "Error: Unknown algorithm '"..algorithm.."'. Currently implemented algorithms: snowballs (2d; hydraulic-like), river (2d; cellular automata-like; fills potholes and lowers towers). Ideas for algorithms to implement are welcome!"
end end
success, stats = worldeditadditions.apply_heightmap_changes( success, stats = worldeditadditions.apply_heightmap_changes(

@ -0,0 +1,123 @@
local wea = worldeditadditions
--- Parses a comma-separated side numbers list out into a list of numbers.
-- @param list string The command separated list to parse.
-- @returns number[] A list of side numbers.
local function parse_sides_list(list)
list = list:gsub("%s", "") -- Spaces are not permitted
return wea.table_unique(wea.table_map(
wea.split(list, ","),
function(value) return tonumber(value) end
))
end
function worldeditadditions.erode.river(heightmap_initial, heightmap, heightmap_size, region_height, params_custom)
local params = {
steps = 1, -- Number of rounds/passes of the algorithm to run
lower_sides = "4,3", -- Cells with this many adjacent horizontal neighbours that are lower than the current pixel will be removed
raise_sides = "4,3", -- Cells with this many adjacect horizontal neighbours that are higher than the current pixel will be filled in
doraise = true, -- Whether to do raise operations or not
dolower = true -- Whether to do lower operations or not
}
-- Apply the custom settings
wea.table_apply(params_custom, params)
params.lower_sides = parse_sides_list(params.lower_sides)
params.raise_sides = parse_sides_list(params.raise_sides)
local timings = {}
local filled = 0
local removed = 0
for i=1,params.steps do
-- print("[DEBUG:river] step ", i)
-- wea.format.array_2d(heightmap, heightmap_size.x)
local time_start = wea.get_ms_time()
-- Store up changes to make and make them at the end of the step
-- This is important, because decisions
local fill = { } -- Indexes to add 1 to
local remove = { } -- Indexes to take 1 away from
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
local thisheight = heightmap[hi]
-- print("[DEBUG:river] z", z, "x", x, "thisheight", thisheight)
local height_up = heightmap[hi]
local height_down = heightmap[hi]
local height_left = heightmap[hi]
local height_down = heightmap[hi]
if x > 0 then height_left = heightmap[z*heightmap_size.x + x-1] end
if x < heightmap_size.x - 1 then height_right = heightmap[z*heightmap_size.x + x+1] end
if z > 0 then height_up = heightmap[(z-1)*heightmap_size.x + x] end
if z < heightmap_size.z - 1 then height_down = heightmap[(z+1)*heightmap_size.x + x] end
-- Whether this pixel is on the edge
local isedge = x <= 0
or z <= 0
or x >= heightmap_size.x - 1
or z >= heightmap_size.z - 1
local sides_higher = 0 -- Number of sides higher than this pixel
local sides_lower = 0 -- Number of sides lower than this pixel
if not isedge then
if height_down > thisheight then sides_higher = sides_higher + 1 end
if height_up > thisheight then sides_higher = sides_higher + 1 end
if height_left > thisheight then sides_higher = sides_higher + 1 end
if height_right > thisheight then sides_higher = sides_higher + 1 end
if height_down < thisheight then sides_lower = sides_lower + 1 end
if height_up < thisheight then sides_lower = sides_lower + 1 end
if height_left < thisheight then sides_lower = sides_lower + 1 end
if height_right < thisheight then sides_lower = sides_lower + 1 end
end
-- Perform an action, but only if we're not on the edge
-- This is important, as we can't accurately tell how many
-- adjacent neighbours a pixel on the edge has.
local action = "none"
if not isedge then
if sides_higher > sides_lower then
for i,sidecount in ipairs(params.raise_sides) do
if sidecount == sides_higher then
action = "fill"
break
end
end
else
for i,sidecount in ipairs(params.lower_sides) do
if sidecount == sides_lower then
action = "remove"
break
end
end
end
end
if action == "fill" and params.doraise then
table.insert(fill, hi)
filled = filled + 1
elseif action == "remove" and params.dolower then
table.insert(remove, hi)
removed = removed + 1
end
-- print("[DEBUG:river] sides_higher", sides_higher, "sides_lower", sides_lower, "action", action)
-- wea.format.array_2d(heightmap, heightmap_size.x)
end
end
for i,hi in ipairs(fill) do
heightmap[hi] = heightmap[hi] + 1
end
for i,hi in ipairs(remove) do
heightmap[hi] = heightmap[hi] - 1
end
table.insert(timings, wea.get_ms_time() - time_start)
end
return true, params.steps.." steps made, raising "..filled.." and lowering "..removed.." columns in "..wea.format.human_time(wea.sum(timings)).." (~"..wea.format.human_time(wea.average(timings)).." per step)"
end

@ -86,7 +86,7 @@ function worldeditadditions.erode.snowballs(heightmap_initial, heightmap, height
maxdiff = 0.4, maxdiff = 0.4,
count = 25000 count = 25000
} }
-- Apply the default settings -- Apply the custom settings
worldeditadditions.table_apply(params_custom, params) worldeditadditions.table_apply(params_custom, params)
-- print("[erode/snowballs] params: ") -- print("[erode/snowballs] params: ")
@ -132,7 +132,7 @@ function worldeditadditions.erode.snowballs(heightmap_initial, heightmap, height
if not params.noconv then if not params.noconv then
local success, matrix = worldeditadditions.get_conv_kernel("gaussian", 3, 3) local success, matrix = worldeditadditions.get_conv_kernel("gaussian", 3, 3)
if not success then return success, matrix end if not success then return success, matrix end
matrix_size = {} matrix_size[0] = 3 matrix_size[1] = 3 local matrix_size = {} matrix_size[0] = 3 matrix_size[1] = 3
worldeditadditions.conv.convolve( worldeditadditions.conv.convolve(
heightmap, heightmap_size, heightmap, heightmap_size,
matrix, matrix,

@ -3,7 +3,7 @@
-- @param tbl number[] The ZERO-indexed list of numbers -- @param tbl number[] The ZERO-indexed list of numbers
-- @param width number The width of 2D array. -- @param width number The width of 2D array.
function worldeditadditions.format.array_2d(tbl, width) function worldeditadditions.format.array_2d(tbl, width)
print("==== count: "..#tbl..", width:"..width.." ====") print("==== count: "..(#tbl+1)..", width:"..width.." ====")
local display_width = 1 local display_width = 1
for _i,value in pairs(tbl) do for _i,value in pairs(tbl) do
display_width = math.max(display_width, #tostring(value)) display_width = math.max(display_width, #tostring(value))

@ -19,12 +19,43 @@ function worldeditadditions.sum(list)
return sum return sum
end end
--- Calculates the mean of all the numbers in the given list.
-- @param list number[] The list (table) of numbers to calculate the mean for.
-- @returns The mean of the numbers in the given table.
function worldeditadditions.average(list) function worldeditadditions.average(list)
if #list == 0 then return 0 end if #list == 0 then return 0 end
return worldeditadditions.sum(list) / #list return worldeditadditions.sum(list) / #list
end end
--- Finds the minimum value in the given list.
-- @param list number[] The list (table) of numbers to find the minimum value of.
-- @returns number The minimum value in the given list.
function worldeditadditions.min(list)
if #list == 0 then return nil end
local min = nil
for i,value in ipairs(list) do
if min == nil or min > value then
min = value
end
end
return min
end
--- Finds the maximum value in the given list.
-- @param list number[] The list (table) of numbers to find the maximum value of.
-- @returns number The maximum value in the given list.
function worldeditadditions.max(list)
if #list == 0 then return nil end
local max = nil
for i,value in ipairs(list) do
if max == nil or max < value then
max = value
end
end
return max
end
--- Returns the minetest.get_us_time() in ms --- Returns the minetest.get_us_time() in ms
-- @return float -- @return float
function worldeditadditions.get_ms_time() function worldeditadditions.get_ms_time()

@ -4,3 +4,4 @@ dofile(worldeditadditions.modpath.."/utils/parse/chance.lua")
dofile(worldeditadditions.modpath.."/utils/parse/map.lua") dofile(worldeditadditions.modpath.."/utils/parse/map.lua")
dofile(worldeditadditions.modpath.."/utils/parse/seed.lua") dofile(worldeditadditions.modpath.."/utils/parse/seed.lua")
dofile(worldeditadditions.modpath.."/utils/parse/weighted_nodes.lua") dofile(worldeditadditions.modpath.."/utils/parse/weighted_nodes.lua")
dofile(worldeditadditions.modpath.."/utils/parse/tokenise_commands.lua")

@ -1,7 +1,7 @@
--- Parses a map of key-value pairs into a table. --- Parses a map of key-value pairs into a table.
-- For example, "count 25000 speed 0.8 rate_erosion 0.006" would be parsed into -- For example, "count 25000 speed 0.8 rate_erosion 0.006 doawesome true" would be parsed into
-- the following table: { count = 25000, speed = 0.8, rate_erosion = 0.006 }. -- the following table: { count = 25000, speed = 0.8, rate_erosion = 0.006, doawesome = true }.
-- @param params_text string The string to parse. -- @param params_text string The string to parse.
-- @returns table A table of key-value pairs parsed out from the given string. -- @returns table A table of key-value pairs parsed out from the given string.
function worldeditadditions.parse.map(params_text) function worldeditadditions.parse.map(params_text)
@ -14,6 +14,9 @@ function worldeditadditions.parse.map(params_text)
-- Try converting to a number to see if it works -- Try converting to a number to see if it works
local part_converted = tonumber(part) local part_converted = tonumber(part)
if as_number == nil then part_converted = part end if as_number == nil then part_converted = part end
-- Look for bools
if part_converted == "true" then part_converted = true end
if part_converted == "false" then part_converted = false end
result[last_key] = part result[last_key] = part
else else
last_key = part last_key = part

@ -0,0 +1,140 @@
--- Uncomment these 2 lines to run in standalone mode
-- worldeditadditions = { parse = { } }
-- function worldeditadditions.trim(str) return (str:gsub("^%s*(.-)%s*$", "%1")) end
--- The main tokeniser. Splits the input string up into space separated tokens, except when said spaces are inside { curly braces }.
-- Note that the outermost set of curly braces are stripped.
-- @param str string The input string to tokenise.
-- @returns string[] A list of tokens
local function tokenise(str)
if type(str) ~= "string" then return false, "Error: Expected input of type string." end
str = str:gsub("%s+", " ") -- Replace all runs of whitespace with a single space
-- The resulting tokens
local result = {}
local nested_depth = 0 -- The nested depth inside { and } we're currently at
local nested_stack = {} -- Stack of starting positions of curly brace { } blocks
local scanpos = 1 -- The current position we're scanning
while scanpos <= #str do
-- Find the next character of interest
local nextpos = str:find("[%s{}]", scanpos)
-- If it's nil, then cleanup and return
if nextpos == nil then
if nested_depth > 0 then
-- Handle unclosed brace groups
return false, "Error: Unclosed brace group detected."
else
-- Make sure we catch any trailing parts
local str_trailing = str:sub(scanpos)
if #str_trailing then table.insert(result, str_trailing) end
return true, result
end
end
-- Extract the character in question
local char = str:sub(nextpos, nextpos)
if char == "}" then
-- Decrease the nested depth
nested_depth = nested_depth - 1
-- Pop the start of this block off the stack and find this block's contents
block_start = table.remove(nested_stack, #nested_stack)
local substr = str:sub(block_start, nextpos - 1)
if #substr > 0 and nested_depth == 0 then table.insert(result, substr) end
elseif char == "{" then
-- Increase the nested depth, and store this position on the stack for later
nested_depth = nested_depth + 1
table.insert(nested_stack, nextpos + 1)
else
-- It's a space! Extract a part, but only if the nested depth is 0 (i.e. we're not inside any braces).
local substr = str:sub(scanpos, nextpos - 1)
if #substr > 0 and nested_depth == 0 then table.insert(result, substr) end
end
-- Move the scanning position up to just after the character we've just
-- found and handled
scanpos = nextpos + 1
end
-- Handle any trailing bits
local str_trailing = str:sub(scanpos)
if #str_trailing > 0 then table.insert(result, str_trailing) end
return true, result
end
--- Recombines a list of tokens into a list of commands.
-- @param parts string[] The tokens from tokenise(str).
-- @returns string[] The tokens, but run through trim() & grouped into commands (1 element in the list = 1 command)
local function recombine(parts)
local result = {}
local acc = {}
for i, value in ipairs(parts) do
value = worldeditadditions.trim(value)
if value:sub(1, 1) == "/" and #acc > 0 then
table.insert(result, table.concat(acc, " "))
acc = {}
end
table.insert(acc, value)
end
if #acc > 0 then table.insert(result, table.concat(acc, " ")) end
return result
end
--- Tokenises a string of multiple commands into an array of individual commands.
-- Preserves the forward slash at the beginning of each command name.
-- Also supports arbitrarily nested and complex curly braces { } for grouping
-- commands together that would normally be split apart.
--
-- Simple example:
-- INPUT: //1 //2 //outset 25 //fixlight
-- OUTPUT: { "//1", "//2", "//outset 25", "//fixlight" }
--
-- Example with curly braces:
-- INPUT: //1 //2 //outset 50 {//many 5 //multi //fixlight //clearcut}
-- OUTPUT: { "//1", "//2", "//outset 50", "//many 5 //multi //fixlight //clearcut"}
--
-- @param command_str str The command string to operate on.
-- @returns bool,(string[]|string) If the operation was successful, then true followed by a table of strings is returned. If the operation was not successful, then false followed by an error message (as a single string) is returned instead.
function worldeditadditions.parse.tokenise_commands(command_str)
local success, result = tokenise(command_str)
if not success then return success, result end
return true, recombine(result)
end
----- Test harness code -----
-----------------------------
-- local function printparts(tbl)
-- for key,value in ipairs(tbl) do
-- print(key..": "..value)
-- end
-- end
--
-- local function test_input(input)
-- local success, result = worldeditadditions.parse.tokenise_commands(input)
-- if success then
-- printparts(result)
--
-- -- print("RECOMBINED:")
-- -- printparts(recombine(result))
-- else
-- print(result)
-- end
--
-- end
--
-- print("\n\n\n*** 1 ***")
-- test_input("//multi //1 //cubeapply 10 set dirt")
-- print("\n\n\n*** 2 ***")
-- test_input("//multi //1 //2 //outset 50 {//many 5 //multi //fixlight //clearcut}")
-- print("\n\n\n*** 3 ***")
-- test_input("//multi //1 //2 //outset 50 {//many 5 //multi //ellipsoid 10 5 7 glass //clearcut}")
-- print("\n\n\n*** 4 ***")
-- test_input("//multi //1 //2 //outset 50 {//many 5 //multi //ellipsoid 10 5 7 glass //clearcut //many {//set dirt //fixlight}}")
-- print("\n\n\n*** 5 ***")
-- test_input("a { b c d { e f { g h i }j} k l m n}o p")
-- print("\n\n\n*** 6 ***")
-- test_input("a { b c d } e f {{ g h i }j k l m n}o p")

@ -22,8 +22,8 @@ end
-- @param str string The string to operate on -- @param str string The string to operate on
-- @param start number The start string to look for -- @param start number The start string to look for
-- @returns bool Whether start is present at the beginning of str -- @returns bool Whether start is present at the beginning of str
function worldeditadditions.str_starts(str,start) function worldeditadditions.str_starts(str, start)
return string.sub(str,1,string.len(start))==start return string.sub(str, 1, string.len(start)) == start
end end
--- Trims whitespace from a string from the beginning and the end. --- Trims whitespace from a string from the beginning and the end.

@ -42,15 +42,12 @@ function worldeditadditions.gsplit(text, pattern, plain)
end end
-- split: split a string into substrings separated by a pattern. -- Split a string into substrings separated by a pattern.
-- -- @param text string The string to iterate over
-- Parameters: -- @param pattern string The separator pattern
-- text (string) - the string to iterate over -- @param plain boolean If true (or truthy), pattern is interpreted as a
-- pattern (string) - the separator pattern -- plain string, not a Lua pattern
-- plain (boolean) - if true (or truthy), pattern is interpreted as a plain -- @returns table A sequence table containing the substrings
-- string, not a Lua pattern
--
-- Returns: table (a sequence table containing the substrings)
function worldeditadditions.split(text, pattern, plain) function worldeditadditions.split(text, pattern, plain)
local ret = {} local ret = {}
for match in worldeditadditions.gsplit(text, pattern, plain) do for match in worldeditadditions.gsplit(text, pattern, plain) do

@ -65,3 +65,53 @@ function worldeditadditions.table_tostring(tbl, sep, new_line)
end end
return ret:concat("") return ret:concat("")
end end
--- Executes the given function on every item in the given table.
-- Ignores return values that are nil and doesn't insert them into the table.
-- @param tbl table The table to operate on.
-- @param func function<any>:any|nil The function to execute on every item in the table.
-- @returns table A new table containing the return values of the function.
function worldeditadditions.table_map(tbl, func)
local result = {}
for i,value in ipairs(tbl) do
local newval = func(value, i)
if newval ~= nil then table.insert(result, newval) end
end
return result
end
--- Filters the items in the given table using the given function.
-- The function is executed for each item in the list. If it returns true, the
-- item is kept. If it returns false, the item is discarded.
-- @param tbl table The table of values to filter.
-- @param func function<any>:bool The filter function to execute - should return a boolean value indicating whether the item provided as the first argument should be kept
-- @returns table A new table containing the values that the given function returned true for.
function worldeditadditions.table_filter(tbl, func)
local result = {}
for i,value in ipairs(tbl) do
if func(value) then
table.insert(result, value)
end
end
return result
end
--- Builds a new table with the elements of the given table appearing at most once.
-- @param tbl table The table of values to make unique.
-- @returns table A new table containing the values of the given table appearing at most once.
function worldeditadditions.table_unique(tbl)
local newtbl = {}
for i,value in ipairs(tbl) do
local seen = false
for j,seenvalue in ipairs(newtbl) do
if value == seenvalue then
seen = true
break
end
end
if not seen then
table.insert(newtbl, value)
end
end
return newtbl
end

@ -4,7 +4,7 @@
-- ██ ██ ██ ██ ██ ██ ██ ██ -- ██ ██ ██ ██ ██ ██ ██ ██
-- ███████ ██ ██ ██████ ██████ ███████ -- ███████ ██ ██ ██████ ██████ ███████
worldedit.register_command("erode", { worldedit.register_command("erode", {
params = "[<snowballs|...> [<key_1> [<value_1>]] [<key_2> [<value_2>]] ...]", params = "[<snowballs|river> [<key_1> [<value_1>]] [<key_2> [<value_2>]] ...]",
description = "**experimental** Runs the specified erosion algorithm over the given defined region. This may occur in 2d or 3d. Currently implemented algorithms: snowballs (default;2d hydraulic-like). Also optionally takes an arbitrary set of key - value pairs representing parameters to pass to the algorithm. See the full documentation for details.", description = "**experimental** Runs the specified erosion algorithm over the given defined region. This may occur in 2d or 3d. Currently implemented algorithms: snowballs (default;2d hydraulic-like). Also optionally takes an arbitrary set of key - value pairs representing parameters to pass to the algorithm. See the full documentation for details.",
privs = { worldedit = true }, privs = { worldedit = true },
require_pos = 2, require_pos = 2,

@ -26,11 +26,6 @@ local function explode(delim, str)
end end
end end
-- From http://lua-users.org/wiki/StringTrim
local function trim(s)
return (s:gsub("^%s*(.-)%s*$", "%1"))
end
minetest.register_chatcommand("/multi", { minetest.register_chatcommand("/multi", {
params = "/<command_a> <args> //<command_b> <args> /<command_c> <args>.....", params = "/<command_a> <args> //<command_b> <args> /<command_c> <args>.....",
description = "Executes multiple chat commands in sequence. Just prepend a list of space-separated chat commands with //multi, and you're good to go! The forward slashes at the beginning of each chat command must be the same as if you were executing it normally.", description = "Executes multiple chat commands in sequence. Just prepend a list of space-separated chat commands with //multi, and you're good to go! The forward slashes at the beginning of each chat command must be the same as if you were executing it normally.",
@ -45,29 +40,36 @@ minetest.register_chatcommand("/multi", {
local master_start_time = worldeditadditions.get_ms_time() local master_start_time = worldeditadditions.get_ms_time()
local times = {} local times = {}
-- Things start at 1, not 0 in Lua :-( -- Tokenise the input into a list of commands
for command in explode(" /", string.sub(params_text, 2)) do local success, commands = worldeditadditions.parse.tokenise_commands(params_text)
if not success then return success, commands end
for i, command in ipairs(commands) do
print("[DEBUG] i", i, "command: '"..command.."'")
local start_time = worldeditadditions.get_ms_time() local start_time = worldeditadditions.get_ms_time()
local found, _, command_name, args = command:find("^([^%s]+)%s(.+)$") local found, _, command_name, args = command:find("^([^%s]+)%s(.+)$")
if not found then command_name = command end if not found then command_name = command end
command_name = trim(command_name) -- Things start at 1, not 0 in Lua :-(
command_name = worldeditadditions.trim(command_name):sub(2) -- Strip the leading /
if not args then args = "" end if not args then args = "" end
print("command_name", command_name)
worldedit.player_notify(name, "#"..i..": /"..command) worldedit.player_notify(name, "#"..i..": "..command)
local cmd = minetest.chatcommands[command_name] local cmd = minetest.chatcommands[command_name]
if not cmd then if not cmd then
return false, "Error: "..command_name.." isn't a valid command." return false, "Error: "..command_name.." isn't a valid command."
end end
if not minetest.check_player_privs(name, cmd.privs) then if not minetest.check_player_privs(name, cmd.privs) then
return false, "Your privileges are insufficient to execute /"..command_name..". Abort." return false, "Your privileges are insufficient to execute "..command_name..". Abort."
end end
-- print("[DEBUG] command_name", command_name, "cmd", dump2(cmd)) -- print("[DEBUG] command_name", command_name, "cmd", dump2(cmd))
minetest.log("action", name.." runs "..command) minetest.log("action", name.." runs "..command)
cmd.func(name, args) cmd.func(name, args)
times[#times + 1] = (worldeditadditions.get_ms_time() - start_time) times[#times + 1] = (worldeditadditions.get_ms_time() - start_time)
i = i + 1 -- i = i + 1
end end
local total_time = (worldeditadditions.get_ms_time() - master_start_time) local total_time = (worldeditadditions.get_ms_time() - master_start_time)