From 28ed864c84e93c0f8513043a0a395eee4dce13fd Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Sun, 26 Nov 2023 22:20:05 +0000 Subject: [PATCH] Added //nodeapply to filter cmd changes via a nodelist --- CHANGELOG.md | 3 +- Chat-Command-Reference.md | 32 +++++ worldeditadditions/init.lua | 1 + worldeditadditions/lib/nodeapply.lua | 101 +++++++++++++++ .../commands/meta/init.lua | 1 + .../commands/meta/nodeapply.lua | 121 ++++++++++++++++++ 6 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 worldeditadditions/lib/nodeapply.lua create mode 100644 worldeditadditions_commands/commands/meta/nodeapply.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 30f51fe..1c0bdaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,12 @@ Note to self: See the bottom of this file for the release template text. - Added the optional argument `all` to [`//unmark`](https://worldeditadditions.mooncarrot.space/Reference/#unmark) - Added a (rather nuclear) fix (attempt 4) at finally exterminating all zombie region marker walls forever - This is not a hotfix to avoid endless small releases fixing the bug, as it's clear it's much more difficult to fix on all systems than initially expected +- Added [`//nodeapply`](https://worldeditadditions.mooncarrot.space/Reference/#nodeapply), a generalisation of [`//airapply`](https://worldeditadditions.mooncarrot.space/Reference/#airapply) that works with a defined list of nodes (thanks for suggesting, @kliv91 from the Discord server!) ## v1.14.5: The multipoint update, hotfix 5 (1st August 2023) - Fix a bug where creative players in survival couldn't punch out position markers -- Added `//listentities`, which lists all currently loaded `ObjectRef`s. This is intended for debugging mods - thanks to @Zughy in #103 +- Added [`//listentities`](https://worldeditadditions.mooncarrot.space/Reference/#listentities), which lists all currently loaded `ObjectRef`s. This is intended for debugging mods - thanks to @Zughy in #103 ## v1.14.4: The multipoint update, hotfix 4 (31st July 2023) diff --git a/Chat-Command-Reference.md b/Chat-Command-Reference.md index 95fd7e2..a7581c0 100644 --- a/Chat-Command-Reference.md +++ b/Chat-Command-Reference.md @@ -1309,6 +1309,38 @@ As with `//ellipsoidapply` for advanced users `//multi` is also supported - but ``` +### `//nodeapply [] [... ] -- ` +It's got `apply` in the name, so as you might imagine it works the same as [`//ellipsoidapply`](#ellipsoidapply), [`//airapply`](#airapply), [`//noiseapply2d`](#noiseapply2d), etc. Only changes made by the given command that replace nodes on the list given will be replaced. For example: + +```weacmd +//nodeapply dirt -- set stone +``` + +....is equivalent to `//replace dirt stone`, in that although `//set stone` will set all nodes to stone, `//nodeapply` will only keep the changes made by `//set` that affect dirt. + +There are some special keywords you can use too: + +Keyword | Meaning +----------------|----------------------------------- +`liquidlike` | Targets all nodes that behave like a liquid. +`airlike` | Targets all nodes that behave like air. Basically like [`//airapply`](#airapply). + +To give a further example, consider this: + +```weacmd +//nodeapply liquidlike -- set river_water_source +``` + +...this will replace all liquid-like nodes (e.g. water, lava, etc) with river water. + +```weacmd +//nodeapply stone -- layers dirt_with_grass dirt 3 +//nodeapply stone dirt sand -- layers bakedclay:natural 3 bakedclay:orange 2 bakedclay:red 3 bakedclay:natural 3 +//nodeapply liquidlike -- set air +//nodeapply airlike -- mix stone 3 dirt 2 +``` + + ### `//noiseapply2d ` Like [`//ellipsoidapply`](#ellipsoidapply), but instead only keeps changes where a noise function (defaults to `perlinmt`, see [`//noise2d`](#noise2d)) returns a value greater than a given threshold value. diff --git a/worldeditadditions/init.lua b/worldeditadditions/init.lua index efcb28e..48997be 100644 --- a/worldeditadditions/init.lua +++ b/worldeditadditions/init.lua @@ -48,6 +48,7 @@ dofile(wea.modpath.."/lib/forest.lua") dofile(wea.modpath.."/lib/ellipsoidapply.lua") dofile(wea.modpath.."/lib/airapply.lua") +dofile(wea.modpath.."/lib/nodeapply.lua") dofile(wea.modpath.."/lib/noiseapply2d.lua") dofile(wea.modpath.."/lib/subdivide.lua") diff --git a/worldeditadditions/lib/nodeapply.lua b/worldeditadditions/lib/nodeapply.lua new file mode 100644 index 0000000..d09ee2c --- /dev/null +++ b/worldeditadditions/lib/nodeapply.lua @@ -0,0 +1,101 @@ +local wea_c = worldeditadditions_core +local Vector3 = wea_c.Vector3 + + + + +-- ███ ██ ██████ ██████ ███████ +-- ████ ██ ██ ██ ██ ██ ██ +-- ██ ██ ██ ██ ██ ██ ██ █████ +-- ██ ██ ██ ██ ██ ██ ██ ██ +-- ██ ████ ██████ ██████ ███████ +-- +-- █████ ██████ ██████ ██ ██ ██ +-- ██ ██ ██ ██ ██ ██ ██ ██ ██ +-- ███████ ██████ ██████ ██ ████ +-- ██ ██ ██ ██ ██ ██ +-- ██ ██ ██ ██ ███████ ██ + +--- Like ellipsoidapply and airapply, but much more flexible, allowing custom sets of nodes to filter changes on. Any changes that don't replace nodes that match the given nodelist will be discarded. +-- Takes a backup copy of the defined region, runs the given function, and then +-- restores the bits around the edge that aren't inside the largest ellipsoid that will fit inside the defined region. +-- @param pos1 Vector3 The 1st position defining the region boundary +-- @param pos2 Vector3 The 2nd positioon defining the region boundary +-- @param nodelist string[] The nodelist to match changes against. Any changes that don't replace nodes on this list will be discarded. The following special node names are also accepted: liquid, air. Note that all node names MUST be normalised, otherwise they won't be recognised! +-- @param func function The function to call that performs the action in question. It is expected that the given function will accept no arguments. +function worldeditadditions.nodeapply(pos1, pos2, nodelist, func) + local time_taken_all = wea_c.get_ms_time() + pos1, pos2 = Vector3.sort(pos1, pos2) + -- pos2 will always have the highest co-ordinates now + + -- Fetch the nodes in the specified area + local manip_before, area_before = worldedit.manip_helpers.init(pos1, pos2) + local data_before = manip_before:get_data() + + local time_taken_fn = wea_c.get_ms_time() + func() + time_taken_fn = wea_c.get_ms_time() - time_taken_fn + + local manip_after, area_after = worldedit.manip_helpers.init(pos1, pos2) + local data_after = manip_after:get_data() + + -- Cache node ids for speed. Even if minetest.get_content_id is efficient, an extra function call is still relatively expensive when called 10K+ times on a large region. + local nodeids = {} + for i,nodename in ipairs(nodelist) do + if nodename == "liquidlike" or nodename == "airlike" then + table.insert(nodeids, nodename) + else + table.insert(nodeids, minetest.get_content_id(nodename)) + end + end + + local allowed_changes = 0 + local denied_changes = 0 + for z = pos2.z, pos1.z, -1 do + for y = pos2.y, pos1.y, -1 do + for x = pos2.x, pos1.x, -1 do + local i_before = area_before:index(x, y, z) + local i_after = area_after:index(x, y, z) + local old_is_airlike = wea_c.is_airlike(data_before[i_before]) + + -- Filter on the list of node ids + local allow_replacement = false + for i,nodeid in ipairs(nodeids) do + if nodeid == "airlike" then + allow_replacement = wea_c.is_airlike(data_before[i_before]) + elseif nodeid == "liquidlike" then + allow_replacement = wea_c.is_liquidlike(data_before[i_before]) + else + allow_replacement = data_before[i_before] == nodeid + end + if allow_replacement then break end + end + + -- Roll back any changes that aren't allowed + -- ...but ensure we only count changed nodes + if not allow_replacement then + if data_after[i_after] ~= data_before[i_before] then + allowed_changes = allowed_changes + 1 + end + -- Roll back + data_after[i_after] = data_before[i_before] + elseif data_after[i_after] ~= data_before[i_before] then + denied_changes = denied_changes + 1 + end + end + end + end + + -- Save the modified nodes back to disk & return + -- No need to save - this function doesn't actually change anything + worldedit.manip_helpers.finish(manip_after, data_after) + + + time_taken_all = wea_c.get_ms_time() - time_taken_all + return true, { + all = time_taken_all, + fn = time_taken_fn, + allowed_changes = allowed_changes, + denied_changes = denied_changes + } +end diff --git a/worldeditadditions_commands/commands/meta/init.lua b/worldeditadditions_commands/commands/meta/init.lua index 15ad16c..1c34fba 100644 --- a/worldeditadditions_commands/commands/meta/init.lua +++ b/worldeditadditions_commands/commands/meta/init.lua @@ -8,6 +8,7 @@ local we_cmdpath = worldeditadditions_commands.modpath .. "/commands/meta/" +dofile(we_cmdpath.."nodeapply.lua") dofile(we_cmdpath.."airapply.lua") dofile(we_cmdpath.."ellipsoidapply.lua") dofile(we_cmdpath.."for.lua") diff --git a/worldeditadditions_commands/commands/meta/nodeapply.lua b/worldeditadditions_commands/commands/meta/nodeapply.lua new file mode 100644 index 0000000..3907dff --- /dev/null +++ b/worldeditadditions_commands/commands/meta/nodeapply.lua @@ -0,0 +1,121 @@ +local wea_c = worldeditadditions_core +local Vector3 = wea_c.Vector3 + +--- Performs initial parsing of params_text for //nodeapply. +-- @param params_text string The arguments to //nodeapply to parse. +-- @returns bool,string,string,string? 1. Success bool (true = success) +-- 2. Error message if success bool == false, otherwise the string from before the delimiter +-- 3. The command name +-- 4. Any arguments to pass to the child command +function extract_parts(params_text) + -- 1: Find delimiter + local index, _, match = string.find(params_text, "(%s+--%s+)") + if index == nil then + return false, "Error: Could not find double-dash ( -- ) separator. Please ensure the double dashes have at least 1 whitespace character either side." + end + + -- 2: Split into before / after delimiter + local before = params_text:sub(1, index) + local after = params_text:sub(index + match:len()) + + -- 3: Wrangle command name and optional args + local cmd_name, args_text = string.match(after, "([^%s]+)%s+(.+)") + if not cmd_name then + cmd_name = after + args_text = "" + end + + -- 4: Return + return true, before, cmd_name, args_text +end + +-- ███ ██ ██████ ██████ ███████ █████ ██████ ██████ ██ ██ ██ +-- ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +-- ██ ██ ██ ██ ██ ██ ██ █████ ███████ ██████ ██████ ██ ████ +-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +-- ██ ████ ██████ ██████ ███████ ██ ██ ██ ██ ███████ ██ +worldeditadditions_core.register_command("nodeapply", { + params = " [] [... ] -- ", + description = "Executes the given command (automatically prepending '//'), but filters the output so only changes that affect the specified list of nodes are kept. Special node names: airlike, liquidlike.", + privs = { worldedit = true }, + require_pos = 2, + parse = function(params_text) + if params_text == "" then return false, "Error: No command specified." end + + --- 0: Break the args apart + local success, before, cmd_name, args_text = extract_parts(params_text) + -- local cmd_name, args_text = params_text:match("([^%s]+)%s+(.+)") + + if not success then return success, before end + + --- 1: Parse the node list + local parts = wea_c.split_shell(before) + local nodelist = {} + + for i,part in ipairs(parts) do + if part == "airlike" or part == "liquidlike" then + table.insert(nodelist, part) + else + local nodeid = worldedit.normalize_nodename(part) + if not nodeid then + return false, "Error: Unknown node name '"..part.."' at position "..tostring(i).." in node list." + end + table.insert(nodelist, part) + end + end + + --- 2: Parse the cmdname & args + + -- Note that we search the worldedit commands here, not the minetest ones + local cmd_we = wea_c.fetch_command_def(cmd_name) + if cmd_we == nil then + return false, "Error: "..cmd_name.." isn't a valid command." + end + if cmd_we.require_pos ~= 2 and cmd_name ~= "multi" then + return false, "Error: The command "..cmd_name.." exists, but doesn't take 2 positions and so can't be used with //airapply ('cause we can't tell how big the area is that it replaces)." + end + + -- 3: Get target command to parse args + -- Lifted from cubeapply in WorldEdit + local args_parsed = { cmd_we.parse(args_text) } + if not table.remove(args_parsed, 1) then + return false, args_parsed[1] + end + + return true, nodelist, cmd_we, args_parsed + end, + nodes_needed = function(name) + return worldedit.volume( + worldedit.pos1[name], + worldedit.pos2[name] + ) + end, + func = function(name, nodelist, cmd, args_parsed) + if not minetest.check_player_privs(name, cmd.privs) then + return false, "Your privileges are insufficient to execute the command '"..cmd.."'." + end + + local pos1, pos2 = Vector3.sort( + worldedit.pos1[name], + worldedit.pos2[name] + ) + + + local success, stats = worldeditadditions.nodeapply( + pos1, pos2, + nodelist, + function() + cmd.func(name, wea_c.table.unpack(args_parsed)) + end + ) + if not success then return success, stats end + + + local time_overhead = 100 - wea_c.round((stats.fn / stats.all) * 100, 3) + local text_time_all = wea_c.format.human_time(stats.all) + local text_time_fn = wea_c.format.human_time(stats.fn) + + minetest.log("action", name.." used //nodeapply at "..pos1.." - "..pos2.." in "..text_time_all) + return true, tostring(stats.allowed_changes).." changes allowed, "..tostring(stats.denied_changes).." filtered in "..text_time_all.." ("..text_time_fn.." fn, "..time_overhead.."% nodeapply overhead)" + end +})