sorcery  node.lua

File lib/node.lua from the latest check-in


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 ground 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
			if fn(sum, n) == false then break end
		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;
}