Added //nodeapply to filter cmd changes via a nodelist

This commit is contained in:
Starbeamrainbowlabs 2023-11-26 22:20:05 +00:00
parent 342357e9de
commit 28ed864c84
No known key found for this signature in database
GPG Key ID: 1BE5172E637709C2
6 changed files with 258 additions and 1 deletions

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

@ -1309,6 +1309,38 @@ As with `//ellipsoidapply` for advanced users `//multi` is also supported - but
```
### `//nodeapply <node_a> [<node_b>] [... <node_N>] -- <command_name> <args>`
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 <threshold> <scale> <command_name> <args>`
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.

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

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

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

@ -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 = "<node_a> [<node_b>] [... <node_N>] -- <command_name> <args>",
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
})