code:
local luaentity = pipeworks.luaentity
local enable_max_limit = minetest.settings:get("pipeworks_enable_items_per_tube_limit")
local max_tube_limit = tonumber(minetest.settings:get("pipeworks_max_items_per_tube")) or 30
if enable_max_limit == nil then enable_max_limit = true end

function pipeworks.tube_item(pos, item)
	error("obsolete pipeworks.tube_item() called; change caller to use pipeworks.tube_inject_item() instead")
end

function pipeworks.tube_inject_item(pos, start_pos, velocity, item, owner)
	-- Take item in any format
	local stack = ItemStack(item)
	local obj = luaentity.add_entity(pos, "pipeworks:tubed_item")
	obj:set_item(stack:to_string())
	obj.start_pos = vector.new(start_pos)
	obj:set_velocity(velocity)
	obj.owner = owner
	--obj:set_color("red") -- todo: this is test-only code
	return obj
end

-- adding two tube functions
-- can_remove(pos,node,stack,dir) returns the maximum number of items of that stack that can be removed
-- remove_items(pos,node,stack,dir,count) removes count items and returns them
-- both optional w/ sensible defaults and fallback to normal allow_* function
-- XXX: possibly change insert_object to insert_item

local adjlist={{x=0,y=0,z=1},{x=0,y=0,z=-1},{x=0,y=1,z=0},{x=0,y=-1,z=0},{x=1,y=0,z=0},{x=-1,y=0,z=0}}

function pipeworks.notvel(tbl, vel)
	local tbl2={}
	for _,val in ipairs(tbl) do
		if val.x ~= -vel.x or val.y ~= -vel.y or val.z ~= -vel.z then table.insert(tbl2, val) end
	end
	return tbl2
end

local tube_item_count = {}

minetest.register_globalstep(function(dtime)
	if not luaentity.entities then
		return
	end
	tube_item_count = {}
	for id, entity in pairs(luaentity.entities) do
		if entity.name == "pipeworks:tubed_item" then
			local h = minetest.hash_node_position(vector.round(entity._pos))
			tube_item_count[h] = (tube_item_count[h] or 0) + 1
		end
	end
end)

-- tube overload mechanism:
-- when the tube's item count (tracked in the above tube_item_count table)
-- exceeds the limit configured per tube, replace it with a broken one.
local crunch_tube = function(pos, cnode, cmeta)
	if enable_max_limit then
		local h = minetest.hash_node_position(pos)
		local itemcount = tube_item_count[h] or 0
		if itemcount > max_tube_limit then
			cmeta:set_string("the_tube_was", minetest.serialize(cnode))
			pipeworks.logger("Warning - a tube at "..minetest.pos_to_string(pos).." broke due to too many items ("..itemcount..")")
			minetest.swap_node(pos, {name = "pipeworks:broken_tube_1"})
			pipeworks.scan_for_tube_objects(pos)
		end
	end
end

-- compatibility behaviour for the existing can_go() callbacks,
-- which can only specify a list of possible positions.
local function go_next_compat(pos, cnode, cmeta, cycledir, vel, stack, owner)
	local next_positions = {}
	local max_priority = 0
	local can_go

	if minetest.registered_nodes[cnode.name] and minetest.registered_nodes[cnode.name].tube and minetest.registered_nodes[cnode.name].tube.can_go then
		can_go = minetest.registered_nodes[cnode.name].tube.can_go(pos, cnode, vel, stack)
	else
		can_go = pipeworks.notvel(adjlist, vel)
	end
	-- can_go() is expected to return an array-like table of candidate offsets.
	-- for each one, look at the node at that offset and determine if it can accept the item.
	-- also note the prioritisation:
	-- if any tube is found with a greater priority than previously discovered,
	-- then the valid positions are reset and and subsequent positions under this are skipped.
	-- this has the effect of allowing only equal priorities to co-exist.
	for _, vect in ipairs(can_go) do
		local npos = vector.add(pos, vect)
		pipeworks.load_position(npos)
		local node = minetest.get_node(npos)
		local reg_node = minetest.registered_nodes[node.name]
		if reg_node then
			local tube_def = reg_node.tube
			local tubedevice = minetest.get_item_group(node.name, "tubedevice")
			local tube_priority = (tube_def and tube_def.priority) or 100
			if tubedevice > 0 and tube_priority >= max_priority then
				if not tube_def or not tube_def.can_insert or
						tube_def.can_insert(npos, node, stack, vect, owner) then
					if tube_priority > max_priority then
						max_priority = tube_priority
						next_positions = {}
					end
					next_positions[#next_positions + 1] = {pos = npos, vect = vect}
				end
			end
		end
	end

	-- indicate not found if no valid rules were picked up,
	-- and don't change the counter.
	if not next_positions[1] then
		return cycledir, false, nil, nil
	end

	-- otherwise rotate to the next output direction and return that
	local n = (cycledir % (#next_positions)) + 1
	local new_velocity = vector.multiply(next_positions[n].vect, vel.speed)
	return n, true, new_velocity, nil
end

-- function called by the on_step callback of the pipeworks tube luaentity.
-- the routine is passed the current node position, velocity, itemstack,
-- and owner name.
-- returns three values:
-- * a boolean "found destination" status;
-- * a new velocity vector that the tubed item should use, or nil if not found;
-- * a "multi-mode" data table (or nil if N/A) where a stack was split apart.
--	if this is not nil, the luaentity spawns new tubed items for each new fragment stack,
--	then deletes itself (i.e. the original item stack).
local function go_next(pos, velocity, stack, owner)
	local cnode = minetest.get_node(pos)
	local cmeta = minetest.get_meta(pos)
	local speed = math.abs(velocity.x + velocity.y + velocity.z)
	if speed == 0 then
		speed = 1
	end
	local vel = {x = velocity.x/speed, y = velocity.y/speed, z = velocity.z/speed,speed=speed}
	if speed >= 4.1 then
		speed = 4
	elseif speed >= 1.1 then
		speed = speed - 0.1
	else
		speed = 1
	end
	vel.speed = speed

	crunch_tube(pos, cnode, cmeta)
	-- cycling of outputs:
	-- an integer counter is kept in each pipe's metadata,
	-- which allows tracking which output was previously chosen.
	-- note reliance on get_int returning 0 for uninitialised.
	local cycledir = cmeta:get_int("tubedir")

	-- pulled out and factored out into go_next_compat() above.
	-- n is the new value of the cycle counter.
	-- XXX: this probably needs cleaning up after being split out,
	-- seven args is a bit too many
	local n, found, new_velocity, multimode = go_next_compat(pos, cnode, cmeta, cycledir, vel, stack, owner)

	-- if not using output cycling,
	-- don't update the field so it stays the same for the next item.
	if pipeworks.enable_cyclic_mode then
		cmeta:set_int("tubedir", n)
	end
	return found, new_velocity, multimode
end

minetest.register_entity("pipeworks:tubed_item", {
	initial_properties = {
		hp_max = 1,
		physical = false,
		collisionbox = {0.1, 0.1, 0.1, 0.1, 0.1, 0.1},
		visual = "wielditem",
		visual_size = {x = 0.15, y = 0.15},
		textures = {""},
		spritediv = {x = 1, y = 1},
		initial_sprite_basepos = {x = 0, y = 0},
		is_visible = false,
	},

	physical_state = false,

	from_data = function(self, itemstring)
		local stack = ItemStack(itemstring)
		local itemtable = stack:to_table()
		local itemname = nil
		if itemtable then
			itemname = stack:to_table().name
		end
		local item_texture = nil
		local item_type = ""
		if minetest.registered_items[itemname] then
			item_texture = minetest.registered_items[itemname].inventory_image
			item_type = minetest.registered_items[itemname].type
		end
		self.object:set_properties({
			is_visible = true,
			textures = {stack:get_name()}
		})
		local def = stack:get_definition()
		self.object:set_yaw((def and def.type == "node") and 0 or math.pi * 0.25)
	end,

	get_staticdata = luaentity.get_staticdata,
	on_activate = function(self, staticdata) -- Legacy code, should be replaced later by luaentity.on_activate
		if staticdata == "" or staticdata == nil then
			return
		end
		if staticdata == "toremove" then
			self.object:remove()
			return
		end
		local item = minetest.deserialize(staticdata)
		pipeworks.tube_inject_item(self.object:get_pos(), item.start_pos, item.velocity, item.itemstring)
		self.object:remove()
	end,
})

minetest.register_entity("pipeworks:color_entity", {
	initial_properties = {
		hp_max = 1,
		physical = false,
		collisionbox = {0.1, 0.1, 0.1, 0.1, 0.1, 0.1},
		visual = "cube",
		visual_size = {x = 3.5, y = 3.5, z = 3.5}, -- todo: find correct size
		textures = {""},
		is_visible = false,
	},

	physical_state = false,

	from_data = function(self, color)
		local t = "pipeworks_color_"..color..".png"
		local prop = {
			is_visible = true,
			visual = "cube",
			textures = {t, t, t, t, t, t} -- todo: textures
		}
		self.object:set_properties(prop)
	end,

	get_staticdata = luaentity.get_staticdata,
	on_activate = luaentity.on_activate,
})

-- see below for usage:
-- determine if go_next returned a multi-mode set.
local is_multimode = function(v)
	return (type(v) == "table") and (v.__multimode)
end

luaentity.register_entity("pipeworks:tubed_item", {
	itemstring = '',
	item_entity = nil,
	color_entity = nil,
	color = nil,
	start_pos = nil,

	set_item = function(self, item)
		local itemstring = ItemStack(item):to_string() -- Accept any input format
		if self.itemstring == itemstring then
			return
		end
		if self.item_entity then
			self:remove_attached_entity(self.item_entity)
		end
		self.itemstring = itemstring
		self.item_entity = self:add_attached_entity("pipeworks:tubed_item", itemstring)
	end,

	set_color = function(self, color)
		if self.color == color then
			return
		end
		self.color = color
		if self.color_entity then
			self:remove_attached_entity(self.color_entity)
		end
		if color then
			self.color_entity = self:add_attached_entity("pipeworks:color_entity", color)
		else
			self.color_entity = nil
		end
	end,

	on_step = function(self, dtime)
		local pos = self:get_pos()
		if self.start_pos == nil then
			self.start_pos = vector.round(pos)
			self:set_pos(pos)
		end

		local stack = ItemStack(self.itemstring)

		local velocity = self:get_velocity()

		local moved = false
		local speed = math.abs(velocity.x + velocity.y + velocity.z)
		if speed == 0 then
			speed = 1
			moved = true
		end
		local vel = {x = velocity.x / speed, y = velocity.y / speed, z = velocity.z / speed, speed = speed}
		local moved_by = vector.distance(pos, self.start_pos)

		if moved_by >= 1 then
			self.start_pos = vector.add(self.start_pos, vel)
			moved = true
		end

		pipeworks.load_position(self.start_pos)
		local node = minetest.get_node(self.start_pos)
		if moved and minetest.get_item_group(node.name, "tubedevice_receiver") == 1 then
			local leftover
			if minetest.registered_nodes[node.name].tube and minetest.registered_nodes[node.name].tube.insert_object then
				leftover = minetest.registered_nodes[node.name].tube.insert_object(self.start_pos, node, stack, vel, self.owner)
			else
				leftover = stack
			end
			if leftover:is_empty() then
				self:remove()
				return
			end
			velocity = vector.multiply(velocity, -1)
			self:set_pos(vector.subtract(self.start_pos, vector.multiply(vel, moved_by - 1)))
			self:set_velocity(velocity)
			self:set_item(leftover:to_string())
			return
		end

		if moved then
			local found_next, new_velocity, multimode = go_next(self.start_pos, velocity, stack, self.owner) -- todo: color
			local rev_vel = vector.multiply(velocity, -1)
			local rev_dir = vector.direction(self.start_pos,vector.add(self.start_pos,rev_vel))
			local rev_node = minetest.get_node(vector.round(vector.add(self.start_pos,rev_dir)))
			local tube_present = minetest.get_item_group(rev_node.name,"tubedevice") == 1
			if not found_next then
				if pipeworks.drop_on_routing_fail or not tube_present or
						minetest.get_item_group(rev_node.name,"tube") ~= 1 then
					-- Using add_item instead of item_drop since this makes pipeworks backward
					-- compatible with Minetest 0.4.13.
					-- Using item_drop here makes Minetest 0.4.13 crash.
					local dropped_item = minetest.add_item(self.start_pos, stack)
					if dropped_item then
						dropped_item:set_velocity(vector.multiply(velocity, 5))
						self:remove()
					end
					return
				else
					velocity = vector.multiply(velocity, -1)
					self:set_pos(vector.subtract(self.start_pos, vector.multiply(vel, moved_by - 1)))
					self:set_velocity(velocity)
				end
			elseif is_multimode(multimode) then
				-- create new stacks according to returned data.
				local s = self.start_pos
				for _, split in ipairs(multimode) do
					pipeworks.tube_inject_item(s, s, split.velocity, split.itemstack, self.owner)
				end
				-- remove ourself now the splits are sent
				self:remove()
				return
			end

			if new_velocity and not vector.equals(velocity, new_velocity) then
				local nvelr = math.abs(new_velocity.x + new_velocity.y + new_velocity.z)
				self:set_pos(vector.add(self.start_pos, vector.multiply(new_velocity, (moved_by - 1) / nvelr)))
				self:set_velocity(new_velocity)
			end
		end
	end
})

if minetest.get_modpath("mesecons_mvps") then
	mesecon.register_mvps_unmov("pipeworks:tubed_item")
	mesecon.register_mvps_unmov("pipeworks:color_entity")
	mesecon.register_on_mvps_move(function(moved_nodes)
		local moved = {}
		for _, n in ipairs(moved_nodes) do
			moved[minetest.hash_node_position(n.oldpos)] = vector.subtract(n.pos, n.oldpos)
		end
		for id, entity in pairs(luaentity.entities) do
			if entity.name == "pipeworks:tubed_item" then
				local pos = entity:get_pos()
				local rpos = vector.round(pos)
				local dir = moved[minetest.hash_node_position(rpos)]
				if dir then
					entity:set_pos(vector.add(pos, dir))
					entity.start_pos = vector.add(entity.start_pos, dir)
				end
			end
		end
	end)
end


code:
local S = minetest.get_translator("pipeworks")

if not minetest.get_modpath("auto_tree_tap") and
  minetest.get_modpath("technic") then

	minetest.register_abm({
		nodenames = { "auto_tree_tap:off", "auto_tree_tap:on" },
		chance = 1,
		interval = 1,
		action = function(pos, node, active_object_count, active_object_count_wider)
			local fdir = node.param2
			local meta = minetest.get_meta(pos)
			local inv = meta:get_inventory()
			inv:set_size("pick", 1)
			inv:set_size("ghost_pick", 1)
			inv:set_size("main", 100)
			minetest.set_node(pos, {name = "pipeworks:nodebreaker_off", param2 = fdir})
			minetest.registered_nodes["pipeworks:nodebreaker_off"].on_punch(pos, node)
			inv:set_stack("pick", 1, ItemStack("technic:treetap"))
		end
	})

	minetest.register_node(":auto_tree_tap:off", {
		description = S("Auto-Tap"),
		tiles = {"pipeworks_nodebreaker_top_off.png","pipeworks_nodebreaker_bottom_off.png","pipeworks_nodebreaker_side2_off.png","pipeworks_nodebreaker_side1_off.png",
			"pipeworks_nodebreaker_back.png","pipeworks_nodebreaker_front_off.png"},
		is_ground_content = true,
		paramtype2 = "facedir",
		groups = {snappy=2,choppy=2,oddly_breakable_by_hand=2, mesecon = 2,tubedevice=1, not_in_creative_inventory=1 },
		sounds = default.node_sound_stone_defaults(),
		tube = {connect_sides={back=1}},
		on_construct = function(pos)
			local meta = minetest.get_meta(pos)
			local inv = meta:get_inventory()
			inv:set_size("pick", 1)
			inv:set_stack("pick", 1, ItemStack("default:pick_mese"))
		end,
		after_place_node = function (pos, placer)
			pipeworks.scan_for_tube_objects(pos, placer)
			local placer_pos = placer:get_pos()

			--correct for the player's height
			if placer:is_player() then placer_pos.y = placer_pos.y + 1.5 end

			--correct for 6d facedir
			if placer_pos then
				local dir = {
					x = pos.x - placer_pos.x,
					y = pos.y - placer_pos.y,
					z = pos.z - placer_pos.z
				}
				local node = minetest.get_node(pos)
				node.param2 = minetest.dir_to_facedir(dir, true)
				minetest.set_node(pos, node)
				minetest.log("action", "real (6d) facedir: " .. node.param2)
			end
		end,
		after_dig_node = pipeworks.scan_for_tube_objects,
	})
end

repo_name: Desour/PlayMinetestServer

license: mit


code:
# textdomain: pipeworks

# License: CC-by-SA 4.0
# Author: Louis Royer <4259825-lroyer@users.noreply.gitlab.com>

## digilines interfacing
Channel=Canal

## init
Allow splitting incoming stacks from tubes=Séparer les piles venant des tubes

## autocrafter
Unknown item=Item inconnu
unconfigured Autocrafter: unknown recipe=Autocrafteur non-configuré : recette inconnue
unconfigured Autocrafter=Autocrafteur non-configuré
'@1' Autocrafter (@2)=Autocrafteur de '@1' (@2)
Save=Valider
paused '@1' Autocrafter=Autocrafteur de '@1' en pause
Autocrafter=Autocrafteur

## compat-furnaces
Allow splitting incoming material (not fuel) stacks from tubes=Séparer les piles (sauf le carburant) venant des tubes

## decorative tubes
Airtight steelblock embedded tube=Tube hermétique intégré à un bloc d’acier
Airtight panel embedded tube=Tube hermétique intégré à un panneau

## devices
Pump/Intake Module=Module de pompage et d’admission
Valve=Vanne
Decorative grating=Grillage décoratif
Spigot outlet=Sortie de robinet
Airtight Pipe entry/exit=Extrémité de tube hermétique
Flow Sensor=Détecteur de débit
Flow sensor (on)=Détecteur de débit (actif)
empty=vide
@1% full=plein à @1 %
Expansion Tank (@1)=Réservoir d’expansion (@1)
Fluid Storage Tank (@1)=Réservoir de liquides (@1)
Fountainhead=Tête de fontaine
Straight-only Pipe=Tuyau droit

## filter-injector
(slot #@1 next)=(slot suivant : #@1)
@1 Filter-Injector=Filtre-injecteur @1
Sequence slots by Priority=Ordonner par priorité
Sequence slots Randomly=Ordonner aléatoirement
Sequence slots by Rotation=Ordonner en rotation
Exact match - off=Filtrage inactif
Exact match - on=Filtrage actif
Prefer item types:=Items à filtrer :
Itemwise=par item
Stackwise=par piles d’items
Digiline=digiline

## legacy
Auto-Tap=Robinet d’arbre automatique

## pipes
Pipe Segment=Segment de tuyau
Pipe Segment (legacy)=Segment de tuyau (obsolète)

## routing tubes
Pneumatic tube segment=Segment de tuyau pneumatique
Broken Tube=Tuyau cassé
High Priority Tube Segment=Segment de tuyau haute priorité
Accelerating Pneumatic Tube Segment=Segment de tuyau pneumatique accélérante
Crossing Pneumatic Tube Segment=Intersection de tuyau pneumatique
One way tube=Tuyau unidirectionnel

## signal tubes
Detecting Pneumatic Tube Segment on=Segment de tuyau pneumatique avec détecteur (actif)
Detecting Pneumatic Tube Segment=Segment de tuyau pneumatique avec détecteur
Digiline Detecting Pneumatic Tube Segment=Segment de tuyau pneumatique avec détecteur digiline
Digiline Detecting Tube=Tuyau avec détecteur digiline
Conducting Pneumatic Tube Segment=Segment de tuyau pneumatique conducteur
Conducting Pneumatic Tube Segment on=Segment de tuyau pneumatique conducteur actif
Digiline Conducting Pneumatic Tube Segment=Segment de tuyau pneumatique conducteur digiline
Mesecon and Digiline Conducting Pneumatic Tube Segment=Segment de tuyau pneumatique conducteur mesecon et digiline
Mesecon and Digiline Conducting Pneumatic Tube Segment on=Segment de tuyau pneumatique conducteur mesecon et digiline (actif)

## sorting tubes
Sorting Pneumatic Tube Segment=Segment de tuyau pneumatique triant
Sorting pneumatic tube=Tuyau pneumatique triant

## teleport tube
Receive=Reception
channels are public by default=Les canaux sont publics par défaut
use <player>:<channel> for fully private channels=Utilisez <joueur>:<canal> pour un canal entièrement privé
use <player>;<channel> for private receivers=Utilisez <joueur>;<canal> pour une réception privée
Teleporting Pneumatic Tube Segment=Segment de tuyau pneumatique téléporteur
unconfigured Teleportation Tube=Tuyau téléporteur non-configuré
Sorry, channel '@1' is reserved for exclusive use by @2=Désolé, le canal '@1' est réservé exclusivement à l’utilisateur @2.
Sorry, receiving from channel '@1' is reserved for @2=Désolé, la réception depuis le canal '@1' est réservée pour @2.
Teleportation Tube @1 on '@2'=Tuyau de téléportation @1 sur '@2'

## trashcan
Trash Can=Poubelle

## tube registration
Pneumatic tube segment (legacy)=Segment de tuyau pneumatique (obsolète)

## vacuum tubes
Vacuuming Pneumatic Tube Segment=Segment de tuyau pneumatique aspirant
Adjustable Vacuuming Pneumatic Tube Segment=Segment de tuyau pneumatique aspirant réglable
Adjustable Vacuuming Pneumatic Tube Segment (@1m)=Segment de tuyau pneumatique aspirant réglable (@1 m)

## wielder
Node Breaker=Casseur de blocs
Deployer=Poseur de blocs
Dispenser=Distributeur


code:
# textdomain: pipeworks

# License: CC-by-SA 4.0
# Author:

## digilines interfacing
Channel=

## init
Allow splitting incoming stacks from tubes=

## autocrafter
Unknown item=
unconfigured Autocrafter: unknown recipe=
unconfigured Autocrafter=
'@1' Autocrafter (@2)=
Save=
paused '@1' Autocrafter=
Autocrafter=

## compat-furnaces
Allow splitting incoming material (not fuel) stacks from tubes=

## decorative tubes
Airtight steelblock embedded tube=
Airtight panel embedded tube=

## devices
Pump/Intake Module=
Valve=
Decorative grating=
Spigot outlet=
Airtight Pipe entry/exit=
Flow Sensor=
Flow sensor (on)=
empty=
@1% full=
Expansion Tank (@1)=
Fluid Storage Tank (@1)=
Fountainhead=
Straight-only Pipe=

## filter-injector
(slot #@1 next)=
@1 Filter-Injector=
Sequence slots by Priority=
Sequence slots Randomly=
Sequence slots by Rotation=
Exact match - off=
Exact match - on=
Prefer item types:=
Itemwise=
Stackwise=
Digiline=

## legacy
Auto-Tap=

## pipes
Pipe Segment=
Pipe Segment (legacy)=

## routing tubes
Pneumatic tube segment=
Broken Tube=
High Priority Tube Segment=
Accelerating Pneumatic Tube Segment=
Crossing Pneumatic Tube Segment=
One way tube=

## signal tubes
Detecting Pneumatic Tube Segment on=
Detecting Pneumatic Tube Segment=
Digiline Detecting Pneumatic Tube Segment=
Digiline Detecting Tube=
Conducting Pneumatic Tube Segment=
Conducting Pneumatic Tube Segment on=
Digiline Conducting Pneumatic Tube Segment=
Mesecon and Digiline Conducting Pneumatic Tube Segment=
Mesecon and Digiline Conducting Pneumatic Tube Segment on=

## sorting tubes
Sorting Pneumatic Tube Segment=
Sorting pneumatic tube=

## teleport tube
Receive=
channels are public by default=
use <player>:<channel> for fully private channels=
use <player>;<channel> for private receivers=
Teleporting Pneumatic Tube Segment=
unconfigured Teleportation Tube=
Sorry, channel '@1' is reserved for exclusive use by @2=
Sorry, receiving from channel '@1' is reserved for @2=
Teleportation Tube @1 on '@2'=

## trashcan
Trash Can=

## tube registration
Pneumatic tube segment (legacy)=

## vacuum tubes
Vacuuming Pneumatic Tube Segment=
Adjustable Vacuuming Pneumatic Tube Segment=
Adjustable Vacuuming Pneumatic Tube Segment (@1m)=

## wielder
Node Breaker=
Deployer=
Dispenser=


code:
--        ______
--       |
--       |
--       |        __       ___  _   __         _  _
-- |   | |       |  | |\ |  |  |_| |  | |  |  |_ |_|
-- |___| |______ |__| | \|  |  | \ |__| |_ |_ |_ |\  tube
-- |
-- |
--

-- Reference
-- ports = get_real_port_states(pos): gets if inputs are powered from outside
-- newport = merge_port_states(state1, state2): just does result = state1 or state2 for every port
-- set_port(pos, rule, state): activates/deactivates the mesecons according to the port states
-- set_port_states(pos, ports): Applies new port states to a Luacontroller at pos
-- run(pos): runs the code in the controller at pos
-- reset_meta(pos, code, errmsg): performs a software-reset, installs new code and prints error messages
-- resetn(pos): performs a hardware reset, turns off all ports
--
-- The Sandbox
-- The whole code of the controller runs in a sandbox,
-- a very restricted environment.
-- Actually the only way to damage the server is to
-- use too much memory from the sandbox.
-- You can add more functions to the environment
-- (see where local env is defined)
-- Something nice to play is appending minetest.env to it.

local BASENAME = "pipeworks:lua_tube"

local rules = {
	red    = {x = -1, y =  0, z =  0, name = "red"},
	blue   = {x =  1, y =  0, z =  0, name = "blue"},
	yellow = {x =  0, y = -1, z =  0, name = "yellow"},
	green  = {x =  0, y =  1, z =  0, name = "green"},
	black  = {x =  0, y =  0, z = -1, name = "black"},
	white  = {x =  0, y =  0, z =  1, name = "white"},
}

local digiline_rules_luatube = {
	{x=0,  y=0,  z=-1},
	{x=1,  y=0,  z=0},
	{x=-1, y=0,  z=0},
	{x=0,  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},
	{x=0,  y=1,  z=1},
	{x=0,  y=-1, z=1},
	{x=0,  y=1,  z=-1},
	{x=0,  y=-1, z=-1},
	-- vertical connectivity
	{x=0,  y=1,  z=0},
	{x=0,  y=-1, z=0},
}

------------------
-- Action stuff --
------------------
-- These helpers are required to set the port states of the lua_tube

local function update_real_port_states(pos, rule_name, new_state)
	local meta = minetest.get_meta(pos)
	if rule_name == nil then
		meta:set_int("real_portstates", 1)
		return
	end
	local n = meta:get_int("real_portstates") - 1
	local L = {}
	for i = 1, 6 do
		L[i] = n % 2
		n = math.floor(n / 2)
	end
	--                  (0,0,-1) (0,-1,0) (-1,0,0)      (1,0,0) (0,1,0) (0,0,1)
	local pos_to_side = {  5,        3,       1,   nil,    2,      4,      6  }
	if rule_name.x == nil then
		for _, rname in ipairs(rule_name) do
			local port = pos_to_side[rname.x + (2 * rname.y) + (3 * rname.z) + 4]
			L[port] = (newstate == "on") and 1 or 0
		end
	else
		local port = pos_to_side[rule_name.x + (2 * rule_name.y) + (3 * rule_name.z) + 4]
		L[port] = (new_state == "on") and 1 or 0
	end
	meta:set_int("real_portstates",
		 1 +
		 1 * L[1] +
		 2 * L[2] +
		 4 * L[3] +
		 8 * L[4] +
		16 * L[5] +
		32 * L[6])
end

local port_names = {"red", "blue", "yellow", "green", "black", "white"}

local function get_real_port_states(pos)
	-- Determine if ports are powered (by itself or from outside)
	local meta = minetest.get_meta(pos)
	local L = {}
	local n = meta:get_int("real_portstates") - 1
	for _, name in ipairs(port_names) do
		L[name] = ((n % 2) == 1)
		n = math.floor(n / 2)
	end
	return L
end

local function merge_port_states(ports, vports)
	return {
		red    = ports.red    or vports.red,
		blue   = ports.blue   or vports.blue,
		yellow = ports.yellow or vports.yellow,
		green  = ports.green  or vports.green,
		black  = ports.black  or vports.black,
		white  = ports.white  or vports.white,
	}
end

local function generate_name(ports)
	local red    = ports.red    and 1 or 0
	local blue   = ports.blue   and 1 or 0
	local yellow = ports.yellow and 1 or 0
	local green  = ports.green  and 1 or 0
	local black  = ports.black  and 1 or 0
	local white  = ports.white  and 1 or 0
	return BASENAME..white..black..green..yellow..blue..red
end

local function set_port(pos, rule, state)
	if state then
		mesecon.receptor_on(pos, {rule})
	else
		mesecon.receptor_off(pos, {rule})
	end
end

local function clean_port_states(ports)
	ports.red    = ports.red    and true or false
	ports.blue   = ports.blue   and true or false
	ports.yellow = ports.yellow and true or false
	ports.green  = ports.green  and true or false
	ports.black  = ports.black  and true or false
	ports.white  = ports.white  and true or false
end

local function set_port_states(pos, ports)
	local node = minetest.get_node(pos)
	local name = node.name
	clean_port_states(ports)
	local vports = minetest.registered_nodes[name].virtual_portstates
	local new_name = generate_name(ports)

	if name ~= new_name and vports then
		-- Problem:
		-- We need to place the new node first so that when turning
		-- off some port, it won't stay on because the rules indicate
		-- there is an onstate output port there.
		-- When turning the output off then, it will however cause feedback
		-- so that the lua_tube will receive an "off" event by turning
		-- its output off.
		-- Solution / Workaround:
		-- Remember which output was turned off and ignore next "off" event.
		local meta = minetest.get_meta(pos)
		local ign = minetest.deserialize(meta:get_string("ignore_offevents")) or {}
		if ports.red    and not vports.red    and not mesecon.is_powered(pos, rules.red)    then ign.red    = true end
		if ports.blue   and not vports.blue   and not mesecon.is_powered(pos, rules.blue)   then ign.blue   = true end
		if ports.yellow and not vports.yellow and not mesecon.is_powered(pos, rules.yellow) then ign.yellow = true end
		if ports.green  and not vports.green  and not mesecon.is_powered(pos, rules.green)  then ign.green  = true end
		if ports.black  and not vports.black  and not mesecon.is_powered(pos, rules.black)  then ign.black  = true end
		if ports.white  and not vports.white  and not mesecon.is_powered(pos, rules.white)  then ign.white  = true end
		meta:set_string("ignore_offevents", minetest.serialize(ign))

		minetest.swap_node(pos, {name = new_name, param2 = node.param2})

		if ports.red    ~= vports.red    then set_port(pos, rules.red,    ports.red)    end
		if ports.blue   ~= vports.blue   then set_port(pos, rules.blue,   ports.blue)   end
		if ports.yellow ~= vports.yellow then set_port(pos, rules.yellow, ports.yellow) end
		if ports.green  ~= vports.green  then set_port(pos, rules.green,  ports.green)  end
		if ports.black  ~= vports.black  then set_port(pos, rules.black,  ports.black)  end
		if ports.white  ~= vports.white  then set_port(pos, rules.white,  ports.white)  end
	end
end

-----------------
-- Overheating --
-----------------
local function burn_controller(pos)
	local node = minetest.get_node(pos)
	node.name = BASENAME.."_burnt"
	minetest.swap_node(pos, node)
	minetest.get_meta(pos):set_string("lc_memory", "");
	-- Wait for pending operations
	minetest.after(0.2, mesecon.receptor_off, pos, mesecon.rules.flat)
end

local function overheat(pos, meta)
	if mesecon.do_overheat(pos) then -- If too hot
		burn_controller(pos)
		return true
	end
end

------------------------
-- Ignored off events --
------------------------

local function ignore_event(event, meta)
	if event.type ~= "off" then return false end
	local ignore_offevents = minetest.deserialize(meta:get_string("ignore_offevents")) or {}
	if ignore_offevents[event.pin.name] then
		ignore_offevents[event.pin.name] = nil
		meta:set_string("ignore_offevents", minetest.serialize(ignore_offevents))
		return true
	end
end

-------------------------
-- Parsing and running --
-------------------------

local function safe_print(param)
	print(dump(param))
end

local function safe_date()
	return(os.date("*t",os.time()))
end

-- string.rep(str, n) with a high value for n can be used to DoS
-- the server. Therefore, limit max. length of generated string.
local function safe_string_rep(str, n)
	if #str * n > mesecon.setting("luacontroller_string_rep_max", 64000) then
		debug.sethook() -- Clear hook
		error("string.rep: string length overflow", 2)
	end

	return string.rep(str, n)
end

-- string.find with a pattern can be used to DoS the server.
-- Therefore, limit string.find to patternless matching.
local function safe_string_find(...)
	if (select(4, ...)) ~= true then
		debug.sethook() -- Clear hook
		error("string.find: 'plain' (fourth parameter) must always be true in a lua controlled tube")
	end

	return string.find(...)
end

local function remove_functions(x)
	local tp = type(x)
	if tp == "function" then
		return nil
	end

	-- Make sure to not serialize the same table multiple times, otherwise
	-- writing mem.test = mem in the lua controlled tube will lead to infinite recursion
	local seen = {}

	local function rfuncs(x)
		if seen[x] then return end
		seen[x] = true
		if type(x) ~= "table" then return end

		for key, value in pairs(x) do
			if type(key) == "function" or type(value) == "function" then
				x[key] = nil
			else
				if type(key) == "table" then
					rfuncs(key)
				end
				if type(value) == "table" then
					rfuncs(value)
				end
			end
		end
	end

	rfuncs(x)

	return x
end

local function get_interrupt(pos)
	-- iid = interrupt id
	local function interrupt(time, iid)
		if type(time) ~= "number" then return end
		local luac_id = minetest.get_meta(pos):get_int("luac_id")
		mesecon.queue:add_action(pos, "pipeworks:lc_tube_interrupt", {luac_id, iid}, time, iid, 1)
	end
	return interrupt
end

local function get_digiline_send(pos)
	if not digiline then return end
	return function(channel, msg)
		-- Make sure channel is string, number or boolean
		if (type(channel) ~= "string" and type(channel) ~= "number" and type(channel) ~= "boolean") then
			return false
		end

		-- It is technically possible to send functions over the wire since
		-- the high performance impact of stripping those from the data has
		-- been decided to not be worth the added realism.
		-- Make sure serialized version of the data is not insanely long to
		-- prevent DoS-like attacks
		local msg_ser = minetest.serialize(msg)
		if #msg_ser > mesecon.setting("luacontroller_digiline_maxlen", 50000) then
			return false
		end

		minetest.after(0, function()
			digilines.receptor_send(pos, digiline_rules_luatube, channel, msg)
		end)
		return true
	end
end

local safe_globals = {
	"assert", "error", "ipairs", "next", "pairs", "select",
	"tonumber", "tostring", "type", "unpack", "_VERSION"
}

local function create_environment(pos, mem, event)
	-- Make sure the tube hasn't broken.
	local vports = minetest.registered_nodes[minetest.get_node(pos).name].virtual_portstates
	if not vports then return {} end

	-- Gather variables for the environment
	local vports_copy = {}
	for k, v in pairs(vports) do vports_copy[k] = v end
	local rports = get_real_port_states(pos)

	-- Create new library tables on each call to prevent one Luacontroller
	-- from breaking a library and messing up other Luacontrollers.
	local env = {
		pin = merge_port_states(vports, rports),
		port = vports_copy,
		event = event,
		mem = mem,
		heat = mesecon.get_heat(pos),
		heat_max = mesecon.setting("overheat_max", 20),
		print = safe_print,
		interrupt = get_interrupt(pos),
		digiline_send = get_digiline_send(pos),
		string = {
			byte = string.byte,
			char = string.char,
			format = string.format,
			len = string.len,
			lower = string.lower,
			upper = string.upper,
			rep = safe_string_rep,
			reverse = string.reverse,
			sub = string.sub,
			find = safe_string_find,
		},
		math = {
			abs = math.abs,
			acos = math.acos,
			asin = math.asin,
			atan = math.atan,
			atan2 = math.atan2,
			ceil = math.ceil,
			cos = math.cos,
			cosh = math.cosh,
			deg = math.deg,
			exp = math.exp,
			floor = math.floor,
			fmod = math.fmod,
			frexp = math.frexp,
			huge = math.huge,
			ldexp = math.ldexp,
			log = math.log,
			log10 = math.log10,
			max = math.max,
			min = math.min,
			modf = math.modf,
			pi = math.pi,
			pow = math.pow,
			rad = math.rad,
			random = math.random,
			sin = math.sin,
			sinh = math.sinh,
			sqrt = math.sqrt,
			tan = math.tan,
			tanh = math.tanh,
		},
		table = {
			concat = table.concat,
			insert = table.insert,
			maxn = table.maxn,
			remove = table.remove,
			sort = table.sort,
		},
		os = {
			clock = os.clock,
			difftime = os.difftime,
			time = os.time,
			datetable = safe_date,
		},
	}
	env._G = env

	for _, name in pairs(safe_globals) do
		env[name] = _G[name]
	end

	return env
end

local function timeout()
	debug.sethook() -- Clear hook
	error("Code timed out!", 2)
end

local function create_sandbox(code, env)
	if code:byte(1) == 27 then
		return nil, "Binary code prohibited."
	end
	local f, msg = loadstring(code)
	if not f then return nil, msg end
	setfenv(f, env)

	-- Turn off JIT optimization for user code so that count
	-- events are generated when adding debug hooks
	if rawget(_G, "jit") then
		jit.off(f, true)
	end

	return function(...)
		-- Use instruction counter to stop execution
		-- after luacontroller_maxevents
		local maxevents = mesecon.setting("luacontroller_maxevents", 10000)
		debug.sethook(timeout, "", maxevents)
		local ok, ret = pcall(f, ...)
		debug.sethook()  -- Clear hook
		if not ok then error(ret, 0) end
		return ret
	end
end

local function load_memory(meta)
	return minetest.deserialize(meta:get_string("lc_memory")) or {}
end

local function save_memory(pos, meta, mem)
	local memstring = minetest.serialize(remove_functions(mem))
	local memsize_max = mesecon.setting("luacontroller_memsize", 100000)

	if (#memstring <= memsize_max) then
		meta:set_string("lc_memory", memstring)
	else
		print("Error: lua_tube memory overflow. "..memsize_max.." bytes available, "
				..#memstring.." required. Controller overheats.")
		burn_controller(pos)
	end
end

local function run(pos, event)
	local meta = minetest.get_meta(pos)
	if overheat(pos) then return end
	if ignore_event(event, meta) then return end

	-- Load code & mem from meta
	local mem  = load_memory(meta)
	local code = meta:get_string("code")

	-- Create environment
	local env = create_environment(pos, mem, event)

	-- Create the sandbox and execute code
	local f, msg = create_sandbox(code, env)
	if not f then return false, msg end
	local succ, msg = pcall(f)
	if not succ then return false, msg end
	if type(env.port) ~= "table" then
		return false, "Ports set are invalid."
	end

	-- Actually set the ports
	set_port_states(pos, env.port)

	-- Save memory. This may burn the luacontroller if a memory overflow occurs.
	save_memory(pos, meta, env.mem)

	return succ, msg
end

mesecon.queue:add_function("pipeworks:lc_tube_interrupt", function (pos, luac_id, iid)
	-- There is no lua_tube anymore / it has been reprogrammed / replaced / burnt
	if (minetest.get_meta(pos):get_int("luac_id") ~= luac_id) then return end
	if (minetest.registered_nodes[minetest.get_node(pos).name].is_burnt) then return end
	run(pos, {type = "interrupt", iid = iid})
end)

local function reset_meta(pos, code, errmsg)
	local meta = minetest.get_meta(pos)
	meta:set_string("code", code)
	code = minetest.formspec_escape(code or "")
	errmsg = minetest.formspec_escape(tostring(errmsg or ""))
	meta:set_string("formspec", "size[12,10]"..
		"background[-0.2,-0.25;12.4,10.75;jeija_luac_background.png]"..
		"textarea[0.2,0.2;12.2,9.5;code;;"..code.."]"..
		"image_button[4.75,8.75;2.5,1;jeija_luac_runbutton.png;program;]"..
		"image_button_exit[11.72,-0.25;0.425,0.4;jeija_close_window.png;exit;]"..
		"label[0.1,9;"..errmsg.."]")
	meta:set_int("luac_id", math.random(1, 65535))
end

local function reset(pos)
	set_port_states(pos, {red = false, blue = false, yellow = false,
		green = false, black = false, white = false})
end

-----------------------
-- Node Registration --
-----------------------

local output_rules = {}
local input_rules = {}

local node_box = {
	type = "fixed",
	fixed = {
		pipeworks.tube_leftstub[1],   -- tube segment against -X face
		pipeworks.tube_rightstub[1],  -- tube segment against +X face
		pipeworks.tube_bottomstub[1], -- tube segment against -Y face
		pipeworks.tube_topstub[1],    -- tube segment against +Y face
		pipeworks.tube_frontstub[1],  -- tube segment against -Z face
		pipeworks.tube_backstub[1],   -- tube segment against +Z face
	}
}

local selection_box = {
	type = "fixed",
	fixed = pipeworks.tube_selectboxes,
}

local digiline = {
	receptor = {},
	effector = {
		action = function(pos, node, channel, msg)
			run(pos, {type = "digiline", channel = channel, msg = msg})
		end
	},
	wire = {
		rules = pipeworks.digilines_rules
	},
}
local function on_receive_fields(pos, form_name, fields, sender)
	if not fields.program then
		return
	end
	local name = sender:get_player_name()
	if minetest.is_protected(pos, name) and not minetest.check_player_privs(name, {protection_bypass=true}) then
		minetest.record_protection_violation(pos, name)
		return
	end
	reset(pos)
	reset_meta(pos, fields.code)
	local succ, err = run(pos, {type="program"})
	if not succ then
		print(err)
		reset_meta(pos, fields.code, err)
	end
end

local function go_back(velocity)
	local adjlist={{x=0,y=0,z=1},{x=0,y=0,z=-1},{x=0,y=1,z=0},{x=0,y=-1,z=0},{x=1,y=0,z=0},{x=-1,y=0,z=0}}
	local speed = math.abs(velocity.x + velocity.y + velocity.z)
	if speed == 0 then
		speed = 1
	end
	local vel = {x = velocity.x/speed, y = velocity.y/speed, z = velocity.z/speed,speed=speed}
	if speed >= 4.1 then
		speed = 4
	elseif speed >= 1.1 then
		speed = speed - 0.1
	else
		speed = 1
	end
	vel.speed = speed
	return pipeworks.notvel(adjlist, vel)
end

local tiles_base = {
	"pipeworks_mese_tube_plain_4.png", "pipeworks_mese_tube_plain_3.png",
	"pipeworks_mese_tube_plain_2.png", "pipeworks_mese_tube_plain_1.png",
	"pipeworks_mese_tube_plain_6.png", "pipeworks_mese_tube_plain_5.png"}

for red    = 0, 1 do -- 0 = off  1 = on
for blue   = 0, 1 do
for yellow = 0, 1 do
for green  = 0, 1 do
for black  = 0, 1 do
for white  = 0, 1 do
	local cid = tostring(white)..tostring(black)..tostring(green)..
			tostring(yellow)..tostring(blue)..tostring(red)
	local node_name = BASENAME..cid
	local tiles = table.copy(tiles_base)
	if red == 1 then
		tiles[1] = tiles[1].."^(pipeworks_lua_tube_port_on.png^[transformR90)"
		tiles[2] = tiles[2].."^(pipeworks_lua_tube_port_on.png^[transformR90)"
		tiles[5] = tiles[5].."^(pipeworks_lua_tube_port_on.png^[transformR270)"
		tiles[6] = tiles[6].."^(pipeworks_lua_tube_port_on.png^[transformR90)"
	else
		tiles[1] = tiles[1].."^(pipeworks_lua_tube_port_off.png^[transformR90)"
		tiles[2] = tiles[2].."^(pipeworks_lua_tube_port_off.png^[transformR90)"
		tiles[5] = tiles[5].."^(pipeworks_lua_tube_port_off.png^[transformR270)"
		tiles[6] = tiles[6].."^(pipeworks_lua_tube_port_off.png^[transformR90)"
	end
	if blue == 1 then
		tiles[1] = tiles[1].."^(pipeworks_lua_tube_port_on.png^[transformR270)"
		tiles[2] = tiles[2].."^(pipeworks_lua_tube_port_on.png^[transformR270)"
		tiles[5] = tiles[5].."^(pipeworks_lua_tube_port_on.png^[transformR90)"
		tiles[6] = tiles[6].."^(pipeworks_lua_tube_port_on.png^[transformR270)"
	else
		tiles[1] = tiles[1].."^(pipeworks_lua_tube_port_off.png^[transformR270)"
		tiles[2] = tiles[2].."^(pipeworks_lua_tube_port_off.png^[transformR270)"
		tiles[5] = tiles[5].."^(pipeworks_lua_tube_port_off.png^[transformR90)"
		tiles[6] = tiles[6].."^(pipeworks_lua_tube_port_off.png^[transformR270)"
	end
	if yellow == 1 then
		tiles[3] = tiles[3].."^(pipeworks_lua_tube_port_on.png^[transformR180)"
		tiles[4] = tiles[4].."^(pipeworks_lua_tube_port_on.png^[transformR180)"
		tiles[5] = tiles[5].."^(pipeworks_lua_tube_port_on.png^[transformR180)"
		tiles[6] = tiles[6].."^(pipeworks_lua_tube_port_on.png^[transformR180)"
	else
		tiles[3] = tiles[3].."^(pipeworks_lua_tube_port_off.png^[transformR180)"
		tiles[4] = tiles[4].."^(pipeworks_lua_tube_port_off.png^[transformR180)"
		tiles[5] = tiles[5].."^(pipeworks_lua_tube_port_off.png^[transformR180)"
		tiles[6] = tiles[6].."^(pipeworks_lua_tube_port_off.png^[transformR180)"
	end
	if green == 1 then
		tiles[3] = tiles[3].."^pipeworks_lua_tube_port_on.png"
		tiles[4] = tiles[4].."^pipeworks_lua_tube_port_on.png"
		tiles[5] = tiles[5].."^pipeworks_lua_tube_port_on.png"
		tiles[6] = tiles[6].."^pipeworks_lua_tube_port_on.png"
	else
		tiles[3] = tiles[3].."^pipeworks_lua_tube_port_off.png"
		tiles[4] = tiles[4].."^pipeworks_lua_tube_port_off.png"
		tiles[5] = tiles[5].."^pipeworks_lua_tube_port_off.png"
		tiles[6] = tiles[6].."^pipeworks_lua_tube_port_off.png"
	end
	if black == 1 then
		tiles[1] = tiles[1].."^(pipeworks_lua_tube_port_on.png^[transformR180)"
		tiles[2] = tiles[2].."^pipeworks_lua_tube_port_on.png"
		tiles[3] = tiles[3].."^(pipeworks_lua_tube_port_on.png^[transformR90)"
		tiles[4] = tiles[4].."^(pipeworks_lua_tube_port_on.png^[transformR270)"
	else
		tiles[1] = tiles[1].."^(pipeworks_lua_tube_port_off.png^[transformR180)"
		tiles[2] = tiles[2].."^pipeworks_lua_tube_port_off.png"
		tiles[3] = tiles[3].."^(pipeworks_lua_tube_port_off.png^[transformR90)"
		tiles[4] = tiles[4].."^(pipeworks_lua_tube_port_off.png^[transformR270)"
	end
	if white == 1 then
		tiles[1] = tiles[1].."^pipeworks_lua_tube_port_on.png"
		tiles[2] = tiles[2].."^(pipeworks_lua_tube_port_on.png^[transformR180)"
		tiles[3] = tiles[3].."^(pipeworks_lua_tube_port_on.png^[transformR270)"
		tiles[4] = tiles[4].."^(pipeworks_lua_tube_port_on.png^[transformR90)"
	else
		tiles[1] = tiles[1].."^pipeworks_lua_tube_port_off.png"
		tiles[2] = tiles[2].."^(pipeworks_lua_tube_port_off.png^[transformR180)"
		tiles[3] = tiles[3].."^(pipeworks_lua_tube_port_off.png^[transformR270)"
		tiles[4] = tiles[4].."^(pipeworks_lua_tube_port_off.png^[transformR90)"
	end

	local groups = {snappy = 3, tube = 1, tubedevice = 1, overheat = 1}
	if red + blue + yellow + green + black + white ~= 0 then
		groups.not_in_creative_inventory = 1
	end

	output_rules[cid] = {}
	input_rules[cid] = {}
	if red    == 1 then table.insert(output_rules[cid], rules.red)    end
	if blue   == 1 then table.insert(output_rules[cid], rules.blue)   end
	if yellow == 1 then table.insert(output_rules[cid], rules.yellow) end
	if green  == 1 then table.insert(output_rules[cid], rules.green)  end
	if black  == 1 then table.insert(output_rules[cid], rules.black)  end
	if white  == 1 then table.insert(output_rules[cid], rules.white)  end

	if red    == 0 then table.insert( input_rules[cid], rules.red)    end
	if blue   == 0 then table.insert( input_rules[cid], rules.blue)   end
	if yellow == 0 then table.insert( input_rules[cid], rules.yellow) end
	if green  == 0 then table.insert( input_rules[cid], rules.green)  end
	if black  == 0 then table.insert( input_rules[cid], rules.black)  end
	if white  == 0 then table.insert( input_rules[cid], rules.white)  end

	local mesecons = {
		effector = {
			rules = input_rules[cid],
			action_change = function (pos, _, rule_name, new_state)
				update_real_port_states(pos, rule_name, new_state)
				run(pos, {type=new_state, pin=rule_name})
			end,
		},
		receptor = {
			state = mesecon.state.on,
			rules = output_rules[cid]
		}
	}

	minetest.register_node(node_name, {
		description = "Lua controlled Tube",
		drawtype = "nodebox",
		tiles = tiles,
		paramtype = "light",
		groups = groups,
		drop = BASENAME.."000000",
		sunlight_propagates = true,
		selection_box = selection_box,
		node_box = node_box,
		on_construct = reset_meta,
		on_receive_fields = on_receive_fields,
		sounds = default.node_sound_wood_defaults(),
		mesecons = mesecons,
		digiline = digiline,
		-- Virtual portstates are the ports that
		-- the node shows as powered up (light up).
		virtual_portstates = {
			red    = red    == 1,
			blue   = blue   == 1,
			yellow = yellow == 1,
			green  = green  == 1,
			black  = black  == 1,
			white  = white  == 1,
		},
		after_dig_node = function(pos, node)
			mesecon.do_cooldown(pos)
			mesecon.receptor_off(pos, output_rules)
			pipeworks.after_dig(pos, node)
		end,
		is_luacontroller = true,
		tubelike = 1,
		tube = {
			connect_sides = {front = 1, back = 1, left = 1, right = 1, top = 1, bottom = 1},
			priority = 50,
			can_go = function(pos, node, velocity, stack)
				local src = {name = nil}
				-- add color of the incoming tube explicitly; referring to rules, in case they change later
				for color, rule in pairs(rules) do
					if (-velocity.x == rule.x and -velocity.y == rule.y and -velocity.z == rule.z) then
						src.name = rule.name
						break
					end
				end
				local succ, msg = run(pos, {
					type = "item",
					pin = src,
					itemstring = stack:to_string(),
					item = stack:to_table(),
					velocity = velocity,
				})
				if not succ or type(msg) ~= "string" then
					return go_back(velocity)
				end
				local r = rules[msg]
				return r and {r} or go_back(velocity)
			end,
		},
		after_place_node = pipeworks.after_place,
		on_blast = function(pos, intensity)
			if not intensity or intensity > 1 + 3^0.5 then
				minetest.remove_node(pos)
				return {string.format("%s_%s", name, dropname)}
			end
			minetest.swap_node(pos, {name = "pipeworks:broken_tube_1"})
			pipeworks.scan_for_tube_objects(pos)
		end,
		on_blast = function(pos, intensity)
			if not intensity or intensity > 1 + 3^0.5 then
				minetest.remove_node(pos)
				return {string.format("%s_%s", name, dropname)}
			end
			minetest.swap_node(pos, {name = "pipeworks:broken_tube_1"})
			pipeworks.scan_for_tube_objects(pos)
		end,
	})
end
end
end
end
end
end

------------------------------------
-- Overheated Lua controlled Tube --
------------------------------------

local tiles_burnt = table.copy(tiles_base)
tiles_burnt[1] = tiles_burnt[1].."^(pipeworks_lua_tube_port_burnt.png^[transformR90)"
tiles_burnt[2] = tiles_burnt[2].."^(pipeworks_lua_tube_port_burnt.png^[transformR90)"
tiles_burnt[5] = tiles_burnt[5].."^(pipeworks_lua_tube_port_burnt.png^[transformR270)"
tiles_burnt[6] = tiles_burnt[6].."^(pipeworks_lua_tube_port_burnt.png^[transformR90)"
tiles_burnt[1] = tiles_burnt[1].."^(pipeworks_lua_tube_port_burnt.png^[transformR270)"
tiles_burnt[2] = tiles_burnt[2].."^(pipeworks_lua_tube_port_burnt.png^[transformR270)"
tiles_burnt[5] = tiles_burnt[5].."^(pipeworks_lua_tube_port_burnt.png^[transformR90)"
tiles_burnt[6] = tiles_burnt[6].."^(pipeworks_lua_tube_port_burnt.png^[transformR270)"
tiles_burnt[3] = tiles_burnt[3].."^(pipeworks_lua_tube_port_burnt.png^[transformR180)"
tiles_burnt[4] = tiles_burnt[4].."^(pipeworks_lua_tube_port_burnt.png^[transformR180)"
tiles_burnt[5] = tiles_burnt[5].."^(pipeworks_lua_tube_port_burnt.png^[transformR180)"
tiles_burnt[6] = tiles_burnt[6].."^(pipeworks_lua_tube_port_burnt.png^[transformR180)"
tiles_burnt[3] = tiles_burnt[3].."^pipeworks_lua_tube_port_burnt.png"
tiles_burnt[4] = tiles_burnt[4].."^pipeworks_lua_tube_port_burnt.png"
tiles_burnt[5] = tiles_burnt[5].."^pipeworks_lua_tube_port_burnt.png"
tiles_burnt[6] = tiles_burnt[6].."^pipeworks_lua_tube_port_burnt.png"
tiles_burnt[1] = tiles_burnt[1].."^(pipeworks_lua_tube_port_burnt.png^[transformR180)"
tiles_burnt[2] = tiles_burnt[2].."^pipeworks_lua_tube_port_burnt.png"
tiles_burnt[3] = tiles_burnt[3].."^(pipeworks_lua_tube_port_burnt.png^[transformR90)"
tiles_burnt[4] = tiles_burnt[4].."^(pipeworks_lua_tube_port_burnt.png^[transformR270)"
tiles_burnt[1] = tiles_burnt[1].."^pipeworks_lua_tube_port_burnt.png"
tiles_burnt[2] = tiles_burnt[2].."^(pipeworks_lua_tube_port_burnt.png^[transformR180)"
tiles_burnt[3] = tiles_burnt[3].."^(pipeworks_lua_tube_port_burnt.png^[transformR270)"
tiles_burnt[4] = tiles_burnt[4].."^(pipeworks_lua_tube_port_burnt.png^[transformR90)"

minetest.register_node(BASENAME .. "_burnt", {
	drawtype = "nodebox",
	tiles = tiles_burnt,
	is_burnt = true,
	paramtype = "light",
	groups = {snappy = 3, tube = 1, tubedevice = 1, not_in_creative_inventory=1},
	drop = BASENAME.."000000",
	sunlight_propagates = true,
	selection_box = selection_box,
	node_box = node_box,
	on_construct = reset_meta,
	on_receive_fields = on_receive_fields,
	sounds = default.node_sound_wood_defaults(),
	virtual_portstates = {red = false, blue = false, yellow = false,
		green = false, black = false, white = false},
	mesecons = {
		effector = {
			rules = mesecon.rules.alldirs,
			action_change = function(pos, _, rule_name, new_state)
				update_real_port_states(pos, rule_name, new_state)
			end,
		},
	},
	tubelike = 1,
	tube = {
		connect_sides = {front = 1, back = 1, left = 1, right = 1, top = 1, bottom = 1},
		priority = 50,
	},
	after_place_node = pipeworks.after_place,
	after_dig_node = pipeworks.after_dig,
	on_blast = function(pos, intensity)
		if not intensity or intensity > 1 + 3^0.5 then
			minetest.remove_node(pos)
			return {string.format("%s_%s", name, dropname)}
		end
		minetest.swap_node(pos, {name = "pipeworks:broken_tube_1"})
		pipeworks.scan_for_tube_objects(pos)
	end,
})

------------------------
-- Craft Registration --
------------------------

minetest.register_craft({
	type = "shapeless",
	output = BASENAME.."000000",
	recipe = {"pipeworks:mese_tube_000000", "mesecons_luacontroller:luacontroller0000"},
})


code:
local max_entity_id = 1000000000000 -- If you need more, there's a problem with your code

local luaentity = {}
pipeworks.luaentity = luaentity

luaentity.registered_entities = {}

local filename = minetest.get_worldpath().."/luaentities"
local function read_file()
	local f = io.open(filename, "r")
	if f == nil then return {} end
    	local t = f:read("*all")
    	f:close()
	if t == "" or t == nil then return {} end
	return minetest.deserialize(t) or {}
end

local function write_file(tbl)
	local f = io.open(filename, "w")
    	f:write(minetest.serialize(tbl))
    	f:close()
end

local function read_entities()
	local t = read_file()
	for _, entity in pairs(t) do

		local x=entity.start_pos.x
		local y=entity.start_pos.y
		local z=entity.start_pos.z

		x=math.max(-30912,x)
		y=math.max(-30912,y)
		z=math.max(-30912,z)
		x=math.min(30927,x)
		y=math.min(30927,y)
		z=math.min(30927,z)

		entity.start_pos.x = x
		entity.start_pos.y = y
		entity.start_pos.z = z

		setmetatable(entity, luaentity.registered_entities[entity.name])
	end
	return t
end

local function write_entities()
	if not luaentity.entities then
		-- This can happen if crashing on startup, causing another error that
		-- masks the original one. Return gracefully in that case instead.
		return
	end
	for _, entity in pairs(luaentity.entities) do
		setmetatable(entity, nil)
		for _, attached in pairs(entity._attached_entities) do
			if attached.entity then
				attached.entity:remove()
				attached.entity = nil
			end
		end
		entity._attached_entities_master = nil
	end
	write_file(luaentity.entities)
end

minetest.register_on_shutdown(write_entities)
luaentity.entities_index = 0

local function get_blockpos(pos)
	return {x = math.floor(pos.x / 16),
	        y = math.floor(pos.y / 16),
	        z = math.floor(pos.z / 16)}
end

local active_blocks = {} -- These only contain active blocks near players (i.e., not forceloaded ones)

local move_entities_globalstep_part1 = function(dtime)
	local active_block_range = tonumber(minetest.settings:get("active_block_range")) or 2
	local new_active_blocks = {}
	for _, player in ipairs(minetest.get_connected_players()) do
		local blockpos = get_blockpos(player:get_pos())
		local minp = vector.subtract(blockpos, active_block_range)
		local maxp = vector.add(blockpos, active_block_range)

		for x = minp.x, maxp.x do
		for y = minp.y, maxp.y do
		for z = minp.z, maxp.z do
			local pos = {x = x, y = y, z = z}
			new_active_blocks[minetest.hash_node_position(pos)] = pos
		end
		end
		end
	end
	active_blocks = new_active_blocks
	-- todo: callbacks on block load/unload
end

local function is_active(pos)
	return active_blocks[minetest.hash_node_position(get_blockpos(pos))] ~= nil
end

local entitydef_default = {
	_attach = function(self, attached, attach_to)
		local attached_def = self._attached_entities[attached]
		local attach_to_def = self._attached_entities[attach_to]
		attached_def.entity:set_attach(
			attach_to_def.entity, "",
			vector.subtract(attached_def.offset, attach_to_def.offset), -- todo: Does not work because is object space
			vector.new(0, 0, 0)
		)
	end,
	_set_master = function(self, index)
		self._attached_entities_master = index
		if not index then
			return
		end
		local def = self._attached_entities[index]
		if not def.entity then
			return
		end
		def.entity:set_pos(vector.add(self._pos, def.offset))
		def.entity:set_velocity(self._velocity)
		def.entity:set_acceleration(self._acceleration)
	end,
	_attach_all = function(self)
		local master = self._attached_entities_master
		if not master then
			return
		end
		for id, entity in pairs(self._attached_entities) do
			if id ~= master and entity.entity then
				self:_attach(id, master)
			end
		end
	end,
	_detach_all = function(self)
		local master = self._attached_entities_master
		for id, entity in pairs(self._attached_entities) do
			if id ~= master and entity.entity then
				entity.entity:set_detach()
			end
		end
	end,
	_add_attached = function(self, index)
		local entity = self._attached_entities[index]
		if entity.entity then
			return
		end
		local entity_pos = vector.add(self._pos, entity.offset)
		if not is_active(entity_pos) then
			return
		end
		local ent = minetest.add_entity(entity_pos, entity.name):get_luaentity()
		ent:from_data(entity.data)
		ent.parent_id = self._id
		ent.attached_id = index
		entity.entity = ent.object
		local master = self._attached_entities_master
		if master then
			self:_attach(index, master)
		else
			self:_set_master(index)
		end
	end,
	_remove_attached = function(self, index)
		local master = self._attached_entities_master
		local entity = self._attached_entities[index]
		local ent = entity and entity.entity
		if entity then entity.entity = nil end
		if index == master then
			self:_detach_all()
			local newmaster
			for id, attached in pairs(self._attached_entities) do
				if id ~= master and attached.entity then
					newmaster = id
					break
				end
			end
			self:_set_master(newmaster)
			self:_attach_all()
		elseif master and ent then
			ent:set_detach()
		end
		if ent then
			ent:remove()
		end
	end,
	_add_loaded = function(self)
		for id, _ in pairs(self._attached_entities) do
			self:_add_attached(id)
		end
	end,
	get_id = function(self)
		return self._id
	end,
	get_pos = function(self)
		return vector.new(self._pos)
	end,
	set_pos = function(self, pos)
		self._pos = vector.new(pos)
		--for _, entity in pairs(self._attached_entities) do
		--	if entity.entity then
		--		entity.entity:set_pos(vector.add(self._pos, entity.offset))
		--	end
		--end
		local master = self._attached_entities_master
		if master then
			local master_def = self._attached_entities[master]
			master_def.entity:set_pos(vector.add(self._pos, master_def.offset))
		end
	end,
	get_velocity = function(self)
		return vector.new(self._velocity)
	end,
	set_velocity = function(self, velocity)
		self._velocity = vector.new(velocity)
		local master = self._attached_entities_master
		if master then
			self._attached_entities[master].entity:set_velocity(self._velocity)
		end
	end,
	get_acceleration = function(self)
		return vector.new(self._acceleration)
	end,
	set_acceleration = function(self, acceleration)
		self._acceleration = vector.new(acceleration)
		local master = self._attached_entities_master
		if master then
			self._attached_entities[master].entity:set_acceleration(self._acceleration)
		end
	end,
	remove = function(self)
		self:_detach_all()
		for _, entity in pairs(self._attached_entities) do
			if entity.entity then
				entity.entity:remove()
			end
		end
		luaentity.entities[self._id] = nil
	end,
	add_attached_entity = function(self, name, data, offset)
		local index = #self._attached_entities + 1
		self._attached_entities[index] = {
			name = name,
			data = data,
			offset = vector.new(offset),
		}
		self:_add_attached(index)
		return index
	end,
	remove_attached_entity = function(self, index)
		self:_remove_attached(index)
		self._attached_entities[index] = nil
	end,
}

function luaentity.register_entity(name, prototype)
	-- name = check_modname_prefix(name)
	prototype.name = name
	setmetatable(prototype, {__index = entitydef_default})
	prototype.__index = prototype -- Make it possible to use it as metatable
	luaentity.registered_entities[name] = prototype
end

-- function luaentity.get_entity_definition(entity)
--	 return luaentity.registered_entities[entity.name]
-- end

function luaentity.add_entity(pos, name)
	if not luaentity.entities then
		minetest.after(0, luaentity.add_entity, vector.new(pos), name)
		return
	end
	local index = luaentity.entities_index
	while luaentity.entities[index] do
		index = index + 1
		if index >= max_entity_id then
			index = 0
		end
	end
	luaentity.entities_index = index

	local entity = {
		name = name,
		_id = index,
		_pos = vector.new(pos),
		_velocity = {x = 0, y = 0, z = 0},
		_acceleration = {x = 0, y = 0, z = 0},
		_attached_entities = {},
	}

	local prototype = luaentity.registered_entities[name]
	setmetatable(entity, prototype) -- Default to prototype for other methods
	luaentity.entities[index] = entity

	if entity.on_activate then
		entity:on_activate()
	end
	return entity
end

-- todo: check if remove in get_staticdata works
function luaentity.get_staticdata(self)
	local parent = luaentity.entities[self.parent_id]
	if parent and parent._remove_attached then
		parent:_remove_attached(self.attached_id)
	end
	return "toremove"
end

function luaentity.on_activate(self, staticdata)
	if staticdata == "toremove" then
		self.object:remove()
	end
end

function luaentity.get_objects_inside_radius(pos, radius)
	local objects = {}
	local index = 1
	for id, entity in pairs(luaentity.entities) do
		if vector.distance(pos, entity:get_pos()) <= radius then
			objects[index] = entity
			index = index + 1
		end
	end
end

local move_entities_globalstep_part2 = function(dtime)
	if not luaentity.entities then
		luaentity.entities = read_entities()
	end
	for id, entity in pairs(luaentity.entities) do
		local master = entity._attached_entities_master
		local master_def = master and entity._attached_entities[master]
		local master_entity = master_def and master_def.entity
		local master_entity_pos = master_entity and master_entity:get_pos()
		if master_entity_pos then
			entity._pos = vector.subtract(master_entity_pos, master_def.offset)
			entity._velocity = master_entity:get_velocity()
			entity._acceleration = master_entity:get_acceleration()
		else
			entity._pos = vector.add(vector.add(
				entity._pos,
				vector.multiply(entity._velocity, dtime)),
				vector.multiply(entity._acceleration, 0.5 * dtime * dtime))
			entity._velocity = vector.add(
				entity._velocity,
				vector.multiply(entity._acceleration, dtime))
		end
		if master and not master_entity_pos then -- The entity has somehow been cleared
			if pipeworks.delete_item_on_clearobject then
				entity:remove()
			else
				entity:_remove_attached(master)
				entity:_add_loaded()
				if entity.on_step then
					entity:on_step(dtime)
				end
			end
		else
			entity:_add_loaded()
			if entity.on_step then
				entity:on_step(dtime)
			end
		end
	end
end

local handle_active_blocks_timer = 0.1

minetest.register_globalstep(function(dtime)
	handle_active_blocks_timer = handle_active_blocks_timer + dtime
	if dtime < 0.2 or handle_active_blocks_timer >= (dtime * 3) then
		handle_active_blocks_timer = 0.1
		move_entities_globalstep_part1(dtime)
		move_entities_globalstep_part2(dtime)
	end
end)


code:
name = pipeworks
description = This mod uses mesh nodes and nodeboxes to supply a complete set of 3D pipes and tubes, along with devices that work with them.
depends = default, basic_materials, screwdriver
optional_depends = mesecons, mesecons_mvps, digilines, signs_lib
release = 3862
author = VanessaE
title = Pipeworks


code:
-- This file supplies the steel pipes
local S = minetest.get_translator("pipeworks")

local REGISTER_COMPATIBILITY = true

local pipes_empty_nodenames = {}
local pipes_full_nodenames = {}

local new_flow_logic_register = pipeworks.flowables.register

local polys = ""
if pipeworks.enable_lowpoly then polys = "_lowpoly" end

local vti = {4, 3, 2, 1, 6, 5}
local cconnects = {{}, {1}, {1, 2}, {1, 3}, {1, 3, 5}, {1, 2, 3}, {1, 2, 3, 5}, {1, 2, 3, 4}, {1, 2, 3, 4, 5}, {1, 2, 3, 4, 5, 6}}
for index, connects in ipairs(cconnects) do
	local outsel = {}

	local jx = 0
	local jy = 0
	local jz = 0
	for _, v in ipairs(connects) do
		if v == 1 or v == 2 then
			jx = jx + 1
		elseif v == 3 or v == 4 then
			jy = jy + 1
		else
			jz = jz + 1
		end
		table.insert(outsel, pipeworks.pipe_selectboxes[v])
	end

	if #connects == 1 then
		local v = connects[1]
		v = v-1 + 2*(v%2) -- Opposite side
	end

	local pgroups = {snappy = 3, pipe = 1, not_in_creative_inventory = 1}
	local pipedesc = S("Pipe Segment").." "..dump(connects)

	if #connects == 0 then
		pgroups = {snappy = 3, tube = 1}
		pipedesc = S("Pipe Segment")
	end

	local outimg_e = { "pipeworks_pipe_plain.png" }
	local outimg_l = { "pipeworks_pipe_plain.png" }

	if index == 3 then
		outimg_e = { "pipeworks_pipe_3_empty.png" }
		outimg_l = { "pipeworks_pipe_3_loaded.png" }
	end

	local mesh = "pipeworks_pipe_"..index..polys..".obj"

	if index == 1 then
		mesh = "pipeworks_pipe_3"..polys..".obj"
	end

	minetest.register_node("pipeworks:pipe_"..index.."_empty", {
		description = pipedesc,
		drawtype = "mesh",
		mesh = mesh,
		tiles = outimg_e,
		sunlight_propagates = true,
		paramtype = "light",
		paramtype2 = "facedir",
		selection_box = {
			type = "fixed",
			fixed = outsel
		},
		collision_box = {
			type = "fixed",
			fixed = outsel
		},
		groups = pgroups,
		sounds = default.node_sound_metal_defaults(),
		walkable = true,
		drop = "pipeworks:pipe_1_empty",
		after_place_node = function(pos)
			pipeworks.scan_for_pipe_objects(pos)
		end,
		after_dig_node = function(pos)
			pipeworks.scan_for_pipe_objects(pos)
		end,
		on_rotate = false,
		check_for_pole = pipeworks.check_for_vert_pipe,
		check_for_horiz_pole = pipeworks.check_for_horiz_pipe,
		pipenumber = index
	})

	local pgroups = {snappy = 3, pipe = 1, not_in_creative_inventory = 1}

	minetest.register_node("pipeworks:pipe_"..index.."_loaded", {
		description = pipedesc,
		drawtype = "mesh",
		mesh = mesh,
		tiles = outimg_l,
		sunlight_propagates = true,
		paramtype = "light",
		paramtype2 = "facedir",
		selection_box = {
			type = "fixed",
			fixed = outsel
		},
		collision_box = {
			type = "fixed",
			fixed = outsel
		},
		groups = pgroups,
		sounds = default.node_sound_metal_defaults(),
		walkable = true,
		drop = "pipeworks:pipe_1_empty",
		after_place_node = function(pos)
			minetest.set_node(pos, { name = "pipeworks:pipe_"..index.."_empty" })
			pipeworks.scan_for_pipe_objects(pos)
		end,
		after_dig_node = function(pos)
			pipeworks.scan_for_pipe_objects(pos)
		end,
		on_rotate = false,
		check_for_pole = pipeworks.check_for_vert_pipe,
		check_for_horiz_pole = pipeworks.check_for_horiz_pipe,
		pipenumber = index
	})

	local emptypipe = "pipeworks:pipe_"..index.."_empty"
	local fullpipe = "pipeworks:pipe_"..index.."_loaded"
	table.insert(pipes_empty_nodenames, emptypipe)
	table.insert(pipes_full_nodenames, fullpipe)
	new_flow_logic_register.simple(emptypipe)
	new_flow_logic_register.simple(fullpipe)
end

if REGISTER_COMPATIBILITY then
	local cempty = "pipeworks:pipe_compatibility_empty"
	local cloaded = "pipeworks:pipe_compatibility_loaded"
	minetest.register_node(cempty, {
		drawtype = "airlike",
		sunlight_propagates = true,
		paramtype = "light",
		description = S("Pipe Segment (legacy)"),
		groups = {not_in_creative_inventory = 1, pipe_to_update = 1},
		drop = "pipeworks:pipe_1_empty",
		after_place_node = function(pos)
			pipeworks.scan_for_pipe_objects(pos)
		end,
		on_rotate = false

	})
	minetest.register_node(cloaded, {
		drawtype = "airlike",
		sunlight_propagates = true,
		paramtype = "light",
		groups = {not_in_creative_inventory = 1, pipe_to_update = 1},
		drop = "pipeworks:pipe_1_empty",
		after_place_node = function(pos)
			pipeworks.scan_for_pipe_objects(pos)
		end,
		on_rotate = false

	})
	for xm = 0, 1 do
	for xp = 0, 1 do
	for ym = 0, 1 do
	for yp = 0, 1 do
	for zm = 0, 1 do
	for zp = 0, 1 do
		local pname = xm..xp..ym..yp..zm..zp
		minetest.register_alias("pipeworks:pipe_"..pname.."_empty", cempty)
		minetest.register_alias("pipeworks:pipe_"..pname.."_loaded", cloaded)
	end
	end
	end
	end
	end
	end
	minetest.register_abm({
		nodenames = {"group:pipe_to_update"},
		interval = 1,
		chance = 1,
		action = function(pos, node, active_object_count, active_object_count_wider)
			local minp = {x = pos.x-1, y = pos.y-1, z = pos.z-1}
			local maxp = {x = pos.x+1, y = pos.y+1, z = pos.z+1}
			if table.getn(minetest.find_nodes_in_area(minp, maxp, "ignore")) == 0 then
				pipeworks.scan_for_pipe_objects(pos)
			end
		end
	})
end

local valve_on = "pipeworks:valve_on_empty"
local valve_off = "pipeworks:valve_off_empty"
local entry_panel_empty = "pipeworks:entry_panel_empty"
local flow_sensor_empty = "pipeworks:flow_sensor_empty"
local sp_empty = "pipeworks:straight_pipe_empty"
-- XXX: why aren't these in devices.lua!?
table.insert(pipes_empty_nodenames, valve_on)
table.insert(pipes_empty_nodenames, valve_off)
table.insert(pipes_empty_nodenames, entry_panel_empty)
table.insert(pipes_empty_nodenames, flow_sensor_empty)
table.insert(pipes_empty_nodenames, sp_empty)

local valve_on_loaded = "pipeworks:valve_on_loaded"
local entry_panel_loaded = "pipeworks:entry_panel_loaded"
local flow_sensor_loaded = "pipeworks:flow_sensor_loaded"
local sp_loaded = "pipeworks:straight_pipe_loaded"
table.insert(pipes_full_nodenames, valve_on_loaded)
table.insert(pipes_full_nodenames, entry_panel_loaded)
table.insert(pipes_full_nodenames, flow_sensor_loaded)
table.insert(pipes_full_nodenames, sp_loaded)

pipeworks.pipes_full_nodenames = pipes_full_nodenames
pipeworks.pipes_empty_nodenames = pipes_empty_nodenames

if pipeworks.toggles.pipe_mode == "classic" then

minetest.register_abm({
	nodenames = pipes_empty_nodenames,
	interval = 1,
	chance = 1,
	action = function(pos, node, active_object_count, active_object_count_wider)
		pipeworks.check_for_inflows(pos,node)
	end
})

minetest.register_abm({
	nodenames = pipes_full_nodenames,
	interval = 1,
	chance = 1,
	action = function(pos, node, active_object_count, active_object_count_wider)
		pipeworks.check_sources(pos,node)
	end
})

minetest.register_abm({
	nodenames = {"pipeworks:spigot","pipeworks:spigot_pouring"},
	interval = 1,
	chance = 1,
	action = function(pos, node, active_object_count, active_object_count_wider)
		pipeworks.spigot_check(pos,node)
	end
})

minetest.register_abm({
	nodenames = {"pipeworks:fountainhead","pipeworks:fountainhead_pouring"},
	interval = 1,
	chance = 1,
	action = function(pos, node, active_object_count, active_object_count_wider)
		pipeworks.fountainhead_check(pos,node)
	end
})

end


code:
-- register new flow logic ABMs
-- written 2017 by thetaepsilon

local register = {}
pipeworks.flowlogic.abmregister = register

local flowlogic = pipeworks.flowlogic

-- register node list for the main logic function.
-- see flowlogic.run() in abms.lua.

local register_flowlogic_abm = function(nodename)
	if pipeworks.toggles.pipe_mode == "pressure" then
		minetest.register_abm({
			label = "pipeworks new_flow_logic run",
			nodenames = { nodename },
			interval = 1,
			chance = 1,
			action = function(pos, node, active_object_count, active_object_count_wider)
				flowlogic.run(pos, node)
			end
		})
	else
		minetest.log("warning", "pipeworks pressure_logic not enabled but register.flowlogic() requested")
	end
end
register.flowlogic = register_flowlogic_abm


code:
-- 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


code:
-- registry of flowable node behaviours in new flow logic
-- written 2017 by thetaepsilon

-- the actual registration functions which edit these tables can be found in flowable_node_registry_install.lua
-- this is because the ABM code needs to inspect these tables,
-- but the registration code needs to reference said ABM code.
-- so those functions were split out to resolve a circular dependency.

pipeworks.flowables = {}
pipeworks.flowables.list = {}
pipeworks.flowables.list.all = {}
-- pipeworks.flowables.list.nodenames = {}

-- simple flowables - balance pressure in any direction
pipeworks.flowables.list.simple = {}
pipeworks.flowables.list.simple_nodenames = {}

-- directional flowables - can only flow on certain sides
-- format per entry is a table with the following fields:
-- neighbourfn: function(node),
--	called to determine which nodes to consider as neighbours.
--	can be used to e.g. inspect the node's param values for facedir etc.
--	returns: array of vector offsets to look for possible neighbours in
-- directionfn: function(node, vector):
--	can this node flow in this direction?
--	called in the context of another node to check the matching entry returned by neighbourfn.
-- for every offset vector returned by neighbourfn,
-- the node at that absolute position is checked.
-- if that node is also a directional flowable,
-- then that node's vector is passed to that node's directionfn
-- (inverted, so that directionfn sees a vector pointing out from it back to the origin node).
-- if directionfn agrees that the neighbour node can currently flow in that direction,
-- the neighbour is to participate in pressure balancing.
pipeworks.flowables.list.directional = {}

-- simple intakes - try to absorb any adjacent water nodes
pipeworks.flowables.inputs = {}
pipeworks.flowables.inputs.list = {}
pipeworks.flowables.inputs.nodenames = {}

-- outputs - takes pressure from pipes and update world to do something with it
pipeworks.flowables.outputs = {}
pipeworks.flowables.outputs.list = {}
-- not currently any nodenames arraylist for this one as it's not currently needed.

-- nodes with registered node transitions
-- nodes will be switched depending on pressure level
pipeworks.flowables.transitions = {}
pipeworks.flowables.transitions.list = {}	-- master list
pipeworks.flowables.transitions.simple = {}	-- nodes that change based purely on pressure
pipeworks.flowables.transitions.mesecons = {}	-- table of mesecons rules to apply on transition


code:
-- flowable node registry: add entries and install ABMs if new flow logic is enabled
-- written 2017 by thetaepsilon

-- use for hooking up ABMs as nodes are registered
local abmregister = pipeworks.flowlogic.abmregister

-- registration functions
pipeworks.flowables.register = {}
local register = pipeworks.flowables.register

-- some sanity checking for passed args, as this could potentially be made an external API eventually
local checkexists = function(nodename)
	if type(nodename) ~= "string" then error("pipeworks.flowables nodename must be a string!") end
	return pipeworks.flowables.list.all[nodename]
end

local insertbase = function(nodename)
	if checkexists(nodename) then error("pipeworks.flowables duplicate registration!") end
	pipeworks.flowables.list.all[nodename] = true
	-- table.insert(pipeworks.flowables.list.nodenames, nodename)
	if pipeworks.toggles.pipe_mode == "pressure" then
		abmregister.flowlogic(nodename)
	end
end

local regwarning = function(kind, nodename)
	local tail = ""
	if pipeworks.toggles.pipe_mode ~= "pressure" then tail = " but pressure logic not enabled" end
	--pipeworks.logger(kind.." flow logic registry requested for "..nodename..tail)
end

-- Register a node as a simple flowable.
-- Simple flowable nodes have no considerations for direction of flow;
-- A cluster of adjacent simple flowables will happily average out in any direction.
register.simple = function(nodename)
	insertbase(nodename)
	pipeworks.flowables.list.simple[nodename] = true
	table.insert(pipeworks.flowables.list.simple_nodenames, nodename)
	regwarning("simple", nodename)
end

-- Register a node as a directional flowable:
-- has a helper function which determines which nodes to consider valid neighbours.
register.directional = function(nodename, neighbourfn, directionfn)
	insertbase(nodename)
	pipeworks.flowables.list.directional[nodename] = {
		neighbourfn = neighbourfn,
		directionfn = directionfn
	}
	regwarning("directional", nodename)
end

-- register a node as a directional flowable that can only flow through either the top or bottom side.
-- used for fountainheads (bottom side) and pumps (top side).
-- this is in world terms, not facedir relative!
register.directional_vertical_fixed = function(nodename, topside)
	local y
	if topside then y = 1 else y = -1 end
	local side = { x=0, y=y, z=0 }
	local neighbourfn = function(node) return { side } end
	local directionfn = function(node, direction)
		return vector.equals(direction, side)
	end
	register.directional(nodename, neighbourfn, directionfn)
end

-- register a node as a directional flowable whose accepting sides depends upon param2 rotation.
-- used for entry panels, valves, flow sensors and spigots.
-- this is mostly for legacy reasons and SHOULD NOT BE USED IN NEW CODE.
register.directional_horizonal_rotate = function(nodename, doubleended)
	local rotations = {
		{x= 0,y= 0,z= 1},
		{x= 1,y= 0,z= 0},
		{x= 0,y= 0,z=-1},
		{x=-1,y= 0,z= 0},
	}
	local getends = function(node)
		--local dname = "horizontal rotate getends() "
		local param2 = node.param2
		-- the pipeworks nodes use a fixed value for vertical facing nodes
		-- if that is detected, just return that directly.
		if param2 == 17 then
			return {{x=0,y=1,z=0}, {x=0,y=-1,z=0}}
		end

		-- the sole end of the spigot points in the direction the rotation bits suggest
		-- also note to self: lua arrays start at one...
		local mainend = (param2 % 4) + 1
		-- use modulus wrap-around to find other end for straight-run devices like the valve
		local otherend = ((param2 + 2) % 4) + 1
		local mainrot = rotations[mainend]
		--pipeworks.logger(dname.."mainrot: "..dump(mainrot))
		local result
		if doubleended then
			result = { mainrot, rotations[otherend] }
		else
			result = { mainrot }
		end
		--pipeworks.logger(dname.."result: "..dump(result))
		return result
	end
	local neighbourfn = function(node)
		return getends(node)
	end
	local directionfn = function(node, direction)
		local result = false
		for index, endvec in ipairs(getends(node)) do
			if vector.equals(direction, endvec) then result = true end
		end
		return result
	end
	register.directional(nodename, neighbourfn, directionfn)
end

local checkbase = function(nodename)
	if not checkexists(nodename) then error("pipeworks.flowables node doesn't exist as a flowable!") end
end

local duplicateerr = function(kind, nodename) error(kind.." duplicate registration for "..nodename) end

-- Registers a node as a fluid intake.
-- intakefn is used to determine the water that can be taken in a node-specific way.
-- Expects node to be registered as a flowable (is present in flowables.list.all),
-- so that water can move out of it.
-- maxpressure is the maximum pipeline pressure that this node can drive;
-- if the input's node exceeds this the callback is not run.
-- possible WISHME here: technic-driven high-pressure pumps
register.intake = function(nodename, maxpressure, intakefn)
	-- check for duplicate registration of this node
	local list = pipeworks.flowables.inputs.list
	checkbase(nodename)
	if list[nodename] then duplicateerr("pipeworks.flowables.inputs", nodename) end
	list[nodename] = { maxpressure=maxpressure, intakefn=intakefn }
	regwarning("intake", nodename)
end

-- Register a node as a simple intake:
-- tries to absorb water source nodes from it's surroundings.
-- may exceed limit slightly due to needing to absorb whole nodes.
register.intake_simple = function(nodename, maxpressure)
	register.intake(nodename, maxpressure, pipeworks.flowlogic.check_for_liquids_v2)
end

-- Register a node as an output.
-- Expects node to already be a flowable.
-- upper and lower thresholds have different meanings depending on whether finite liquid mode is in effect.
-- if not (the default unless auto-detected),
-- nodes above their upper threshold have their outputfn invoked (and pressure deducted),
-- nodes between upper and lower are left idle,
-- and nodes below lower have their cleanup fn invoked (to say remove water sources).
-- the upper and lower difference acts as a hysteresis to try and avoid "gaps" in the flow.
-- if finite mode is on, upper is ignored and lower is used to determine whether to run outputfn;
-- cleanupfn is ignored in this mode as finite mode assumes something causes water to move itself.
register.output = function(nodename, upper, lower, outputfn, cleanupfn)
	if pipeworks.flowables.outputs.list[nodename] then
		error("pipeworks.flowables.outputs duplicate registration!")
	end
	checkbase(nodename)
	pipeworks.flowables.outputs.list[nodename] = {
		upper=upper,
		lower=lower,
		outputfn=outputfn,
		cleanupfn=cleanupfn,
	}
	-- output ABM now part of main flow logic ABM to preserve ordering.
	-- note that because outputs have to be a flowable first
	-- (and the installation of the flow logic ABM is conditional),
	-- registered output nodes for new_flow_logic is also still conditional on the enable flag.
	regwarning("output node", nodename)
end

-- register a simple output:
-- drains pressure by attempting to place water in nearby nodes,
-- which can be set by passing a list of offset vectors.
-- will attempt to drain as many whole nodes as there are positions in the offset list.
-- for meanings of upper and lower, see register.output() above.
-- non-finite mode:
--	above upper pressure: places water sources as appropriate, keeps draining pressure.
--	below lower presssure: removes it's neighbour water sources.
-- finite mode:
--	same as for above pressure in non-finite mode,
--	but only drains pressure when water source nodes are actually placed.
register.output_simple = function(nodename, upper, lower, neighbours)
	local outputfn = pipeworks.flowlogic.helpers.make_neighbour_output_fixed(neighbours)
	local cleanupfn = pipeworks.flowlogic.helpers.make_neighbour_cleanup_fixed(neighbours)
	register.output(nodename, upper, lower, outputfn, cleanupfn)
end

-- common base checking for transition nodes
-- ensures the node has only been registered once as a transition.
local transition_list = pipeworks.flowables.transitions.list
local insert_transition_base = function(nodename)
	checkbase(nodename)
	if transition_list[nodename] then duplicateerr("base transition", nodename) end
	transition_list[nodename] = true
end

-- register a simple transition set.
-- expects a table with nodenames as keys and threshold pressures as values.
-- internally, the table is sorted by value, and when one of these nodes needs to transition,
-- the table is searched starting from the lowest (even if it's value is non-zero),
-- until a value is found which is higher than or equal to the current node pressure.
-- ex. nodeset = { ["mod:level_0"] = 0, ["mod:level_1"] = 1, --[[ ... ]] }
local simpleseterror = function(msg)
	error("register.transition_simple_set(): "..msg)
end
local simple_transitions = pipeworks.flowables.transitions.simple

register.transition_simple_set = function(nodeset, extras)
	local set = {}
	if extras == nil then extras = {} end

	local length = #nodeset
	if length < 2 then simpleseterror("nodeset needs at least two elements!") end
	for index, element in ipairs(nodeset) do
		if type(element) ~= "table" then simpleseterror("element "..tostring(index).." in nodeset was not table!") end
		local nodename = element[1]
		local value = element[2]
		if type(nodename) ~= "string" then simpleseterror("nodename "..tostring(nodename).."was not a string!") end
		if type(value) ~= "number" then simpleseterror("pressure value "..tostring(value).."was not a number!") end
		insert_transition_base(nodename)
		if simple_transitions[nodename] then duplicateerr("simple transition set", nodename) end
		-- assigning set to table is done separately below

		table.insert(set, { nodename=nodename, threshold=value })
	end

	-- sort pressure values, smallest first
	local smallest_first = function(a, b)
		return a.threshold < b.threshold
	end
	table.sort(set, smallest_first)

	-- individual registration of each node, all sharing this set,
	-- so each node in the set will transition to the correct target node.
	for _, element in ipairs(set) do
		--pipeworks.logger("register.transition_simple_set() after sort: nodename "..element.nodename.." value "..tostring(element.threshold))
		simple_transitions[element.nodename] = set
	end

	-- handle extra options
	-- if mesecons rules table was passed, set for each node
	if extras.mesecons then
		local mesecons_rules = pipeworks.flowables.transitions.mesecons
		for _, element in ipairs(set) do
			mesecons_rules[element.nodename] = extras.mesecons
		end
	end
end