mirror of
https://github.com/sbrl/Minetest-WorldEditAdditions.git
synced 2024-11-26 17:12:59 +00:00
//nodeapply: Generalise matching engine into NodeListMatcher
Also add support for @groups, i.e. @crumbly matches nodes that are a member of the "crumbly" group This groups feature is untested atm as I need to implement //ninfo....
This commit is contained in:
parent
5bddeb5bb5
commit
050bd80cf3
5 changed files with 146 additions and 22 deletions
|
@ -8,7 +8,7 @@ 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!)
|
||||
- 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. Check out [the reference](https://worldeditadditions.mooncarrot.space/Reference/#nodeapply) - it has some cool tricks to it! (thanks for suggesting, @kliv91 from the Discord server!)
|
||||
|
||||
|
||||
## v1.14.5: The multipoint update, hotfix 5 (1st August 2023)
|
||||
|
|
|
@ -1333,6 +1333,14 @@ To give a further example, consider this:
|
|||
|
||||
...this will replace all liquid-like nodes (e.g. water, lava, etc) with river water.
|
||||
|
||||
In addition, any node names prefixed an at sign `@` are considered group names. For example: `@crumbly` would allow changes only to nodes that are a member of the `crumbly` group:
|
||||
|
||||
```weacmd
|
||||
//nodeapply @crumbly -- layers dirt_with_grass dirt 3 stone 10
|
||||
```
|
||||
|
||||
More misc examples to end this command description, as is customary:
|
||||
|
||||
```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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
local wea_c = worldeditadditions_core
|
||||
local Vector3 = wea_c.Vector3
|
||||
|
||||
local NodeListMatcher = wea_c.NodeListMatcher
|
||||
|
||||
|
||||
|
||||
|
@ -40,14 +40,8 @@ function worldeditadditions.nodeapply(pos1, pos2, nodelist, func)
|
|||
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 success, matcher = NodeListMatcher.new(nodelist)
|
||||
if not success then return success, matcher end
|
||||
|
||||
local allowed_changes = 0
|
||||
local denied_changes = 0
|
||||
|
@ -56,20 +50,9 @@ function worldeditadditions.nodeapply(pos1, pos2, nodelist, func)
|
|||
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
|
||||
local allow_replacement = matcher:match_id(data_before[i_before])
|
||||
|
||||
-- Roll back any changes that aren't allowed
|
||||
-- ...but ensure we only count changed nodes
|
||||
|
|
|
@ -29,6 +29,7 @@ wea_c.Mesh, wea_c.Face = dofile(wea_c.modpath.."/utils/mesh.lua")
|
|||
|
||||
wea_c.Queue = dofile(wea_c.modpath.."/utils/queue.lua")
|
||||
wea_c.LRU = dofile(wea_c.modpath.."/utils/lru.lua")
|
||||
wea_c.NodeListMatcher = dofile(wea_c.modpath.."/utils/NodeListMatcher.lua")
|
||||
wea_c.inspect = dofile(wea_c.modpath.."/utils/inspect.lua")
|
||||
|
||||
-- I/O compatibility layer
|
||||
|
|
132
worldeditadditions_core/utils/NodeListMatcher.lua
Normal file
132
worldeditadditions_core/utils/NodeListMatcher.lua
Normal file
|
@ -0,0 +1,132 @@
|
|||
local weac = worldeditadditions_core
|
||||
local table_map = dofile(weac.modpath.."/utils/table/table_map.lua")
|
||||
|
||||
--- Matcherer that compares node ids to given list of node names.
|
||||
-- This is a class so that caching can be done locally and per-task.
|
||||
-- @class worldeditadditions_core.NodeListMatcher
|
||||
local NodeListMatcher = {}
|
||||
NodeListMatcher.__index = NodeListMatcher
|
||||
NodeListMatcher.__name = "NodeListMatcher" -- A hack to allow identification in wea.inspect
|
||||
|
||||
local node_names_special = {
|
||||
airlike = function(node_id)
|
||||
return weac.is_airlike(node_id)
|
||||
end,
|
||||
liquidlike = function(node_id)
|
||||
return weac.is_liquidlike(node_id)
|
||||
end
|
||||
}
|
||||
|
||||
--- Parses a list of node names, accounting for special keywords and group names.
|
||||
-- @param nodelist string[] The list of nodes to parse.
|
||||
-- @returns bool,string|table<string|function|number> A success bool (true = success), followed by eitheran error string if success == false or the parsed nodelist if success == true.
|
||||
--
|
||||
-- Different data types in the parsed nodelist mean different things:
|
||||
--
|
||||
-- - **string:** The name of a group
|
||||
-- - **function:** A special function to call with the node id to match against as the first and only argument.
|
||||
-- - **number:** The id of the node to check against.
|
||||
local function __parse_nodelist(nodelist)
|
||||
local failed_msg = nil
|
||||
local parsed = table_map(nodelist, function(node_name)
|
||||
-- 1: match against defined special keywords
|
||||
if node_names_special[node_name] ~= nil then
|
||||
return node_names_special[node_name]
|
||||
end
|
||||
-- 2: Is it a group name? Group names start with an at sign @.
|
||||
if node_name:sub(1, 1) == "@" then
|
||||
return node_name:sub(2)
|
||||
end
|
||||
|
||||
-- 3: It's probably a node name then.
|
||||
local node_id = minetest.get_content_id(node_name)
|
||||
if node_id == nil then failed_msg = "Error: Failed to resolve node name '"..node_name.."' to a node id. This is probably a bug. Did you remember to normalise the input node names?" end
|
||||
|
||||
return node_id
|
||||
end)
|
||||
|
||||
if failed_msg ~= nil then return false, failed_msg end
|
||||
return true, parsed
|
||||
end
|
||||
|
||||
--- Creates a new NodeListMatcher instance with the given node list.
|
||||
-- Once created, you probably want to call the :match_id(node_id) function.
|
||||
-- @param nodelist string[] The list of nodes to match against.
|
||||
--
|
||||
-- `airlike` and `liquidlike` are special keywords that instead check if a given node id behaves like air or a liquid respectively.
|
||||
--
|
||||
-- Node names prefixed with an at sign (@) are considered group names, and given node ids are checked for membership of the given group.
|
||||
-- @returns bool,string|NodeListMatcher A success bool (true == success), and then either a string (if succes == false) or otherwise the newly created NodeListMatcher instance.
|
||||
function NodeListMatcher.new(nodelist)
|
||||
local success, nodelist_parsed = __parse_nodelist(nodelist)
|
||||
if not success then return success, nodelist_parsed end
|
||||
|
||||
local result = {
|
||||
-- The parsed nodelist
|
||||
nodelist = nodelist_parsed,
|
||||
-- The group cache, since node id → group membership checking requires 2 minetest.* calls.
|
||||
groupcache = {}
|
||||
}
|
||||
setmetatable(result, NodeListMatcher)
|
||||
return true, result
|
||||
end
|
||||
|
||||
--- Matches the given node id against the nodelist provided at the instantiation of this NodeListMatcher instance.
|
||||
-- Returns true if the given node_id matches against any one of the items in the nodelist. In other words, performs a logical OR operation.
|
||||
--
|
||||
-- We use the term 'item' and not 'node' here since not all items in the nodelist are nodes: nodelists support special keywords such as 'liquidlike' and 'airlike', as well as group names (prefixed with an at sign @).
|
||||
-- @param matcher NodeListMatcher The NodeListMatcher instance to query against. Use some_matcher:match_id(node_id) to avoid specifying this manually (note the colon : and not a dot . there).
|
||||
-- @param node_id number The numerical id of the node to match against the internal nodelist.
|
||||
-- @returns bool True if the given node id matches against any of the items in the nodelist.
|
||||
function NodeListMatcher.match_id(matcher, node_id)
|
||||
print("DEBUG matcher", weac.inspect(matcher))
|
||||
for i,target in ipairs(matcher.nodelist) do
|
||||
local target_type = type(target)
|
||||
if target_type == "number" then
|
||||
-- It's a node id!
|
||||
if target == node_id then return true end
|
||||
elseif target_type == "function" then
|
||||
-- It's a special function!
|
||||
local result = target(node_id)
|
||||
if result then return true end
|
||||
elseif target_type == "string" then
|
||||
local result = matcher:match_group(node_id, target)
|
||||
if result then return true end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- Determines if the given node id has the given group name.
|
||||
-- Caches the result for performance. You probably want NodeListMatcher:match_id(node_id).
|
||||
-- @param matcher NodeListMatcher The NodeListMatcher instance to use to make the query.
|
||||
-- @param node_id number The numerical id of the node to check.
|
||||
-- @param group_name string The name of the group to check if the given node id has.
|
||||
-- @returns bool True if the given node id belongs to the specified group, and false if it does not.
|
||||
function NodeListMatcher.match_group(matcher, node_id, group_name)
|
||||
-- 0: Preamble
|
||||
if matcher.groupcache[node_id] == nil then
|
||||
matcher.groupcache[node_id] = {}
|
||||
end
|
||||
|
||||
-- 1: Check the cache
|
||||
if matcher.groupcache[node_id][group_name] ~= nil then
|
||||
return matcher.groupcache[node_id][group_name]
|
||||
end
|
||||
|
||||
-- 2: Nope, not in the cache. Time to query!
|
||||
local node_name = minetest.get_name_from_content_id(node_id)
|
||||
local group_value = minetest.get_item_group(node_name, group_name)
|
||||
if group_value == 0 then group_value = false
|
||||
else group_value = true end
|
||||
|
||||
-- 3: Update the cache
|
||||
matcher.groupcache[node_id][group_name] = group_value
|
||||
|
||||
-- 4: Return the value now it's in the cache
|
||||
return group_value
|
||||
end
|
||||
|
||||
|
||||
|
||||
return NodeListMatcher
|
Loading…
Reference in a new issue