summaryrefslogtreecommitdiff
path: root/pipeworks/pressure_logic/abms.lua
blob: 083d8c313a0921b6597c814c76438ed865d31738 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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
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
299
300
301
302
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
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
-- reimplementation of new_flow_logic branch: processing functions
-- written 2017 by thetaepsilon



local flowlogic = {}
flowlogic.helpers = {}
pipeworks.flowlogic = flowlogic



-- borrowed from above: might be useable to replace the above coords tables
local make_coords_offsets = function(pos, include_base)
	local coords = {
		{x=pos.x,y=pos.y-1,z=pos.z},
		{x=pos.x,y=pos.y+1,z=pos.z},
		{x=pos.x-1,y=pos.y,z=pos.z},
		{x=pos.x+1,y=pos.y,z=pos.z},
		{x=pos.x,y=pos.y,z=pos.z-1},
		{x=pos.x,y=pos.y,z=pos.z+1},
	}
	if include_base then table.insert(coords, pos) end
	return coords
end



-- local debuglog = function(msg) print("## "..msg) end



local formatvec = function(vec) local sep="," return "("..tostring(vec.x)..sep..tostring(vec.y)..sep..tostring(vec.z)..")" end



-- new version of liquid check
-- accepts a limit parameter to only delete water blocks that the receptacle can accept,
-- and returns it so that the receptacle can update it's pressure values.
local check_for_liquids_v2 = function(pos, limit)
	local coords = make_coords_offsets(pos, false)
	local total = 0
	for index, tpos in ipairs(coords) do
		if total >= limit then break end
		local name = minetest.get_node(tpos).name
		if name == "default:water_source" then
			minetest.remove_node(tpos)
			total = total + 1
		end
	end
	--pipeworks.logger("check_for_liquids_v2@"..formatvec(pos).." total "..total)
	return total
end
flowlogic.check_for_liquids_v2 = check_for_liquids_v2



local label_pressure = "pipeworks.water_pressure"
local get_pressure_access = function(pos)
	local metaref = minetest.get_meta(pos)
	return {
		get = function()
			return metaref:get_float(label_pressure)
		end,
		set = function(v)
			metaref:set_float(label_pressure, v)
		end
	}
end


-- logging is unreliable when something is crashing...
local nilexplode = function(caller, label, value)
	if value == nil then
		error(caller..": "..label.." was nil")
	end
end



local finitemode = pipeworks.toggles.finite_water
flowlogic.run = function(pos, node)
	local nodename = node.name
	-- get the current pressure value.
	local nodepressure = get_pressure_access(pos)
	local currentpressure = nodepressure.get()
	local oldpressure = currentpressure

	-- if node is an input: run intake phase
	local inputdef = pipeworks.flowables.inputs.list[nodename]
	if inputdef then
		currentpressure = flowlogic.run_input(pos, node, currentpressure, inputdef)
		--debuglog("post-intake currentpressure is "..currentpressure)
		--nilexplode("run()", "currentpressure", currentpressure)
	end

	-- balance pressure with neighbours
	currentpressure = flowlogic.balance_pressure(pos, node, currentpressure)

	-- if node is an output: run output phase
	local outputdef = pipeworks.flowables.outputs.list[nodename]
	if outputdef then
		currentpressure = flowlogic.run_output(
			pos,
			node,
			currentpressure,
			oldpressure,
			outputdef,
			finitemode)
	end

	-- if node has pressure transitions: determine new node
	if pipeworks.flowables.transitions.list[nodename] then
		local newnode = flowlogic.run_transition(node, currentpressure)
		--pipeworks.logger("flowlogic.run()@"..formatvec(pos).." transition, new node name = "..dump(newnode).." pressure "..tostring(currentpressure))
		minetest.swap_node(pos, newnode)
		flowlogic.run_transition_post(pos, newnode)
	end

	-- set the new pressure
	nodepressure.set(currentpressure)
end



local simple_neighbour_offsets = {
		{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},
}
local get_neighbour_positions = function(pos, node)
	-- local dname = "get_neighbour_positions@"..formatvec(pos).." "
	-- get list of node neighbours.
	-- if this node is directional and only flows on certain sides,
	-- invoke the callback to retrieve the set.
	-- for simple flowables this is just an auto-gen'd list of all six possible neighbours.
	local candidates = {}
	if pipeworks.flowables.list.simple[node.name] then
		candidates = simple_neighbour_offsets
	else
		-- directional flowables: call the callback to get the list
		local directional = pipeworks.flowables.list.directional[node.name]
		if directional then
			--pipeworks.logger(dname.."invoking neighbourfn")
			local offsets = directional.neighbourfn(node)
			candidates = offsets
		end
	end

	-- then, check each possible neighbour to see if they can be reached from this node.
	local connections = {}
	for index, offset in ipairs(candidates) do
		local npos = vector.add(pos, offset)
		local neighbour = minetest.get_node(npos)
		local nodename = neighbour.name
		local is_simple = (pipeworks.flowables.list.simple[nodename])
		if is_simple then
			local n = get_pressure_access(npos)
			table.insert(connections, n)
		else
			-- if target node is also directional, check if it agrees it can flow in that direction
			local directional = pipeworks.flowables.list.directional[nodename]
			if directional then
				--pipeworks.logger(dname.."directionality test for offset "..formatvec(offset))
				local towards_origin = vector.multiply(offset, -1)
				--pipeworks.logger(dname.."vector passed to directionfn: "..formatvec(towards_origin))
				local result = directional.directionfn(neighbour, towards_origin)
				--pipeworks.logger(dname.."result: "..tostring(result))
				if result then
					local n = get_pressure_access(npos)
					table.insert(connections, n)
				end
			end
		end
	end

	return connections
end



flowlogic.balance_pressure = function(pos, node, currentpressure)
	-- local dname = "flowlogic.balance_pressure()@"..formatvec(pos).." "
	-- check the pressure of all nearby flowable nodes, and average it out.

	-- pressure handles to average over
	local connections = {}
	-- unconditionally include self in nodes to average over.
	-- result of averaging will be returned as new pressure for main flow logic callback
	local totalv = currentpressure
	local totalc = 1

	local connections = get_neighbour_positions(pos, node)

	-- for each neighbour, add neighbour's pressure to the total to balance out
	for _, neighbour in ipairs(connections) do
		local n = neighbour.get()
		totalv = totalv + n
		totalc = totalc + 1
	end
	local average = totalv / totalc
	for _, target in ipairs(connections) do
		target.set(average)
	end

	return average
end



flowlogic.run_input = function(pos, node, currentpressure, inputdef)
	-- intakefn allows a given input node to define it's own intake logic.
	-- this function will calculate the maximum amount of water that can be taken in;
	-- the intakefn will be given this and is expected to return the actual absorption amount.

	local maxpressure = inputdef.maxpressure
	local intake_limit = maxpressure - currentpressure
	if intake_limit <= 0 then return currentpressure end

	local actual_intake = inputdef.intakefn(pos, intake_limit)
	--pipeworks.logger("run_input@"..formatvec(pos).." oldpressure "..currentpressure.." intake_limit "..intake_limit.." actual_intake "..actual_intake)
	if actual_intake <= 0 then return currentpressure end

	local newpressure = actual_intake + currentpressure
	--debuglog("run_input() end, oldpressure "..currentpressure.." intake_limit "..intake_limit.." actual_intake "..actual_intake.." newpressure "..newpressure)
	return newpressure
end



-- flowlogic output helper implementation:
-- outputs water by trying to place water nodes nearby in the world.
-- neighbours is a list of node offsets to try placing water in.
-- this is a constructor function, returning another function which satisfies the output helper requirements.
-- note that this does *not* take rotation into account.
flowlogic.helpers.make_neighbour_output_fixed = function(neighbours)
	return function(pos, node, currentpressure, finitemode)
		local taken = 0
		for _, offset in pairs(neighbours) do
			local npos = vector.add(pos, offset)
			local name = minetest.get_node(npos).name
			if currentpressure < 1 then break end
			-- take pressure anyway in non-finite mode, even if node is water source already.
			-- in non-finite mode, pressure has to be sustained to keep the sources there.
			-- so in non-finite mode, placing water is dependent on the target node;
			-- draining pressure is not.
			local canplace = (name == "air") or (name == "default:water_flowing")
			if canplace then
				minetest.swap_node(npos, {name="default:water_source"})
			end
			if (not finitemode) or canplace then
				taken = taken + 1
				currentpressure = currentpressure - 1
			end
		end
		return taken
	end
end

-- complementary function to the above when using non-finite mode:
-- removes water sources from neighbor positions when the output is "off" due to lack of pressure.
flowlogic.helpers.make_neighbour_cleanup_fixed = function(neighbours)
	return function(pos, node, currentpressure)
		--pipeworks.logger("neighbour_cleanup_fixed@"..formatvec(pos))
		for _, offset in pairs(neighbours) do
			local npos = vector.add(pos, offset)
			local name = minetest.get_node(npos).name
			if (name == "default:water_source") then
				--pipeworks.logger("neighbour_cleanup_fixed removing "..formatvec(npos))
				minetest.remove_node(npos)
			end
		end
	end
end



flowlogic.run_output = function(pos, node, currentpressure, oldpressure, outputdef, finitemode)
	-- processing step for water output devices.
	-- takes care of checking a minimum pressure value and updating the resulting pressure level
	-- the outputfn is provided the current pressure and returns the pressure "taken".
	-- as an example, using this with the above spigot function,
	-- the spigot function tries to output a water source if it will fit in the world.
	--pipeworks.logger("flowlogic.run_output() pos "..formatvec(pos).." old -> currentpressure "..tostring(oldpressure).." "..tostring(currentpressure).." finitemode "..tostring(finitemode))
	local upper = outputdef.upper
	local lower = outputdef.lower
	local result = currentpressure
	local threshold = nil
	if finitemode then threshold = lower else threshold = upper end
	if currentpressure > threshold then
		local takenpressure = outputdef.outputfn(pos, node, currentpressure, finitemode)
		local newpressure = currentpressure - takenpressure
		if newpressure < 0 then newpressure = 0 end
		result = newpressure
	end
	if (not finitemode) and (currentpressure < lower) and (oldpressure < lower) then
		--pipeworks.logger("flowlogic.run_output() invoking cleanup currentpressure="..tostring(currentpressure))
		outputdef.cleanupfn(pos, node, currentpressure)
	end
	return result
end



-- determine which node to switch to based on current pressure
flowlogic.run_transition = function(node, currentpressure)
	local simplesetdef = pipeworks.flowables.transitions.simple[node.name]
	local result = node
	local found = false

	-- simple transition sets: assumes all nodes in the set share param values.
	if simplesetdef then
		-- assumes that the set has been checked to contain at least one element...
		local nodename_prev = simplesetdef[1].nodename
		local result_nodename = node.name

		for index, element in ipairs(simplesetdef) do
			-- find the highest element that is below the current pressure.
			local threshold = element.threshold
			if threshold > currentpressure then
				result_nodename = nodename_prev
				found = true
				break
			end
			nodename_prev = element.nodename
		end

		-- use last element if no threshold is greater than current pressure
		if not found then
			result_nodename = nodename_prev
			found = true
		end

		-- preserve param1/param2 values
		result = { name=result_nodename, param1=node.param1, param2=node.param2 }
	end

	if not found then
		pipeworks.logger("flowlogic.run_transition() BUG no transition definitions found! nodename="..nodename.." currentpressure="..tostring(currentpressure))
	end

	return result
end

-- post-update hook for run_transition
-- among other things, updates mesecons if present.
-- node here means the new node, returned from run_transition() above
flowlogic.run_transition_post = function(pos, node)
	local mesecons_def = minetest.registered_nodes[node.name].mesecons
	local mesecons_rules = pipeworks.flowables.transitions.mesecons[node.name]
	if minetest.global_exists("mesecon") and (mesecons_def ~= nil) and mesecons_rules then
		if type(mesecons_def) ~= "table" then
			pipeworks.logger("flowlogic.run_transition_post() BUG mesecons def for "..node.name.."not a table: got "..tostring(mesecons_def))
		else
			local receptor = mesecons_def.receptor
			if receptor then
				local state = receptor.state
				if state == mesecon.state.on then
					mesecon.receptor_on(pos, mesecons_rules)
				elseif state == mesecon.state.off then
					mesecon.receptor_off(pos, mesecons_rules)
				end
			end
		end
	end
end