diff --git a/CHANGELOG.md b/CHANGELOG.md index 0719670..1951984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) - 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 `//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) @@ -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 - `//torus`, `//hollowtorus`: Add optional new axes - `//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) @@ -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) - `//many` can now be used with commands with no arguments. - `//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 @@ -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 - 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. diff --git a/Chat-Command-Reference.md b/Chat-Command-Reference.md index 90682e9..87f9ff2 100644 --- a/Chat-Command-Reference.md +++ b/Chat-Command-Reference.md @@ -408,14 +408,17 @@ The sigma value is only applicable to the `gaussian` kernel, and can be thought //convolve gaussian 5 0.2 ``` -## `//erode [ [ []] [ []] ...]` +## `//erode [ [ []] [ []] ...]` 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: 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). +`river` | 2D | Fills in potholes and lowers pillars using a cellular automata-like algorithm that analyses the height of neighouring columns. 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/). Parameter | Type | Default Value | Description ---------------------|-----------|-------------------|------------------------ +--------------------|-----------|-------------------|-------------------------- 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 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. 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. +### 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` 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 .....` 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 //hollowtorus 30 5 stone //hollowtorus 20 3 dirt //torus 10 2 dirt_with_grass diff --git a/README.md b/README.md index a315291..6077452 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ The detailed explanations have moved! Check them out [here](https://github.com/s - [`//layers [ []] [ []] ...`](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 []`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#fillcaves-node_name) - [`//convolve [[,]] []`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#convolve-kernel-widthheight-sigma) - - [`//erode [ [ []] [ []] ...]`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#erode-snowballs-key_1-value_1-key_2-value_2-) **experimental** + - [`//erode [ [ []] [ []] ...]`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#erode-snowballsriver-key_1-value_1-key_2-value_2-) **experimental** ### Flora - [`//bonemeal [ []]`](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#bonemeal-strength-chance) diff --git a/screenshot2.png b/screenshot2.png new file mode 100644 index 0000000..86802ff Binary files /dev/null and b/screenshot2.png differ diff --git a/worldeditadditions/lib/erode/erode.lua b/worldeditadditions/lib/erode/erode.lua index cd7466d..76bd0e5 100644 --- a/worldeditadditions/lib/erode/erode.lua +++ b/worldeditadditions/lib/erode/erode.lua @@ -1,6 +1,7 @@ worldeditadditions.erode = {} dofile(worldeditadditions.modpath.."/lib/erode/snowballs.lua") +dofile(worldeditadditions.modpath.."/lib/erode/river.lua") 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) local success, msg, stats 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 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 success, stats = worldeditadditions.apply_heightmap_changes( diff --git a/worldeditadditions/lib/erode/river.lua b/worldeditadditions/lib/erode/river.lua new file mode 100644 index 0000000..9c6ea3f --- /dev/null +++ b/worldeditadditions/lib/erode/river.lua @@ -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 diff --git a/worldeditadditions/lib/erode/snowballs.lua b/worldeditadditions/lib/erode/snowballs.lua index 34fce59..10c70b3 100644 --- a/worldeditadditions/lib/erode/snowballs.lua +++ b/worldeditadditions/lib/erode/snowballs.lua @@ -86,7 +86,7 @@ function worldeditadditions.erode.snowballs(heightmap_initial, heightmap, height maxdiff = 0.4, count = 25000 } - -- Apply the default settings + -- Apply the custom settings worldeditadditions.table_apply(params_custom, params) -- print("[erode/snowballs] params: ") @@ -132,7 +132,7 @@ function worldeditadditions.erode.snowballs(heightmap_initial, heightmap, height if not params.noconv then local success, matrix = worldeditadditions.get_conv_kernel("gaussian", 3, 3) 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( heightmap, heightmap_size, matrix, diff --git a/worldeditadditions/utils/format/array_2d.lua b/worldeditadditions/utils/format/array_2d.lua index 8af7a8a..5e940d1 100644 --- a/worldeditadditions/utils/format/array_2d.lua +++ b/worldeditadditions/utils/format/array_2d.lua @@ -3,7 +3,7 @@ -- @param tbl number[] The ZERO-indexed list of numbers -- @param width number The width of 2D array. function worldeditadditions.format.array_2d(tbl, width) - print("==== count: "..#tbl..", width:"..width.." ====") + print("==== count: "..(#tbl+1)..", width:"..width.." ====") local display_width = 1 for _i,value in pairs(tbl) do display_width = math.max(display_width, #tostring(value)) diff --git a/worldeditadditions/utils/numbers.lua b/worldeditadditions/utils/numbers.lua index 9bb8d77..ce56a8f 100644 --- a/worldeditadditions/utils/numbers.lua +++ b/worldeditadditions/utils/numbers.lua @@ -19,12 +19,43 @@ function worldeditadditions.sum(list) return sum 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) if #list == 0 then return 0 end return worldeditadditions.sum(list) / #list 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 -- @return float function worldeditadditions.get_ms_time() diff --git a/worldeditadditions/utils/parse/init.lua b/worldeditadditions/utils/parse/init.lua index 9ce52be..27c9336 100644 --- a/worldeditadditions/utils/parse/init.lua +++ b/worldeditadditions/utils/parse/init.lua @@ -4,3 +4,4 @@ dofile(worldeditadditions.modpath.."/utils/parse/chance.lua") dofile(worldeditadditions.modpath.."/utils/parse/map.lua") dofile(worldeditadditions.modpath.."/utils/parse/seed.lua") dofile(worldeditadditions.modpath.."/utils/parse/weighted_nodes.lua") +dofile(worldeditadditions.modpath.."/utils/parse/tokenise_commands.lua") diff --git a/worldeditadditions/utils/parse/map.lua b/worldeditadditions/utils/parse/map.lua index ce7cef6..aa35983 100644 --- a/worldeditadditions/utils/parse/map.lua +++ b/worldeditadditions/utils/parse/map.lua @@ -1,7 +1,7 @@ --- 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 --- the following table: { count = 25000, speed = 0.8, rate_erosion = 0.006 }. +-- 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, doawesome = true }. -- @param params_text string The string to parse. -- @returns table A table of key-value pairs parsed out from the given string. 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 local part_converted = tonumber(part) 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 else last_key = part diff --git a/worldeditadditions/utils/parse/tokenise_commands.lua b/worldeditadditions/utils/parse/tokenise_commands.lua new file mode 100644 index 0000000..2a96746 --- /dev/null +++ b/worldeditadditions/utils/parse/tokenise_commands.lua @@ -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") diff --git a/worldeditadditions/utils/strings/polyfill.lua b/worldeditadditions/utils/strings/polyfill.lua index d362353..d08edc3 100644 --- a/worldeditadditions/utils/strings/polyfill.lua +++ b/worldeditadditions/utils/strings/polyfill.lua @@ -22,8 +22,8 @@ end -- @param str string The string to operate on -- @param start number The start string to look for -- @returns bool Whether start is present at the beginning of str -function worldeditadditions.str_starts(str,start) - return string.sub(str,1,string.len(start))==start +function worldeditadditions.str_starts(str, start) + return string.sub(str, 1, string.len(start)) == start end --- Trims whitespace from a string from the beginning and the end. diff --git a/worldeditadditions/utils/strings/split.lua b/worldeditadditions/utils/strings/split.lua index 90e2b6d..69e5518 100644 --- a/worldeditadditions/utils/strings/split.lua +++ b/worldeditadditions/utils/strings/split.lua @@ -42,15 +42,12 @@ function worldeditadditions.gsplit(text, pattern, plain) end --- split: split a string into substrings separated by a pattern. --- --- Parameters: --- text (string) - the string to iterate over --- pattern (string) - the separator pattern --- plain (boolean) - if true (or truthy), pattern is interpreted as a plain --- string, not a Lua pattern --- --- Returns: table (a sequence table containing the substrings) +-- Split a string into substrings separated by a pattern. +-- @param text string The string to iterate over +-- @param pattern string The separator pattern +-- @param plain boolean If true (or truthy), pattern is interpreted as a +-- plain string, not a Lua pattern +-- @returns table A sequence table containing the substrings function worldeditadditions.split(text, pattern, plain) local ret = {} for match in worldeditadditions.gsplit(text, pattern, plain) do diff --git a/worldeditadditions/utils/tables.lua b/worldeditadditions/utils/tables.lua index e20bd63..a68c9ea 100644 --- a/worldeditadditions/utils/tables.lua +++ b/worldeditadditions/utils/tables.lua @@ -65,3 +65,53 @@ function worldeditadditions.table_tostring(tbl, sep, new_line) end return ret:concat("") 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|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: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 diff --git a/worldeditadditions_commands/commands/erode.lua b/worldeditadditions_commands/commands/erode.lua index 807d2ce..d41df3e 100644 --- a/worldeditadditions_commands/commands/erode.lua +++ b/worldeditadditions_commands/commands/erode.lua @@ -4,7 +4,7 @@ -- ██ ██ ██ ██ ██ ██ ██ ██ -- ███████ ██ ██ ██████ ██████ ███████ worldedit.register_command("erode", { - params = "[ [ []] [ []] ...]", + params = "[ [ []] [ []] ...]", 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 }, require_pos = 2, diff --git a/worldeditadditions_commands/commands/meta/multi.lua b/worldeditadditions_commands/commands/meta/multi.lua index c63da7d..81fec34 100644 --- a/worldeditadditions_commands/commands/meta/multi.lua +++ b/worldeditadditions_commands/commands/meta/multi.lua @@ -26,11 +26,6 @@ local function explode(delim, str) end end --- From http://lua-users.org/wiki/StringTrim -local function trim(s) - return (s:gsub("^%s*(.-)%s*$", "%1")) -end - minetest.register_chatcommand("/multi", { params = "/ // / .....", 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 times = {} - -- Things start at 1, not 0 in Lua :-( - for command in explode(" /", string.sub(params_text, 2)) do + -- Tokenise the input into a list of commands + 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 found, _, command_name, args = command:find("^([^%s]+)%s(.+)$") 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 + print("command_name", command_name) - worldedit.player_notify(name, "#"..i..": /"..command) + worldedit.player_notify(name, "#"..i..": "..command) local cmd = minetest.chatcommands[command_name] if not cmd then return false, "Error: "..command_name.." isn't a valid command." end 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 -- print("[DEBUG] command_name", command_name, "cmd", dump2(cmd)) minetest.log("action", name.." runs "..command) cmd.func(name, args) times[#times + 1] = (worldeditadditions.get_ms_time() - start_time) - i = i + 1 + -- i = i + 1 end local total_time = (worldeditadditions.get_ms_time() - master_start_time)