local log = sorcery.logger('lib.node')
local ofs = {
neighbors = {
{x = 0, y = 1, z = 0};
{x = 0, y = -1, z = 0};
{x = 1, y = 0, z = 0};
{x = -1, y = 0, z = 0};
{x = 0, y = 0, z = 1};
{x = 0, y = 0, z = -1};
};
corners = {
{x = 1, y = 0, z = 1};
{x = -1, y = 0, z = 1};
{x = -1, y = 0, z = -1};
{x = 1, y = 0, z = -1};
};
planecorners = {
{x = 1, y = 0, z = 1};
{x = -1, y = 0, z = 1};
{x = -1, y = 0, z = -1};
{x = 1, y = 0, z = -1};
{x = 1, y = 1, z = 0};
{x = -1, y = 1, z = 0};
{x = -1, y = -1, z = 0};
{x = 1, y = -1, z = 0};
};
cubecorners = {
{x = 1, y = 1, z = 1};
{x = -1, y = 1, z = 1};
{x = -1, y = -1, z = 1};
{x = -1, y = -1, z = -1};
{x = 1, y = -1, z = -1};
{x = 1, y = 1, z = -1};
{x = 1, y = -1, z = 1};
{x = -1, y = 1, z = -1};
};
nextto = {
{x = 1, y = 0, z = 0};
{x = -1, y = 0, z = 0};
{x = 0, y = 0, z = 1};
{x = 0, y = 0, z = -1};
};
cardinal = {
{x = 1, y = 0, z = 0};
{x = -1, y = 0, z = 0};
{x = 0, y = 0, z = 1};
{x = 0, y = 0, z = -1};
};
}
ofs.adjoining = sorcery.lib.tbl.append(sorcery.lib.tbl.append(
ofs.neighbors,ofs.planecorners),ofs.cubecorners)
local purge_container = function(only, pos,node,meta,user)
local offset = function(pos,range)
local r = function(min,max)
return (math.random() * (max - min)) + min
end
return {
x = pos.x + r(0 - range, range);
y = pos.y;
z = pos.z + r(0 - range, range);
}
end
for name, inv in pairs(meta.inventory) do
if only and not sorcery.lib.tbl.has(only,name) then goto skip end
for _, item in pairs(inv) do
if not item:is_empty() then
minetest.add_item(offset(pos,0.4), item)
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;
local is_air = function(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)
end;
local is_clear = function(pos)
if not sorcery.lib.node.is_air(pos) then return false end
local ents = minetest.get_objects_inside_radius(pos,0.5)
if #ents > 0 then return false end
return true
end;
return {
offsets = ofs;
purge_container = function(...) return purge_container(nil, ...) end;
purge_only = function(lst)
return function(...)
return purge_container(lst, ...)
end
end;
is_air = is_air;
is_clear = is_clear;
insert = function(item, slot, npos, user, inv)
inv = inv or minetest.get_meta(npos):get_inventory()
if inv:room_for_item(slot,item) then
inv:add_item(slot,item)
else repeat
if user then
local ui = user:get_inventory()
if ui:room_for_item('main', item) then
ui:add_item('main', item)
break
end
end
minetest.add_item(npos, item)
until true end
end;
install_bed = function(bed, where, dir)
local bottom = bed .. '_bottom'
local top = bed .. '_top'
local d
if type(dir) == 'number' then
d = dir
dir = minetest.facedir_to_dir(d)
else
d = minetest.dir_to_facedir(dir)
end
if not is_clear(where) and is_clear(where - dir) then return false end
minetest.set_node(where, {name = top, param2 = d})
minetest.set_node(where - dir, {name = bottom, param2 = d})
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
--
-- UPDATE: in practice this was way too expensive to be functional, and causes
-- servers to hang. ripped it out and replaced it with a simpler version
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, function(ent)
return sorcery.lib.tbl.strmatch(ent.node, treetype)
end)
local leaftype = treedef and treedef.leaves or nil
if not leaftype then return false end
local uppermost, lowermost
local found_leaves = false
local treemap, treenodes = amass(pos,function(node, where)
if node.name == treetype and node.param1 == 0 then
-- abuse predicate so we can avoid iterating over it all later
-- again -- this function is expensive enough already
if (not lowermost) or where.y < lowermost.y then
lowermost = where
end
if (not uppermost) or where.y > uppermost.y then
uppermost = where
end
return true
elseif not found_leaves and node.name == leaftype and node.param2 == 0 then
found_leaves = true
end
return false
end, ofs.adjoining)
if not found_leaves then return false end
-- do -- leaf search
-- local pss, ct = minetest.find_nodes_in_area(uppermost:offset(-1,0,-1), uppermost:offset(1,1,1), leaftype)
-- if ct[leaftype] >= 1 then
-- for _, p in pairs(pss) do
-- local ln = force(p)
-- if ln.param2 == 0 then goto found_leaves end
-- end
-- end
-- return false
-- end
::found_leaves:: do -- soil check
local beneath = force(lowermost:offset(0,-1,0))
if minetest.get_item_group(beneath.name, 'soil') == 0 then
return false
end
end
return true, {map = treemap, nodes = treenodes, trunk = treetype, leaves = leaftype, topnode = uppermost, roots = lowermost}
-- if checklight then iterate to leaf top and check light
-- 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
if air(p) then
if air(vector.offset(p,0,1,0)) then return p end
if air(vector.offset(p,0,-1,0)) then return vector.offset(p,0,-1,0) end
end
return false
end
do local t = try(pos) if t then return t end end
for _,o in pairs(ofs.neighbors) do
local p = vector.add(pos, o)
do local t = try(p) if t then return t end end
end
end;
forneighbor = function(pos, n, fn)
for _,p in pairs(n) do
local sum = vector.add(pos, p)
local n = minetest.get_node(sum)
if n.name == 'ignore' then
minetest.load_area(sum)
n = minetest.get_node(sum)
end
fn(sum, n)
end
end;
amass = amass;
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)
sorcery.lib.node.forneighbor(pos, sorcery.ley.txofs, function(sum,node)
if minetest.get_item_group(node.name,'sorcery_ley_device') ~= 0 then
sorcery.ley.notify(sum)
end
end)
end;
blockpos = function(pos)
return {
x = math.floor(pos.x / 16);
y = math.floor(pos.y / 16);
z = math.floor(pos.z / 16);
}
end;
preload = function(pos, user)
minetest.load_area(pos)
user:send_mapblock(sorcery.lib.node.blockpos(pos))
end;
discharger = function(pos)
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)
if hopper:room_for_item('main',i) then
return hopper:add_item('main',i), true
end
return i, false
end
else
return function(i) return i, false end
end
end;
autopreserve = function(id, tbl)
tbl.drop = tbl.drop or {
max_items = 1;
items = {
{ items = {id} };
};
}
local next_apn = tbl.after_place_node
tbl.after_place_node = function(...) local pos, who, stack = ...
minetest.get_meta(pos):from_table(stack:get_meta():to_table())
if next_apn then return next_apn(...) end
end
local next_pm = tbl.preserve_metadata
tbl.preserve_metadata = function(...) local pos, node, meta, drops = ...
drops[1]:get_meta():from_table({fields = meta})
if next_pm then return next_pm(...) end
end
return tbl
end;
reg_autopreserve = function(id, tbl)
minetest.register_node(id, sorcery.lib.node.autopreserve(id, tbl))
end;
}