@@ -1,4 +1,5 @@ +local log = sorcery.logger('lib.node') local ofs = { neighbors = { {x = 1, y = 0, z = 0}; {x = -1, y = 0, z = 0}; @@ -58,8 +59,59 @@ end end ::skip::end end; + +local force = function(pos,preload_for) + local n = minetest.get_node_or_nil(pos) + if preload_for then sorcery.lib.node.preload(pos,preload_for) end + if n then return n end + + minetest.load_area(pos) + return minetest.get_node(pos) +end; + +local amass = function(startpoint,names,directions) + if not directions then directions = ofs.neighbors end + local check = function(n) + return sorcery.lib.tbl.has(names, n.name, function(check,against) + return sorcery.lib.item.groupmatch(against,check) + end)-- match found + end + if type(names) == 'function' then check = names end + local nodes, positions, checked = {},{},{} + local checkedp = function(pos) + for _,v in pairs(checked) do + if vector.equals(pos,v) then return true end + end + return false + end + local i,stack = 1,{startpoint} repeat + local pos = stack[i] + local n = force(pos) + if check(n, pos, nodes, positions) then + -- record the find + nodes[pos] = n.name + if positions[n.name] + then positions[n.name][#positions[n.name]+1] = pos + else positions[n.name] = {pos} + end + + -- check selected neighbors to see if any need scanning + for _,d in pairs(directions) do + local sum = vector.add(pos, d) + if not checkedp(sum) then + stack[#stack + 1] = sum + checked[#checked+1] = sum + end + end + end + checked[#checked+1] = pos + i = i + 1 + until i > #stack + return nodes, positions +end; + return { offsets = ofs; purge_container = function(...) return purge_container(nil, ...) end; purge_only = function(lst) @@ -68,9 +120,9 @@ end end; is_air = function(pos) - local n = sorcery.lib.node.force(pos) + local n = force(pos) if n.name == 'air' then return true end local d = minetest.registered_nodes[n.name] if not d then return false end return (d.walkable == false) and (d.drawtype == 'airlike' or d.buildable_to == true) @@ -81,8 +133,163 @@ local ents = minetest.get_objects_inside_radius(pos,0.5) if #ents > 0 then return false end return true end; + + tree_is_live = function(pos, checklight) -- VERY EXPENSIVE FUNCTION + -- this is going to require some explanation. + -- + -- for various purposes, we want to be able to tell the difference between + -- a tree that has grown naturally from the grown vs. a couple of trunk nodes + -- that the player has jammed together, even if she's built her own counterfeit + -- tree. unfortunately, mtg provides no easy way to do this. the only + -- difference between a cluster of trunk blocks and a real tree is that the + -- real tree will have a specific kind of leaves attached with their param2 + -- set to 1 so that they can be distinguished for the purpose of leaf cleaning. + -- so to check a tree's state, we need to amass its whole potential body, and if + -- there are legitimate leaves connected, then we identify it as a legit tree. + -- + -- couple of caveats. firstly, we need to prevent the user from faking a tree + -- simply by using a chain of leaf nodes to connect a fraudulent tree to a + -- Genuine Arboreal® Product™. this means we can't just use a naive amass() + -- call, and instead need to define our own checking function. this allows us + -- to eliminate nonliving leaf nodes in the mass-collection process, so that + -- they can't be used to "branch" (hurr hurr geddit) off to a real tree. + -- + -- secondly, we want this to work with trees that aren't necessarily known to + -- the sorcery mod, so we can't rely on the tree register. we'll use it when we + -- can, but otherwise we'll have to guesstimate the correct leaf node. we do + -- this with a stateful closure, by saving the name of the first living leaf + -- node we see, and then referring back to it from that point on. this is ugly + -- but covers all but pathological edge cases. + -- + -- finally, the trunk itself is basically inert. the user can mine and then + -- replace as many trunk blocks as he likes without "killing" the tree by + -- this function's estimation. there is a way around this but it would require + -- hooking the on_dignode global callback to invalidate leaf bodies, and this + -- is way too trivial and niche a use for such a performance-critical function, + -- especially since it would involve calling amass on trees for *every node you + -- dig*. imagine O(chopping down an emergent jungle tree) with something like + -- *that* hooked! not on my watch, pal. + -- + -- however, there is one problem we have to deal with, and unfortunately there + -- is no good solution. the user can still attach new trunk blocks to living + -- leaves and get extra tree to work with, e.g. for sap generation. this is + -- l33t hax and we don't want it, but preventing it is nontrivial. the best i + -- can come up with for now is hooking the after_place_node functions of + -- known trees to set a metadata key excluding them from amassing if they're + -- positioned near a relevant leaf node. this is ugly and not very efficient, + -- and if you have a better idea i'd love to hear it. apart from no back-compat + -- for existing maps, it also fails to address + -- two edge cases: + -- - a sapling grows such that its leaf nodes connect to a fake trunk + -- - a non-growth trunk node is inserted by another mod that fails to use + -- the place function and attribute the call to a user + -- + -- various problems could be avoided by unconditionally inserted the meta key, + -- or inserting it also when it comes into contact with another trunk node, + -- but pepole use these things to build with and that is just way way too many + -- meta keys for me to consider it an option. + -- + -- verdict: not very good, but decent enough for most cases. mtg should have + -- done better than this, but now we're all stuck with their bullshit + + local treetype = force(pos).name + if minetest.get_item_group(treetype, 'tree') == 0 then -- sir this is not a tree + return nil -- 無 + end + local treedef = sorcery.lib.tbl.select(sorcery.data.trees, 'node', treetype) + local leaftype = treedef and treedef.leaves or nil + local uppermost, lowermost + + local treemap, treenodes = amass(pos,function(node, where) + if node.name == treetype then + -- abuse predicate function to also track y minimum, so we can + -- avoid iterating over it all later again -- this function is + -- expensive enough already + if (not lowermost) or where.y < lowermost then + lowermost = where.y + end + if (not uppermost) or where.y > uppermost then + uppermost = where.y + end + local m=minetest.get_meta(where) + if m:get_int('sorcery:trunk_node_role') ~= 1 then + return true + else + log.warn('found a log node!') + return false + end + end + if leaftype == nil then + if minetest.get_item_group(node.name, 'leaves') ~= 0 and node.param2 == 0 then + log.warn('guessing leaf node for tree',treetype,'is',node.name,'; please report this bug to the mod responsible for this tree and ask for appropriate Sorcery interop code to be added') + leaftype = node.name + return true + end + elseif leaftype == node.name and node.param2 == 0 then + return true + end + return false + end,ofs.adjoining) + + if leaftype == nil then return false end + + local trunkmap, trunknodes = amass(pos, {treetype}, ofs.adjoining) + if treenodes[leaftype] == nil then return false end + + local cache = {} + local uppermost_check_leaves = true + local topnode + for _, p in pairs(treenodes[treetype]) do + -- if not sorcery.lib.tbl.select(trunknodes[treetype], function(v) + -- return vector.equals(p, v) + -- end, cache) then + -- log.act('tree node', p, 'not accounted for in trunk!') + -- return false + -- end + if p.y == uppermost and uppermost_check_leaves then + topnode = p + uppermost_check_leaves = false + end + if p.y == lowermost then + -- this is the bottom of the tree, bail if it's in not full contact + -- with soil or other eligible nodes as determined by the tree def's + -- 'rooted' predicate + local beneath = vector.offset(p, 0,-1,0); + if treedef.rooted then + if not treedef.rooted { + trunk = p; + groundpos = beneath; + ground = force(beneath); + treemap = treemap; + treenodes = treenodes; + } then return false end + else + if minetest.get_item_group(force(beneath).name, 'soil') == 0 then + return false + end + end + end + end + + if uppermost_check_leaves then + for _,p in pairs(treenodes[leaftype]) do + if p.y == uppermost then + topnode = p + break + end + end + end + -- + --make sure the tree gets enough light + if checklight and minetest.get_natural_light(vector.offset(topnode,0,1,0), 0.5) < 13 then return false end + + -- other possible checks: make sure all ground-touching nodes are directly + -- adjacent + + return true, {map = treemap, nodes = treenodes, trunk = treetype, leaves = leaftype, topnode = topnode} + end; get_arrival_point = function(pos) local try = function(p) local air = sorcery.lib.node.is_clear @@ -99,41 +306,8 @@ do local t = try(p) if t then return t end end end end; - amass = function(startpoint,names,directions) - if not directions then directions = ofs.neighbors end - local nodes, positions, checked = {},{},{} - local checkedp = function(pos) - for _,v in pairs(checked) do - if vector.equals(pos,v) then return true end - end - return false - end - local i,stack = 1,{startpoint} repeat - local pos = stack[i] - local n = sorcery.lib.node.force(pos).name - if sorcery.lib.tbl.has(names, n, function(check,against) - return sorcery.lib.item.groupmatch(against,check) - end) then -- match found - -- record the find - nodes[pos] = n - if positions[n] then positions[n][#positions[n]] = pos - else positions[n] = {pos} end - - -- check selected neighbors to see if any need scanning - for _,d in pairs(directions) do - local sum = vector.add(pos, d) - if not checkedp(sum) then - stack[#stack + 1] = sum - end - end - end - checked[#checked+1] = pos - i = i + 1 - until i > #stack - return nodes, positions - end; forneighbor = function(pos, n, fn) for _,p in pairs(n) do local sum = vector.add(pos, p) @@ -145,16 +319,9 @@ fn(sum, n) end end; - force = function(pos,preload_for) - local n = minetest.get_node_or_nil(pos) - if preload_for then sorcery.lib.node.preload(pos,preload_for) end - if n then return n end - - minetest.load_area(pos) - return minetest.get_node(pos) - end; + force = force; -- when items have already been removed; notify cannot be relied on -- to reach the entire network; this function accounts for the gap notifyneighbors = function(pos) @@ -178,9 +345,9 @@ user:send_mapblock(sorcery.lib.node.blockpos(pos)) end; discharger = function(pos) - local below = sorcery.lib.node.force(vector.subtract(pos,{x=0,y=1,z=0})) + local below = force(vector.subtract(pos,{x=0,y=1,z=0})) if below.name == 'hopper:hopper' or below.name == 'hopper:hopper_side' then local hopper = minetest.get_meta(below):get_inventory() return function(i)