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
2
3
4
5
6
7
..
55
56
57
58
59
60
61



















































62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84



























































































































































85
86
87
88
89
90
91
..
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
...
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189

local ofs = {
	neighbors = {
		{x =  1, y =  0, z =  0};
		{x = -1, y =  0, z =  0};
		{x =  0, y =  1, z =  0};
		{x =  0, y = -1, z =  0};
		{x =  0, y =  0, z =  1};
................................................................................
		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;



















































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 = function(pos)
		local n = sorcery.lib.node.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;

	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;




























































































































































	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
................................................................................
		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;

	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)
			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;
	
	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;

	-- 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)
................................................................................

	preload = function(pos, user)
		minetest.load_area(pos)
		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}))
		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
>







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>










|












>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<













|
<
<
<
<
<
<
<







 







|







1
2
3
4
5
6
7
8
..
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
...
303
304
305
306
307
308
309

































310
311
312
313
314
315
316
317
318
319
320
321
322
323







324
325
326
327
328
329
330
...
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
local log = sorcery.logger('lib.node')
local ofs = {
	neighbors = {
		{x =  1, y =  0, z =  0};
		{x = -1, y =  0, z =  0};
		{x =  0, y =  1, z =  0};
		{x =  0, y = -1, z =  0};
		{x =  0, y =  0, z =  1};
................................................................................
		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;

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 = 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;

	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;

	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
			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
................................................................................
		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;
	
	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)
................................................................................

	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