sorcery  Diff

Differences From Artifact [b8ef82adf6]:

  • File lib/node.lua — part of check-in [15f176a7fe] at 2020-10-31 19:49:49 on branch trunk — add background noise for condensers (temporary hack, need to write a proper environment sound framework as the fucking env_sounds module is completely impossible to extend), fix a couple of really stupid bugs, make higher-quality phials increase the chance of getting good runes so it's not a complete waste to burn iridium or levitanium powder on making them, add targeted disjunction and some other amulet spells (user: lexi, size: 5167) [annotate] [blame] [check-ins using]

To Artifact [d8278e1811]:


            1  +local log = sorcery.logger('lib.node')
     1      2   local ofs = {
     2      3   	neighbors = {
     3      4   		{x =  1, y =  0, z =  0};
     4      5   		{x = -1, y =  0, z =  0};
     5      6   		{x =  0, y =  1, z =  0};
     6      7   		{x =  0, y = -1, z =  0};
     7      8   		{x =  0, y =  0, z =  1};
................................................................................
    55     56   		for _, item in pairs(inv) do
    56     57   			if not item:is_empty() then
    57     58   				minetest.add_item(offset(pos,0.4), item)
    58     59   			end
    59     60   		end
    60     61   	::skip::end
    61     62   end;
           63  +
           64  +local force = function(pos,preload_for)
           65  +	local n = minetest.get_node_or_nil(pos)
           66  +	if preload_for then sorcery.lib.node.preload(pos,preload_for) end
           67  +	if n then return n end
           68  +
           69  +	minetest.load_area(pos)
           70  +	return minetest.get_node(pos)
           71  +end;
           72  +
           73  +local amass = function(startpoint,names,directions)
           74  +	if not directions then directions = ofs.neighbors end
           75  +	local check = function(n)
           76  +		return sorcery.lib.tbl.has(names, n.name, function(check,against)
           77  +			return sorcery.lib.item.groupmatch(against,check)
           78  +		end)-- match found
           79  +	end
           80  +	if type(names) == 'function' then check = names end
           81  +	local nodes, positions, checked = {},{},{}
           82  +	local checkedp = function(pos)
           83  +		for _,v in pairs(checked) do
           84  +			if vector.equals(pos,v) then return true end
           85  +		end
           86  +		return false
           87  +	end
           88  +	local i,stack = 1,{startpoint} repeat
           89  +		local pos = stack[i]
           90  +		local n = force(pos)
           91  +		if check(n, pos, nodes, positions) then
           92  +			-- record the find
           93  +			nodes[pos] = n.name
           94  +			if positions[n.name]
           95  +				then positions[n.name][#positions[n.name]+1] = pos
           96  +				else positions[n.name] = {pos}
           97  +			end
           98  +
           99  +			-- check selected neighbors to see if any need scanning
          100  +			for _,d in pairs(directions) do
          101  +				local sum = vector.add(pos, d)
          102  +				if not checkedp(sum) then
          103  +					stack[#stack + 1] = sum
          104  +					checked[#checked+1] = sum
          105  +				end
          106  +			end
          107  +		end
          108  +		checked[#checked+1] = pos
          109  +		i = i + 1
          110  +	until i > #stack
          111  +	return nodes, positions
          112  +end;
          113  +
    62    114   return {
    63    115   	offsets = ofs;
    64    116   	purge_container = function(...) return purge_container(nil, ...) end;
    65    117   	purge_only = function(lst)
    66    118   		return function(...)
    67    119   			return purge_container(lst, ...)
    68    120   		end
    69    121   	end; 
    70    122   
    71    123   	is_air = function(pos)
    72         -		local n = sorcery.lib.node.force(pos)
          124  +		local n = force(pos)
    73    125   		if n.name == 'air' then return true end
    74    126   		local d = minetest.registered_nodes[n.name]
    75    127   		if not d then return false end
    76    128   		return (d.walkable == false) and (d.drawtype == 'airlike' or d.buildable_to == true)
    77    129   	end;
    78    130   
    79    131   	is_clear = function(pos)
    80    132   		if not sorcery.lib.node.is_air(pos) then return false end
    81    133   		local ents = minetest.get_objects_inside_radius(pos,0.5)
    82    134   		if #ents > 0 then return false end
    83    135   		return true
    84    136   	end;
          137  +
          138  +	tree_is_live = function(pos, checklight) -- VERY EXPENSIVE FUNCTION
          139  +		-- this is going to require some explanation.
          140  +		--
          141  +		-- for various purposes, we want to be able to tell the difference between
          142  +		-- a tree that has grown naturally from the grown vs. a couple of trunk nodes
          143  +		-- that the player has jammed together, even if she's built her own counterfeit
          144  +		-- tree. unfortunately, mtg provides no easy way to do this. the only 
          145  +		-- difference between a cluster of trunk blocks and a real tree is that the
          146  +		-- real tree will have a specific kind of leaves attached with their param2
          147  +		-- set to 1 so that they can be distinguished for the purpose of leaf cleaning.
          148  +		-- so to check a tree's state, we need to amass its whole potential body, and if
          149  +		-- there are legitimate leaves connected, then we identify it as a legit tree.
          150  +		--
          151  +		-- couple of caveats. firstly, we need to prevent the user from faking a tree
          152  +		-- simply by using a chain of leaf nodes to connect a fraudulent tree to a
          153  +		-- Genuine Arboreal® Product™. this means we can't just use a naive amass()
          154  +		-- call, and instead need to define our own checking function. this allows us
          155  +		-- to eliminate nonliving leaf nodes in the mass-collection process, so that 
          156  +		-- they can't be used to "branch" (hurr hurr geddit) off to a real tree.
          157  +		--
          158  +		-- secondly, we want this to work with trees that aren't necessarily known to
          159  +		-- the sorcery mod, so we can't rely on the tree register. we'll use it when we
          160  +		-- can, but otherwise we'll have to guesstimate the correct leaf node. we do
          161  +		-- this with a stateful closure, by saving the name of the first living leaf
          162  +		-- node we see, and then referring back to it from that point on. this is ugly
          163  +		-- but covers all but pathological edge cases.
          164  +		--
          165  +		-- finally, the trunk itself is basically inert. the user can mine and then
          166  +		-- replace as many trunk blocks as he likes without "killing" the tree by
          167  +		-- this function's estimation. there is a way around this but it would require
          168  +		-- hooking the on_dignode global callback to invalidate leaf bodies, and this
          169  +		-- is way too trivial and niche a use for such a performance-critical function,
          170  +		-- especially since it would involve calling amass on trees for *every node you
          171  +		-- dig*. imagine O(chopping down an emergent jungle tree) with something like
          172  +		-- *that* hooked! not on my watch, pal.
          173  +		-- 
          174  +		-- however, there is one problem we have to deal with, and unfortunately there
          175  +		-- is no good solution. the user can still attach new trunk blocks to living 
          176  +		-- leaves and get extra tree to work with, e.g. for sap generation. this is
          177  +		-- l33t hax and we don't want it, but preventing it is nontrivial. the best i
          178  +		-- can come up with for now is hooking the after_place_node functions of
          179  +		-- known trees to set a metadata key excluding them from amassing if they're
          180  +		-- positioned near a relevant leaf node. this is ugly and not very efficient,
          181  +		-- and if you have a better idea i'd love to hear it. apart from no back-compat
          182  +		-- for existing maps, it also fails to address
          183  +		-- two edge cases:
          184  +		--  - a sapling grows such that its leaf nodes connect to a fake trunk
          185  +		--  - a non-growth trunk node is inserted by another mod that fails to use
          186  +		--    the place function and attribute the call to a user
          187  +		--
          188  +		-- various problems could be avoided by unconditionally inserted the meta key,
          189  +		-- or inserting it also when it comes into contact with another trunk node,
          190  +		-- but pepole use these things to build with and that is just way way too many
          191  +		-- meta keys for me to consider it an option.
          192  +		--
          193  +		-- verdict: not very good, but decent enough for most cases. mtg should have
          194  +		--          done better than this, but now we're all stuck with their bullshit
          195  +
          196  +		local treetype = force(pos).name
          197  +		if minetest.get_item_group(treetype, 'tree') == 0 then -- sir this is not a tree
          198  +			return nil -- 無
          199  +		end
          200  +		local treedef = sorcery.lib.tbl.select(sorcery.data.trees, 'node', treetype)
          201  +		local leaftype = treedef and treedef.leaves or nil
          202  +		local uppermost, lowermost
          203  +
          204  +		local treemap, treenodes = amass(pos,function(node, where)
          205  +			if node.name == treetype then
          206  +				-- abuse predicate function to also track y minimum, so we can
          207  +				-- avoid iterating over it all later again -- this function is
          208  +				-- expensive enough already
          209  +				if (not lowermost) or where.y < lowermost then
          210  +					lowermost = where.y
          211  +				end
          212  +				if (not uppermost) or where.y > uppermost then
          213  +					uppermost = where.y
          214  +				end
          215  +				local m=minetest.get_meta(where)
          216  +				if m:get_int('sorcery:trunk_node_role') ~= 1 then
          217  +					return true
          218  +				else
          219  +					log.warn('found a log node!')
          220  +					return false
          221  +				end
          222  +			end
          223  +			if leaftype == nil then
          224  +				if minetest.get_item_group(node.name, 'leaves') ~= 0 and node.param2 == 0 then
          225  +					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')
          226  +					leaftype = node.name
          227  +					return true
          228  +				end
          229  +			elseif leaftype == node.name and node.param2 == 0 then
          230  +				return true
          231  +			end
          232  +			return false
          233  +		end,ofs.adjoining)
          234  +
          235  +		if leaftype == nil then return false end
          236  +
          237  +		local trunkmap, trunknodes = amass(pos, {treetype}, ofs.adjoining)
          238  +		if treenodes[leaftype] == nil then return false end
          239  +
          240  +		local cache = {}
          241  +		local uppermost_check_leaves = true
          242  +		local topnode
          243  +		for _, p in pairs(treenodes[treetype]) do
          244  +			-- if not sorcery.lib.tbl.select(trunknodes[treetype], function(v)
          245  +			-- 	return vector.equals(p, v)
          246  +			-- end, cache) then
          247  +			-- 	log.act('tree node', p, 'not accounted for in trunk!')
          248  +			-- 	return false
          249  +			-- end
          250  +			if p.y == uppermost and uppermost_check_leaves then
          251  +				topnode = p
          252  +				uppermost_check_leaves = false
          253  +			end
          254  +			if p.y == lowermost then
          255  +				-- this is the bottom of the tree, bail if it's in not full contact
          256  +				-- with soil or other eligible nodes as determined by the tree def's
          257  +				-- 'rooted' predicate
          258  +				local beneath = vector.offset(p, 0,-1,0);
          259  +				if treedef.rooted then
          260  +					if not treedef.rooted {
          261  +						trunk = p;
          262  +						groundpos = beneath;
          263  +						ground = force(beneath);
          264  +						treemap = treemap;
          265  +						treenodes = treenodes;
          266  +					} then return false end
          267  +				else
          268  +					if minetest.get_item_group(force(beneath).name, 'soil') == 0 then
          269  +						return false
          270  +					end
          271  +				end
          272  +			end
          273  +		end
          274  +
          275  +		if uppermost_check_leaves then
          276  +			for _,p in pairs(treenodes[leaftype]) do
          277  +				if p.y == uppermost then
          278  +					topnode = p
          279  +					break
          280  +				end
          281  +			end
          282  +		end
          283  +		--
          284  +		--make sure the tree gets enough light
          285  +		if checklight and minetest.get_natural_light(vector.offset(topnode,0,1,0), 0.5) < 13 then return false end
          286  +		
          287  +		-- other possible checks: make sure all ground-touching nodes are directly
          288  +		-- adjacent
          289  +
          290  +		return true, {map = treemap, nodes = treenodes, trunk = treetype, leaves = leaftype, topnode = topnode}
          291  +	end;
    85    292   
    86    293   	get_arrival_point = function(pos)
    87    294   		local try = function(p)
    88    295   			local air = sorcery.lib.node.is_clear
    89    296   			if air(p) then
    90    297   				if air(vector.offset(p,0,1,0))  then return p end
    91    298   				if air(vector.offset(p,0,-1,0)) then return vector.offset(p,0,-1,0) end
................................................................................
    96    303   		do local t = try(pos) if t then return t end end
    97    304   		for _,o in pairs(ofs.neighbors) do
    98    305   			local p = vector.add(pos, o)
    99    306   			do local t = try(p) if t then return t end end
   100    307   		end
   101    308   	end;
   102    309   
   103         -	amass = function(startpoint,names,directions)
   104         -		if not directions then directions = ofs.neighbors end
   105         -		local nodes, positions, checked = {},{},{}
   106         -		local checkedp = function(pos)
   107         -			for _,v in pairs(checked) do
   108         -				if vector.equals(pos,v) then return true end
   109         -			end
   110         -			return false
   111         -		end
   112         -		local i,stack = 1,{startpoint} repeat
   113         -			local pos = stack[i]
   114         -			local n = sorcery.lib.node.force(pos).name
   115         -			if sorcery.lib.tbl.has(names, n, function(check,against)
   116         -				return sorcery.lib.item.groupmatch(against,check)
   117         -			end) then -- match found
   118         -				-- record the find
   119         -				nodes[pos] = n
   120         -				if positions[n] then positions[n][#positions[n]] = pos
   121         -				else positions[n] = {pos} end
   122         -
   123         -				-- check selected neighbors to see if any need scanning
   124         -				for _,d in pairs(directions) do
   125         -					local sum = vector.add(pos, d)
   126         -					if not checkedp(sum) then
   127         -						stack[#stack + 1] = sum
   128         -					end
   129         -				end
   130         -			end
   131         -			checked[#checked+1] = pos
   132         -			i = i + 1
   133         -		until i > #stack
   134         -		return nodes, positions
   135         -	end;
   136    310   
   137    311   	forneighbor = function(pos, n, fn)
   138    312   		for _,p in pairs(n) do
   139    313   			local sum = vector.add(pos, p)
   140    314   			local n = minetest.get_node(sum)
   141    315   			if n.name == 'ignore' then
   142    316   				minetest.load_area(sum)
   143    317   				n = minetest.get_node(sum)
   144    318   			end
   145    319   			fn(sum, n)
   146    320   		end
   147    321   	end;
   148    322   	
   149         -	force = function(pos,preload_for)
   150         -		local n = minetest.get_node_or_nil(pos)
   151         -		if preload_for then sorcery.lib.node.preload(pos,preload_for) end
   152         -		if n then return n end
   153         -
   154         -		minetest.load_area(pos)
   155         -		return minetest.get_node(pos)
   156         -	end;
          323  +	force = force;
   157    324   
   158    325   	-- when items have already been removed; notify cannot be relied on
   159    326   	-- to reach the entire network; this function accounts for the gap
   160    327   	notifyneighbors = function(pos)
   161    328   		sorcery.lib.node.forneighbor(pos, sorcery.ley.txofs, function(sum,node)
   162    329   			if minetest.get_item_group(node.name,'sorcery_ley_device') ~= 0 then
   163    330   				sorcery.ley.notify(sum)
................................................................................
   175    342   
   176    343   	preload = function(pos, user)
   177    344   		minetest.load_area(pos)
   178    345   		user:send_mapblock(sorcery.lib.node.blockpos(pos))
   179    346   	end;
   180    347   
   181    348   	discharger = function(pos)
   182         -		local below = sorcery.lib.node.force(vector.subtract(pos,{x=0,y=1,z=0}))
          349  +		local below = force(vector.subtract(pos,{x=0,y=1,z=0}))
   183    350   		if below.name == 'hopper:hopper'
   184    351   		or below.name == 'hopper:hopper_side' then
   185    352   			local hopper = minetest.get_meta(below):get_inventory()
   186    353   			return function(i)
   187    354   				if hopper:room_for_item('main',i) then
   188    355   					return hopper:add_item('main',i), true
   189    356   				end