code:
-- mods/lagcheck/init.lua
-- =================
-- See README.md for licensing and other information.

--ABM

lagcheck_abm = {}
abm_count = 0
abm_last = {}
local old_register_abm = minetest.register_abm
function minetest.register_abm(spec)
	abm_count = abm_count + 1
	if abm_last.mod ~= minetest.get_current_modname() then
		abm_last.mod = minetest.get_current_modname()
		abm_last.count = 1
	else
		abm_last.count = abm_last.count + 1
	end
	lagcheck_abm[abm_count] = {mod = (minetest.get_current_modname() or "Unknown") .." (".. abm_last.count ..")", data = {}}
	spec.id = abm_count
	spec.action2 = spec.action
	spec.action = function(pos, node, active_object_count, active_object_count_wider)
		local time1 = minetest.get_us_time()
		spec.action2(pos, node, active_object_count, active_object_count_wider)
		table.insert(lagcheck_abm[spec.id].data, minetest.get_us_time() - time1 > 0 and minetest.get_us_time() - time1 or 0)
	end
	return old_register_abm(spec)
end

--Globalstep

lagcheck_globalstep = {}
globalstep_count = 0
globalstep_last = {}
local old_register_globalstep = minetest.register_globalstep
function minetest.register_globalstep(spec)
	globalstep_count = globalstep_count + 1
	if globalstep_last.mod ~= minetest.get_current_modname() then
		globalstep_last.mod = minetest.get_current_modname()
		globalstep_last.count = 1
	else
		globalstep_last.count = globalstep_last.count + 1
	end
	lagcheck_globalstep[globalstep_count] = {mod = (minetest.get_current_modname() or "Unknown") .." (".. globalstep_last.count ..")", data = {}}
	local spec_old = spec
	spec = function(dtime)
		local time1 = minetest.get_us_time()
		spec_old(dtime)
		table.insert(lagcheck_globalstep[globalstep_count].data, minetest.get_us_time() - time1 > 0 and minetest.get_us_time() - time1 or 0)
	end
	return old_register_globalstep(spec)
end

--Print out

local function sort_tablefunc(a)
	local aall = 0
	local amin = a[1] or 0
	local amax = a[1] or 0
	for key, value in ipairs(a.data) do
		aall = aall + value
		if value > amax then
			amax = value
		end
		if value < amin then
			amin = value
		end
	end
	return {min = amin, max = amax, all = #a.data == 0 and -1 or aall / #a.data}
end

local function sort_table(a, b)
	a = sort_tablefunc(a).all
	b = sort_tablefunc(b).all
	if a > b then
		return true
	end
	return false
end

local formatted = string.format("%%-%ds | %%%ds | %%%ds | %%%ds", 25, 9, 9, 9)

minetest.register_chatcommand("lagcheck", {
	description = "Writes lagcheck",
	privs = {privs = true},
	func = function(name, param)
		local file = io.open(minetest.get_worldpath() .."/LagCheck.txt", "w")

		--ABM
		local str = "ABM:\n\n"
		str = str .. string.format(formatted, "Modname", "Average", "Min", "Max") .."\n"
		table.sort(lagcheck_abm, function(a, b) return sort_table(a, b) end)
		for key, value in ipairs(lagcheck_abm) do
			local data = sort_tablefunc(value)
			str = str .. string.format(formatted, value.mod, tostring(math.floor(data.all)), tostring(math.floor(data.min)), tostring(math.floor(data.max))) .."\n"
		end

		--Globalstep
		str = str .."\n\nGlobalstep:\n\n"
		str = str .. string.format(formatted, "Modname", "Average", "Min", "Max") .."\n"
		table.sort(lagcheck_globalstep, function(a, b) return sort_table(a, b) end)
		for key, value in ipairs(lagcheck_globalstep) do
			local data = sort_tablefunc(value)
			str = str .. string.format(formatted, value.mod, tostring(math.floor(data.all)), tostring(math.floor(data.min)), tostring(math.floor(data.max))) .."\n"
		end

		file:write(str)
		file:close()
		minetest.chat_send_player(name, "Lagcheck: File saved to world folder")
end})


code:
name = lagcheck
description = Minetest mod adding system to log the consumption of every mod

repo_name: Desour/PlayMinetestServer

license: mit


code:
name: integration-test

on: [push, pull_request]

jobs:
  build:

    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
    - uses: actions/checkout@master

    - name: integration-test
      run: ./test/integration-test.sh


code:
name: luacheck

on: [push]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1
    - name: apt
      run: sudo apt-get install -y luarocks
    - name: luacheck install
      run: luarocks install --local luacheck
    - name: luacheck run
      run: $HOME/.luarocks/bin/luacheck ./


code:
allow_defined_top = true

globals = {
	"mail",
}

read_globals = {
	-- Stdlib
	string = {fields = {"split"}},
	table = {fields = {"copy", "getn"}},

	-- Minetest
	"minetest",
	"vector", "ItemStack",
	"dump",

	-- Deps
	"unified_inventory", "default",

	-- optional mods
	"xban"
}


code:
Mail mod for Minetest (ingame mod)
======

![](https://github.com/minetest-mail/mail_mod/workflows/integration-test/badge.svg)
![](https://github.com/minetest-mail/mail_mod/workflows/luacheck/badge.svg)

This is a fork of cheapies mail mod

It adds a mail-system that allows players to send each other messages in-game and via webmail (optional)

# Screenshots

Ingame mail
![](pics/ingame.png?raw=true)

# Installation

## In-game mail mod

Install it like any other mod: copy the directory `mail_mod` to your "worldmods" folder

## Webmail

See: https://github.com/minetest-mail/mail

# Commands/Howto

To access your mail click on the inventory mail button or use the "/mail" command
Mails can be deleted, marked as read or unread, replied to and forwarded to another player

# Dependencies
* None

# License

See the "LICENSE" file

# Textures
* textures/email_mail.png (https://github.com/rubenwardy/email.git WTFPL)

# Contributors

* Cheapie (initial idea/project)
* Rubenwardy (lua/ui improvements)

# Old/Historic stuff
* Old forum topic: https://forum.minetest.net/viewtopic.php?t=14464
* Old mod: https://cheapiesystems.com/git/mail/


code:
-- see: mail.md

mail.registered_on_receives = {}
function mail.register_on_receive(func)
	mail.registered_on_receives[#mail.registered_on_receives + 1] = func
end

mail.receive_mail_message = "You have a new message from %s! Subject: %s\nTo view it, type /mail"
mail.read_later_message = "You can read your messages later by using the /mail command"

--[[
mail sending function, can be invoked with one object argument (new api) or
all 4 parameters (old compat version)
see: "Mail format" api.md
--]]
function mail.send(src, dst, subject, body)
	local m
	if dst == nil and subject == nil and body == nil then
		-- new format (one object param)
		m = src
	else
		-- old format
		m = {}
		m.src = src
		m.dst = dst
		m.subject = subject
		m.body = body
	end

	minetest.log("action", "[mail] '" .. m.src .. "' sends mail to '" .. m.dst ..
		"' with subject '" .. m.subject .. "' and body: '" .. m.body .. "'")

	local messages = mail.getMessages(m.dst)

	table.insert(messages, 1, {
		unread  = true,
		sender  = m.src,
		subject = m.subject,
		body    = m.body,
		time    = os.time(),
	})
	mail.setMessages(m.dst, messages)

	for _, player in ipairs(minetest.get_connected_players()) do
		local name = player:get_player_name()
		if name == m.dst then
			if m.subject == "" then m.subject = "(No subject)" end
			if string.len(m.subject) > 30 then
				m.subject = string.sub(m.subject,1,27) .. "..."
			end
			minetest.chat_send_player(m.dst,
					string.format(mail.receive_mail_message, m.src, m.subject))
		end
	end

	for i=1, #mail.registered_on_receives do
		if mail.registered_on_receives[i](m) then
			break
		end
	end
end


code:
# Mail format
The mail format in the api hooks

```lua
mail = {
	src = "source name",
	dst = "destination name",
	subject = "subject line",
	body = "mail body",
	-- 8 attachments max
	attachments = {"default:stone 99", "default:gold_ingot 99"}
}
```

## Sending mail
Old variant (pre-1.1)
```lua
mail.send("source name", "destination name", "subject line", "mail body")
```

New variant (1.1+)
```lua
mail.send({
	src = "source name",
	dst = "destination name",
	subject = "subject line",
	body = "mail body"
})
```

# Hooks
On-receive mail hook:

```lua
mail.register_on_receive(function(m)
	-- "m" is an object in the form: "Mail format"
end)
```

# internal mail format (on-disk)
The mail format on-disk

> (worldfolder)/mails/(playername).json

```json
[{
	"unread": true,
	"sender": "sender name",
	"subject": "subject name",
	"body": "main\nmultiline\nbody",
	"time": 1551258349,
	"attachments": [
		"default:stone 99",
		"default:gold_ingot 99"
	]
}]

```


code:
local invmap = {}

mail.getAttachmentInventory = function(playername)
	return invmap[playername]
end

mail.getAttachmentInventoryName = function(playername)
	return "mail:" .. playername
end

minetest.register_on_joinplayer(function(player)
	local name = player:get_player_name()
	local inv = minetest.create_detached_inventory(mail.getAttachmentInventoryName(name), {})

	invmap[name] = inv
end)

minetest.register_on_leaveplayer(function(player)
	local name = player:get_player_name()
	invmap[name] = nil
	if minetest.remove_detached_inventory then
		minetest.remove_detached_inventory(mail.getAttachmentInventoryName(name))
	end
end)


code:
minetest.register_chatcommand("mail",{
	description = "Open the mail interface",
	func = function(name)
		mail.show_inbox(name)
	end
})


code:
selected_message_idxs = {}

local theme
if minetest.get_modpath("default") then
	theme = default.gui_bg .. default.gui_bg_img
else
	theme = ""
end

mail.inbox_formspec = "size[8,9;]" .. theme .. [[
		button_exit[7.5,0;0.5,0.5;quit;X]
		button[6,1;2,0.5;new;New Message]
		button[6,2;2,0.5;read;Read]
		button[6,3;2,0.5;reply;Reply]
		button[6,4;2,0.5;forward;Forward]
		button[6,5;2,0.5;delete;Delete]
		button[6,6;2,0.5;markread;Mark Read]
		button[6,7;2,0.5;markunread;Mark Unread]
		button[6,8;2,0.5;about;About]
		tablecolumns[color;text;text]
		table[0,0;5.75,9;messages;#999,From,Subject]]

function mail.show_about(name)
	local formspec = [[
			size[8,5;]
			button[7.5,0;0.5,0.5;back;X]
			label[0,0;Mail]
			label[0,0.5;By cheapie]
			label[0,1;http://github.com/cheapie/mail]
			label[0,1.5;See LICENSE file for license information]
			label[0,2.5;NOTE: Communication using this system]
			label[0,3;is NOT guaranteed to be private!]
			label[0,3.5;Admins are able to view the messages]
			label[0,4;of any player.]
		]] .. theme

	minetest.show_formspec(name, "mail:about", formspec)
end

function mail.show_inbox(name)
	local formspec = { mail.inbox_formspec }
	local messages = mail.getMessages(name)

	if messages[1] then
		for _, message in ipairs(messages) do
			if message.unread then
				formspec[#formspec + 1] = ",#FFD700"
			else
				formspec[#formspec + 1] = ","
			end
			formspec[#formspec + 1] = ","
			formspec[#formspec + 1] = minetest.formspec_escape(message.sender)
			formspec[#formspec + 1] = ","
			if message.subject ~= "" then
				if string.len(message.subject) > 30 then
					formspec[#formspec + 1] =
							minetest.formspec_escape(string.sub(message.subject, 1, 27))
					formspec[#formspec + 1] = "..."
				else
					formspec[#formspec + 1] = minetest.formspec_escape(message.subject)
				end
			else
				formspec[#formspec + 1] = "(No subject)"
			end
		end
		if selected_message_idxs[name] then
			formspec[#formspec + 1] = ";"
			formspec[#formspec + 1] = tostring(selected_message_idxs[name] + 1)
		end
		formspec[#formspec + 1] = "]"
	else
		formspec[#formspec + 1] = "]label[2,4.5;No mail]"
	end
	minetest.show_formspec(name, "mail:inbox", table.concat(formspec, ""))
end

function mail.show_message(name, msgnumber)
	local messages = mail.getMessages(name)
	local message = messages[msgnumber]
	local formspec = [[
			size[8,7.2]
			button[7,0;1,0.5;back;X]
			label[0,0;From: %s]
			label[0,0.5;Subject: %s]
			textarea[0.25,1.25;8,6.25;body;;%s]
			button[1,6.7;2,1;reply;Reply]
			button[3,6.7;2,1;forward;Forward]
			button[5,6.7;2,1;delete;Delete]
		]] .. theme

	local sender = minetest.formspec_escape(message.sender)
	local subject = minetest.formspec_escape(message.subject)
	local body = minetest.formspec_escape(message.body)
	formspec = string.format(formspec, sender, subject, body)

	minetest.show_formspec(name,"mail:message",formspec)
end

function mail.show_compose(name, defaulttgt, defaultsubj, defaultbody)
	local formspec = [[
			size[8,7.2]
			field[0.25,0.5;4,1;to;To:;%s]
			field[0.25,1.7;8,1;subject;Subject:;%s]
			textarea[0.25,2.4;8,5;body;;%s]
			button[0.5,6.7;3,1;cancel;Cancel]
			button[7,0;1,0.5;cancel;X]
			button[4.5,6.7;3,1;send;Send]
		]] .. theme

	formspec = string.format(formspec,
		minetest.formspec_escape(defaulttgt),
		minetest.formspec_escape(defaultsubj),
		minetest.formspec_escape(defaultbody))

	minetest.show_formspec(name, "mail:compose", formspec)
end

function mail.handle_receivefields(player, formname, fields)
	if formname == "" and fields and fields.quit and minetest.get_modpath("unified_inventory") then
		unified_inventory.set_inventory_formspec(player, "craft")
	end

	if formname == "mail:about" then
		minetest.after(0.5, function()
			mail.show_inbox(player:get_player_name())
		end)

	elseif formname == "mail:inbox" then
		local name = player:get_player_name()
		local messages = mail.getMessages(name)

		if fields.messages then
			local evt = minetest.explode_table_event(fields.messages)
			selected_message_idxs[name] = evt.row - 1
			if evt.type == "DCL" and messages[selected_message_idxs[name]] then
				messages[selected_message_idxs[name]].unread = false
				mail.show_message(name, selected_message_idxs[name])
			end
			mail.setMessages(name, messages)
			return true
		end
		if fields.read then
			if messages[selected_message_idxs[name]] then
				messages[selected_message_idxs[name]].unread = false
				mail.show_message(name, selected_message_idxs[name])
			end

		elseif fields.delete then
			if messages[selected_message_idxs[name]] then
				table.remove(messages, selected_message_idxs[name])
			end

			mail.show_inbox(name)
		elseif fields.reply and messages[selected_message_idxs[name]] then
			local message = messages[selected_message_idxs[name]]
			local replyfooter = "Type your reply here.\n\n--Original message follows--\n" ..message.body
			mail.show_compose(name, message.sender, "Re: "..message.subject,replyfooter)

		elseif fields.forward and messages[selected_message_idxs[name]] then
			local message = messages[selected_message_idxs[name]]
			local fwfooter = "Type your message here.\n\n--Original message follows--\n" ..message.body
			mail.show_compose(name, "", "Fw: "..message.subject, fwfooter)

		elseif fields.markread then
			if messages[selected_message_idxs[name]] then
				messages[selected_message_idxs[name]].unread = false
			end
			mail.show_inbox(name)

		elseif fields.markunread then
			if messages[selected_message_idxs[name]] then
				messages[selected_message_idxs[name]].unread = true
			end
			mail.show_inbox(name)

		elseif fields.new then
			mail.show_compose(name,"","","Type your message here.")

		elseif fields.quit then
			if minetest.get_modpath("unified_inventory") then
				unified_inventory.set_inventory_formspec(player, "craft")
			end

		elseif fields.about then
			mail.show_about(name)

		end

		mail.setMessages(name, messages)
		return true
	elseif formname == "mail:message" then
		local name = player:get_player_name()
		local messages = mail.getMessages(name)

		if fields.back then
			mail.show_inbox(name)
		elseif fields.reply then
			local message = messages[selected_message_idxs[name]]
			local replyfooter = "Type your reply here.\n\n--Original message follows--\n" ..message.body
			mail.show_compose(name, message.sender, "Re: "..message.subject, replyfooter)
		elseif fields.forward then
			local message = messages[selected_message_idxs[name]]
			local fwfooter = "Type your message here.\n\n--Original message follows--\n" ..message.body
			mail.show_compose(name, "", "Fw: "..message.subject, fwfooter)
		elseif fields.delete then
			if messages[selected_message_idxs[name]] then
				table.remove(messages,selected_message_idxs[name])
			end
			mail.show_inbox(name)
		end

		mail.setMessages(name, messages)
		return true
	elseif formname == "mail:compose" then
		if fields.send then
			mail.send({
				src = player:get_player_name(),
				dst = fields.to,
				subject = fields.subject,
				body = fields.body
			})
		end
		minetest.after(0.5, function()
			mail.show_inbox(player:get_player_name())
		end)
		return true

	elseif fields.mail then
		mail.show_inbox(player:get_player_name())
	else
		return false
	end
end

minetest.register_on_player_receive_fields(mail.handle_receivefields)

if minetest.get_modpath("unified_inventory") then
	mail.receive_mail_message = mail.receive_mail_message ..
		" or use the mail button in the inventory"
	mail.read_later_message = mail.read_later_message ..
		" or by using the mail button in the inventory"

	unified_inventory.register_button("mail", {
			type = "image",
			image = "mail_button.png",
			tooltip = "Mail"
		})
end


code:
local huddata = {}

minetest.register_on_joinplayer(function(player)
	local name = player:get_player_name()
	local data = {}

	data.imageid = player:hud_add({
		hud_elem_type = "image",
		name = "MailIcon",
		position = {x=0.52, y=0.52},
		text="",
		scale = {x=1,y=1},
		alignment = {x=0.5, y=0.5},
	})

	data.textid = player:hud_add({
		hud_elem_type = "text",
		name = "MailText",
		position = {x=0.55, y=0.52},
		text= "",
		scale = {x=1,y=1},
		alignment = {x=0.5, y=0.5},
	})

	huddata[name] = data
end)

minetest.register_on_leaveplayer(function(player)
	local name = player:get_player_name()
	huddata[name] = nil
end)

mail.hud_update = function(playername, messages)
	local data = huddata[playername]
	local player = minetest.get_player_by_name(playername)

	if not data or not player then
		return
	end

	local unreadcount = 0
	for _, message in ipairs(messages) do
		if message.unread then
			unreadcount = unreadcount + 1
		end
	end

	if unreadcount == 0 then
		player:hud_change(data.imageid, "text", "")
		player:hud_change(data.textid, "text", "")
	else
		player:hud_change(data.imageid, "text", "email_mail.png")
		player:hud_change(data.textid, "text", unreadcount .. " /mail")
	end

end


code:
mail = {

	-- mark webmail fork for other mods
	fork = "webmail",

	-- api version
	apiversion = 1.1,

	-- mail directory
	maildir = minetest.get_worldpath().."/mails",

	-- allow item/node attachments
	allow_attachments = minetest.settings:get("mail.allow_attachments") == "true",

	webmail = {
		-- disallow banned players in the webmail interface
		disallow_banned_players = minetest.settings:get("webmail.disallow_banned_players") == "true",

		-- url and key to the webmail server
		url = minetest.settings:get("webmail.url"),
		key = minetest.settings:get("webmail.key")
	},

	tan = {}
}

local MP = minetest.get_modpath(minetest.get_current_modname())
dofile(MP .. "/chatcommands.lua")
dofile(MP .. "/migrate.lua")
dofile(MP .. "/attachment.lua")
dofile(MP .. "/hud.lua")
dofile(MP .. "/storage.lua")
dofile(MP .. "/api.lua")
dofile(MP .. "/gui.lua")
dofile(MP .. "/onjoin.lua")

-- optional webmail stuff below
local http = minetest.request_http_api()

if http then
	local webmail_url = mail.webmail.url
	local webmail_key = mail.webmail.key

	if not webmail_url then error("webmail.url is not defined") end
	if not webmail_key then error("webmail.key is not defined") end

	print("[mail] loading webmail-component with endpoint: " .. webmail_url)

	mail.handlers = {}
	dofile(MP .. "/webmail/tan.lua")
	dofile(MP .. "/webmail/webmail.lua")
	dofile(MP .. "/webmail/hook.lua")
	dofile(MP .. "/webmail/handler_auth.lua")
	dofile(MP .. "/webmail/handler_send.lua")
	dofile(MP .. "/webmail/handler_messages.lua")
	dofile(MP .. "/webmail/handler_delete.lua")
	dofile(MP .. "/webmail/handler_mark_read.lua")
	dofile(MP .. "/webmail/handler_mark_unread.lua")
	mail.webmail_init(http, webmail_url, webmail_key)
end

-- migrate storage
mail.migrate()


code:
-- migrate from mail.db to player-file-based mailbox

mail.migrate = function()
	-- create directory, just in case
	minetest.mkdir(mail.maildir)

	local file = io.open(minetest.get_worldpath().."/mail.db", "r")
	if file then
		print("[mail] migrating to new per-player storage")

		local data = file:read("*a")
		local oldmails = minetest.deserialize(data)
		file:close()

		for name, oldmessages in pairs(oldmails) do
			mail.setMessages(name, oldmessages)
		end

		-- rename file
		print("[mail] migration done, renaming old mail.db")
		os.rename(minetest.get_worldpath().."/mail.db", minetest.get_worldpath().."/mail.db.old")
	end

end


code:
name = mail
description = mail mod
optional_depends = unified_inventory,default,xban2


code:
minetest.register_on_joinplayer(function(player)
	minetest.after(2, function(name)
		local messages = mail.getMessages(name)

		local unreadcount = 0

		for _, message in pairs(messages) do
			if message.unread then
				unreadcount = unreadcount + 1
			end
		end

		if unreadcount > 0 then
			minetest.chat_send_player(name,
				"(" ..  unreadcount .. ") You have mail! Type /mail to read")

		end
	end, player:get_player_name())
end)


code:
-- TODO: maybe local cache?

function getMailFile(playername)
	local saneplayername = string.gsub(playername, "[.|/]", "")
	return mail.maildir .. "/" .. saneplayername .. ".json"
end

mail.getMessages = function(playername)
	local file = io.open(getMailFile(playername), "r")
	local messages = {}
	if file then
		local json = file:read("*a")
		messages = minetest.parse_json(json or "[]") or {}
		mail.hud_update(playername, messages)
		file:close()
	end

	return messages
end

mail.setMessages = function(playername, messages)
	local file = io.open(getMailFile(playername),"w")
	local json = minetest.write_json(messages)
	if file and file:write(json) and file:close() then
		mail.hud_update(playername, messages)
		return true
	else
		minetest.log("error","[mail] Save failed - messages may be lost!")
		return false
	end
end


code:
User,Password
-----------------
test,enter
test2,enter


code:
#!/bin/bash

MINETEST_VERSION=5.2.0

# prerequisites
jq --version || exit 1
curl --version || exit 1

# ensure proper current directory
CWD=$(dirname $0)
cd ${CWD}

# setup
unset use_proxy
unset http_proxy
unset https_proxy
unset HTTP_PROXY
unset HTTPS_PROXY

# run mail-server
docker pull minetestmail/mail
docker run --name mail --rm \
 -e WEBMAILKEY=myserverkey \
 -e WEBMAIL_DEBUG=true \
 --network host \
 minetestmail/mail &

# wait for startup
bash -c 'while !</dev/tcp/localhost/8080; do sleep 1; done;'

# start minetest with mail mod
docker pull registry.gitlab.com/minetest/minetest/server:${MINETEST_VERSION}
docker run --rm --name minetest \
  -u root:root \
	-v $(pwd)/minetest.conf:/etc/minetest/minetest.conf:ro \
  -v $(pwd)/world.mt:/root/.minetest/worlds/world/world.mt \
  -v $(pwd)/auth.sqlite:/root/.minetest/worlds/world/auth.sqlite \
  -v $(pwd)/../:/root/.minetest/worlds/world/worldmods/mail \
  -v $(pwd)/test_mod:/root/.minetest/worlds/world/worldmods/mail_test \
  -e use_proxy=false \
  -e http_proxy= \
  -e HTTP_PROXY= \
  --network host \
	registry.gitlab.com/minetest/minetest/server:${MINETEST_VERSION} &

# prepare cleanup
function cleanup {
	# cleanup
	docker stop mail
  docker stop minetest
}

trap cleanup EXIT

# wait for startup
sleep 5

# Execute calls against mail-server

# login
LOGIN_DATA='{"username":"test","password":"enter"}'
RES=$(curl --data "${LOGIN_DATA}" -H "Content-Type: application/json" "http://127.0.0.1:8080/api/login")
echo Login response: $RES
SUCCESS=$(echo $RES | jq -r .success)
TOKEN=$(echo $RES | jq -r .token)

# login succeeded
test "$SUCCESS" == "true" || exit 1
# token extracted
test -n "$TOKEN" || exit 1

# fetch mails
RES=$(curl -H "Authorization: ${TOKEN}" "http://127.0.0.1:8080/api/inbox")
echo Mailbox: ${RES}

# inbox count is 1
test "$(echo $RES | jq '. | length')" == "1" || exit 1

echo "Test complete!"


code:
secure.http_mods = mail
webmail.url = http://localhost:8080
webmail.key = myserverkey
webmail.disallow_banned_players = true


code:
#!/bin/sh

docker run --rm -it \
	-u root:root \
	-v $(pwd)/minetest.conf:/etc/minetest/minetest.conf \
	-v $(pwd)/auth.sqlite:/root/.minetest/worlds/world/auth.sqlite \
	-v $(pwd)/../:/root/.minetest/worlds/world/worldmods/mail_mod \
	-v mail_world:/root/.minetest/worlds/world/ \
	--network host \
	registry.gitlab.com/minetest/minetest/server:5.2.0


code:
minetest.log("warning", "[TEST] integration-test enabled!")

minetest.register_on_mods_loaded(function()
	minetest.log("warning", "[TEST] starting tests")
	mail.send("spammer", "test", "subject", "body");
end)


code:
name = mail_test
depends = mail


code:
enable_damage = true
creative_mode = false
gameid = minetest
auth_backend = sqlite3
backend = sqlite3
player_backend = sqlite3


code:
-- bi-directional http-channel
-- with long-poll GET and POST on the same URL

local function Channel(http, url, cfg)
	cfg = cfg or {}
	local extra_headers = cfg.extra_headers or {}
	local timeout = cfg.timeout or 1
	local long_poll_timeout = cfg.long_poll_timeout or 30
	local error_retry = cfg.error_retry or 10

	-- assemble post-header with json content
	local post_headers = { "Content-Type: application/json" }
	for _,header in pairs(cfg.extra_headers) do
		table.insert(post_headers, header)
	end

	local recv_listeners = {}
	local run = true

	local recv_loop

	recv_loop = function()
		assert(run)

		-- long-poll GET
		http.fetch({
			url = url,
			extra_headers = extra_headers,
			timeout = long_poll_timeout
		}, function(res)
			if res.succeeded and res.code == 200 then
				local data = minetest.parse_json(res.data)

				if data then
					for _,listener in pairs(recv_listeners) do
						if #data > 0 then
							-- array received
							for _, entry in ipairs(data) do
								listener(entry)
							end
						else
							-- single item received
							listener(data)
						end
					end
				end
				-- reschedule immediately
				minetest.after(0, recv_loop)
			else
				-- error, retry after some time
				minetest.after(error_retry, recv_loop)
			end
		end)
	end

	local send = function(data)
		assert(run)
		-- POST

		http.fetch({
			url = url,
			extra_headers = post_headers,
			timeout = timeout,
			post_data = minetest.write_json(data)
		}, function()
			-- TODO: error-handling
		end)
	end

	local receive = function(listener)
		table.insert(recv_listeners, listener)
	end

	local close = function()
		run = false
	end

	recv_loop();

	return {
		send = send,
		receive = receive,
		close = close
	}

end

return Channel


code:
local has_xban2_mod = minetest.get_modpath("xban2")

-- auth request from webmail
function mail.handlers.auth(auth)
	local handler = minetest.get_auth_handler()
	minetest.log("action", "[webmail] auth: " .. auth.name)

	local success = false
	local banned = false
	local message = ""

	if mail.webmail.disallow_banned_players and has_xban2_mod then
		-- check xban db
		local xbanentry = xban.find_entry(auth.name)
		if xbanentry and xbanentry.banned then
			banned = true
			message = "Banned!"
		end
	end

	if not banned then
		-- check tan
		local tan = mail.tan[auth.name]
		if tan ~= nil then
			success = tan == auth.password
		end

		-- check auth
		if not success then
			local entry = handler.get_auth(auth.name)
			if entry and minetest.check_password_entry(auth.name, entry.password, auth.password) then
				success = true
			end
		end
	end

	mail.channel.send({
		type = "auth",
		data = {
			name = auth.name,
			success = success,
			message = message
		}
	})
end


code:
-- remove mail
function mail.handlers.delete(playername, index)
	local messages = mail.getMessages(playername)
	if messages[index] then
		table.remove(messages, index)
	end
	mail.setMessages(playername, messages)
end


code:
-- mark mail as read
function mail.handlers.mark_read(playername, index)
	local messages = mail.getMessages(playername)
	if messages[index] then
		messages[index].unread = false
	end
	mail.setMessages(playername, messages)
end


code:
-- mark mail as unread
function mail.handlers.mark_unread(playername, index)
	local messages = mail.getMessages(playername)
	if messages[index] then
		messages[index].unread = true
	end
	mail.setMessages(playername, messages)
end


code:
-- get player messages request from webmail
function mail.handlers.messages(playername)
	local messages = mail.getMessages(playername)
	mail.channel.send({
		type = "player-messages",
		playername = playername,
		data = messages
	})
end


code:
-- send request from webmail
function mail.handlers.send(sendmail)
	-- send mail from webclient
	minetest.log("action", "[webmail] sending mail from webclient: " .. sendmail.src .. " -> " .. sendmail.dst)
	mail.send(sendmail)
end


code:
function mail.webmail_send_hook(m)
	mail.channel.send({
		type = "new-message",
		data = m
	})
end

mail.register_on_receive(mail.webmail_send_hook)


code:
minetest.register_chatcommand("webmail_tan", {
	description = "generates a tan (temporary access number) for the webmail access",
	func = function(name)
    local tan = "" .. math.random(1000, 9999)
    mail.tan[name] = tan

		return true, "Your tan is " .. tan .. ", it will expire upon leaving the game"
	end
})

minetest.register_on_leaveplayer(function(player)
	local name = player:get_player_name()
	mail.tan[name] = nil
end)


code:
local MP = minetest.get_modpath(minetest.get_current_modname())
local Channel = dofile(MP .. "/util/channel.lua")

function mail.webmail_init(http, url, key)
	mail.channel = Channel(http, url .. "/api/minetest/channel", {
		extra_headers = { "webmailkey: " .. key }
	})

	mail.channel.receive(function(data)
		if data.type == "auth" then
			mail.handlers.auth(data.data)

		elseif data.type == "send" then
			mail.handlers.send(data.data) -- { src, dst, subject, body }

		elseif data.type == "delete-mail" then
			mail.handlers.delete(data.playername, data.index) -- index 1-based

		elseif data.type == "mark-mail-read" then
			mail.handlers.mark_read(data.playername, data.index) -- index 1-based

		elseif data.type == "mark-mail-unread" then
			mail.handlers.mark_unread(data.playername, data.index) -- index 1-based

		elseif data.type == "player-messages" then
			mail.handlers.messages(data.data)

		end
	end)
end


code:
*~
*.patch
*.diff


code:
    ########################################################################
    ##  __    __   _____   _____   _____   _____   _____   _   _   _____  ##
    ## |  \  /  | |  ___| |  ___| |  ___| |  ___| |  _  | | \ | | |  ___| ##
    ## |   \/   | | |___  | |___  | |___  | |     | | | | |  \| | | |___  ##
    ## | |\__/| | |  ___| |___  | |  ___| | |     | | | | |     | |___  | ##
    ## | |    | | | |___   ___| | | |___  | |___  | |_| | | |\  |  ___| | ##
    ## |_|    |_| |_____| |_____| |_____| |_____| |_____| |_| \_| |_____| ##
    ##                                                                    ##
    ########################################################################

MESECONS by Jeija and contributors

Mezzee-what?
------------
[Mesecons](http://mesecons.net/)! They're yellow, they're conductive, and they'll add a whole new dimension to Minetest's gameplay.

Mesecons is a mod for [Minetest](http://minetest.net/) that implements a ton of items related to digital circuitry, such as wires, buttons, lights, and even programmable controllers. Among other things, there are also pistons, solar panels, pressure plates, and note blocks.

Mesecons has a similar goal to Redstone in Minecraft, but works in its own way, with different rules and mechanics.

OK, I want in.
--------------
Go get it!

[DOWNLOAD IT NOW](https://github.com/minetest-mods/mesecons/archive/master.zip)

Now go ahead and install it like any other Minetest mod. Don't know how? Check out [the wonderful page about it](https://wiki.minetest.net/Mods) over at the official Minetest Wiki. For your convenience, here's a quick summary:

1. If Mesecons is still in a ZIP file, extract the folder inside to somewhere on the computer.
2. Make sure that when you open the folder, you can directly find `README.md` in the listing. If you just see another folder, move that folder up one level and delete the old one.
3. Open up the Minetest mods folder - usually `/mods/`. If you see the `minetest` or folder inside of that, that is your mod folder instead.
4. Copy the Mesecons folder into the mods folder.

Don't like some parts of Mesecons? Open up the Mesecons folder and delete the subfolder containing the mod you don't want. If you didn't want movestones, for example, all you have to do is delete the `mesecons_movestones` folder and they will no longer be available.

There are no dependencies - it will work right after installing!

How do I use this thing?
------------------------
How about a [quick overview video](https://www.youtube.com/watch?v=6kmeQj6iW5k)?

Or maybe a [comprehensive reference](http://mesecons.net/items.html) is your style?

An overview for the very newest of new beginners? How does [this one](http://uberi.mesecons.net/projects/MeseconsBasics/index.html) look?

There is also a [wiki page](https://wiki.minetest.net/Mods/Mesecons) dedicated to this mod.

Want to get more into building? Why not check out the [Mesecons Laboratory](http://uberi.mesecons.net/), a website dedicated to advanced Mesecons builders?

Want to contribute to Mesecons itself? Check out the [source code](https://github.com/minetest-mods/mesecons)!

Who wrote it anyways?
---------------------
These awesome people made Mesecons possible!

| Contributor     | Contribution                     |
| --------------- | -------------------------------- |
| Hawk777         | Code for VoxelManip caching      |
| Jat15           | Various tweaks.                  |
| Jeija           | **Main developer! Everything.**  |
| Jordach         | Noteblock sounds.                |
| khonkhortistan  | Code, recipes, textures.         |
| Kotolegokot     | Nodeboxes for items.             |
| minerd247       | Textures.                        |
| Nore/Novatux    | Code.                            |
| RealBadAngel    | Fixes, improvements.             |
| sfan5           | Code, recipes, textures.         |
| suzenako        | Piston sounds.                   |
| Uberi/Temperest | Code, textures, documentation.   |
| VanessaE        | Code, recipes, textures, design. |
| Whiskers75      | Logic gates implementation.      |

There are also a whole bunch of other people helping with everything from code to testing and feedback. Mesecons would also not be possible without their help!

Alright, how can I use it?
--------------------------
All textures in this project are licensed under the CC-BY-SA 3.0 (Creative Commons Attribution-ShareAlike 3.0 Generic). That means you can distribute and remix them as much as you want to, under the condition that you give credit to the authors and the project, and that if you remix and release them, they must be under the same or similar license to this one.

All code in this project is licensed under the LGPL version 3 or later. That means you have unlimited freedom to distribute and modify the work however you see fit, provided that if you decide to distribute it or any modified versions of it, you must also use the same license. The LGPL also grants the additional freedom to write extensions for the software and distribute them without the extensions being subject to the terms of the LGPL, although the software itself retains its license.

No warranty is provided, express or implied, for any part of the project.


mesecons

description: Mesecons is a mod for Minetest that implements items related to digital circuitry: wires, buttons, lights, and programmable controllers.

authors: Jeija

keywords: mesecons minetest mod


code:
--[[
Mesecons uses something it calls an ActionQueue.

The ActionQueue holds functions and actions.
Functions are added on load time with a specified name.
Actions are preserved over server restarts.

Each action consists of a position, the name of an added function to be called,
the params that should be used in this function call (additionally to the pos),
the time after which it should be executed, an optional overwritecheck and a
priority.

If time = 0, the action will be executed in the next globalstep, otherwise the
earliest globalstep when it will be executed is the after next globalstep.

It is guaranteed, that for two actions ac1, ac2 where ac1 ~= ac2,
ac1.time == ac2.time, ac1.priority == ac2.priority and ac1 was added earlier
than ac2, ac1 will be executed before ac2 (but in the same globalstep).

Note: Do not pass references in params, as they can not be preserved.

Also note: Some of the guarantees here might be dropped at some time.
]]

-- localize for speed
local queue = mesecon.queue

queue.actions = {} -- contains all ActionQueue actions

function queue:add_function(name, func)
	queue.funcs[name] = func
end

-- If add_action with twice the same overwritecheck and same position are called, the first one is overwritten
-- use overwritecheck nil to never overwrite, but just add the event to the queue
-- priority specifies the order actions are executed within one globalstep, highest first
-- should be between 0 and 1
function queue:add_action(pos, func, params, time, overwritecheck, priority)
	-- Create Action Table:
	time = time or 0 -- time <= 0 --> execute, time > 0 --> wait time until execution
	priority = priority or 1
	local action = {
		pos = mesecon.tablecopy(pos),
		func = func,
		params = mesecon.tablecopy(params or {}),
		time = time,
		owcheck = (overwritecheck and mesecon.tablecopy(overwritecheck)) or nil,
		priority = priority
	}

	 -- check if old action has to be overwritten / removed:
	if overwritecheck then
		for i, ac in ipairs(queue.actions) do
			if vector.equals(pos, ac.pos)
					and mesecon.cmpAny(overwritecheck, ac.owcheck) then
				-- remove the old action
				table.remove(queue.actions, i)
				break
			end
		end
	end

	table.insert(queue.actions, action)
end

-- execute the stored functions on a globalstep
-- if however, the pos of a function is not loaded (get_node_or_nil == nil), do NOT execute the function
-- this makes sure that resuming mesecons circuits when restarting minetest works fine (hm, where do we do this?)
-- However, even that does not work in some cases, that's why we delay the time the globalsteps
-- start to be execute by 4 seconds

local function globalstep_func(dtime)
	local actions = queue.actions
	-- split into two categories:
	-- actions_now: actions to execute now
	-- queue.actions: actions to execute later
	local actions_now = {}
	queue.actions = {}

	for _, ac in ipairs(actions) do
		if ac.time > 0 then
			-- action ac is to be executed later
			-- ~> insert into queue.actions
			ac.time = ac.time - dtime
			table.insert(queue.actions, ac)
		else
			-- action ac is to be executed now
			-- ~> insert into actions_now
			table.insert(actions_now, ac)
		end
	end

	-- stable-sort the executed actions after their priority
	-- some constructions might depend on the execution order, hence we first
	-- execute the actions that had a lower index in actions_now
	local old_action_order = {}
	for i, ac in ipairs(actions_now) do
		old_action_order[ac] = i
	end
	table.sort(actions_now, function(ac1, ac2)
		if ac1.priority ~= ac2.priority then
			return ac1.priority > ac2.priority
		else
			return old_action_order[ac1] < old_action_order[ac2]
		end
	end)

	-- execute highest priorities first, until all are executed
	for _, ac in ipairs(actions_now) do
		queue:execute(ac)
	end
end

-- delay the time the globalsteps start to be execute by 4 seconds
do
	local m_time = 0
	local resumetime = mesecon.setting("resumetime", 4)
	local globalstep_func_index = #minetest.registered_globalsteps + 1

	minetest.register_globalstep(function(dtime)
		m_time = m_time + dtime
		-- don't even try if server has not been running for XY seconds; resumetime = time to wait
		-- after starting the server before processing the ActionQueue, don't set this too low
		if m_time < resumetime then
			return
		end
		-- replace this globalstep function
		minetest.registered_globalsteps[globalstep_func_index] = globalstep_func
	end)
end

function queue:execute(action)
	-- ignore if action queue function name doesn't exist,
	-- (e.g. in case the action queue savegame was written by an old mesecons version)
	if queue.funcs[action.func] then
		queue.funcs[action.func](action.pos, unpack(action.params))
	end
end

-- Store and read the ActionQueue to / from a file
-- so that upcoming actions are remembered when the game
-- is restarted
queue.actions = mesecon.file2table("mesecon_actionqueue")

minetest.register_on_shutdown(function()
	mesecon.table2file("mesecon_actionqueue", queue.actions)
end)


code:
-- |\    /| ____ ____  ____ _____   ____         _____
-- | \  / | |    |     |    |      |    | |\   | |
-- |  \/  | |___ ____  |___ |      |    | | \  | |____
-- |      | |        | |    |      |    | |  \ |     |
-- |      | |___ ____| |___ |____  |____| |   \| ____|
-- by Jeija, Uberi (Temperest), sfan5, VanessaE, Hawk777 and contributors
--
--
--
-- This mod adds mesecons[=minecraft redstone] and different receptors/effectors to minetest.
-- See the documentation on the forum for additional information, especially about crafting
--
--
-- For basic development resources, see http://mesecons.net/developers.html
--
--
--
--Quick draft for the mesecons array in the node's definition
--mesecons =
--{
--	receptor =
--	{
--		state = mesecon.state.on/off
--		rules = rules/get_rules
--	},
--	effector =
--	{
--		action_on = function
--		action_off = function
--		action_change = function
--		rules = rules/get_rules
--	},
--	conductor =
--	{
--		state = mesecon.state.on/off
--		offstate = opposite state (for state = on only)
--		onstate = opposite state (for state = off only)
--		rules = rules/get_rules
--	}
--}

-- PUBLIC VARIABLES
mesecon={} -- contains all functions and all global variables
mesecon.queue={} -- contains the ActionQueue
mesecon.queue.funcs={} -- contains all ActionQueue functions

-- Settings
dofile(minetest.get_modpath("mesecons").."/settings.lua")

-- Utilities like comparing positions,
-- adding positions and rules,
-- mostly things that make the source look cleaner
dofile(minetest.get_modpath("mesecons").."/util.lua");

-- Presets (eg default rules)
dofile(minetest.get_modpath("mesecons").."/presets.lua");

-- The ActionQueue
-- Saves all the actions that have to be execute in the future
dofile(minetest.get_modpath("mesecons").."/actionqueue.lua");

-- Internal stuff
-- This is the most important file
-- it handles signal transmission and basically everything else
-- It is also responsible for managing the nodedef things,
-- like calling action_on/off/change
dofile(minetest.get_modpath("mesecons").."/internal.lua");

-- API
-- these are the only functions you need to remember

mesecon.queue:add_function("receptor_on", function (pos, rules)
	mesecon.vm_begin()

	rules = rules or mesecon.rules.default

	-- Call turnon on all linking positions
	for _, rule in ipairs(mesecon.flattenrules(rules)) do
		local np = vector.add(pos, rule)
		local rulenames = mesecon.rules_link_rule_all(pos, rule)
		for _, rulename in ipairs(rulenames) do
			mesecon.turnon(np, rulename)
		end
	end

	mesecon.vm_commit()
end)

function mesecon.receptor_on(pos, rules)
	mesecon.queue:add_action(pos, "receptor_on", {rules}, nil, rules)
end

mesecon.queue:add_function("receptor_off", function (pos, rules)
	rules = rules or mesecon.rules.default

	-- Call turnoff on all linking positions
	for _, rule in ipairs(mesecon.flattenrules(rules)) do
		local np = vector.add(pos, rule)
		local rulenames = mesecon.rules_link_rule_all(pos, rule)
		for _, rulename in ipairs(rulenames) do
			mesecon.vm_begin()
			mesecon.changesignal(np, minetest.get_node(np), rulename, mesecon.state.off, 2)

			-- Turnoff returns true if turnoff process was successful, no onstate receptor
			-- was found along the way. Commit changes that were made in voxelmanip. If turnoff
			-- returns true, an onstate receptor was found, abort voxelmanip transaction.
			if (mesecon.turnoff(np, rulename)) then
				mesecon.vm_commit()
			else
				mesecon.vm_abort()
			end
		end
	end
end)

function mesecon.receptor_off(pos, rules)
	mesecon.queue:add_action(pos, "receptor_off", {rules}, nil, rules)
end

print("[OK] Mesecons")

-- Deprecated stuff
-- To be removed in future releases
dofile(minetest.get_modpath("mesecons").."/legacy.lua");

--Services like turnoff receptor on dignode and so on
dofile(minetest.get_modpath("mesecons").."/services.lua");


code:
-- Internal.lua - The core of mesecons
--
-- For more practical developer resources see http://mesecons.net/developers.php
--
-- Function overview
-- mesecon.get_effector(nodename)	--> Returns the mesecons.effector -specifictation in the nodedef by the nodename
-- mesecon.get_receptor(nodename)	--> Returns the mesecons.receptor -specifictation in the nodedef by the nodename
-- mesecon.get_conductor(nodename)	--> Returns the mesecons.conductor-specifictation in the nodedef by the nodename
-- mesecon.get_any_inputrules (node)	--> Returns the rules of a node if it is a conductor or an effector
-- mesecon.get_any_outputrules (node)	--> Returns the rules of a node if it is a conductor or a receptor

-- RECEPTORS
-- mesecon.is_receptor(nodename)	--> Returns true if nodename is a receptor
-- mesecon.is_receptor_on(nodename	--> Returns true if nodename is an receptor with state = mesecon.state.on
-- mesecon.is_receptor_off(nodename)	--> Returns true if nodename is an receptor with state = mesecon.state.off
-- mesecon.receptor_get_rules(node)	--> Returns the rules of the receptor (mesecon.rules.default if none specified)

-- EFFECTORS
-- mesecon.is_effector(nodename)	--> Returns true if nodename is an effector
-- mesecon.is_effector_on(nodename)	--> Returns true if nodename is an effector with nodedef.mesecons.effector.action_off
-- mesecon.is_effector_off(nodename)	--> Returns true if nodename is an effector with nodedef.mesecons.effector.action_on
-- mesecon.effector_get_rules(node)	--> Returns the input rules of the effector (mesecon.rules.default if none specified)

-- SIGNALS
-- mesecon.activate(pos, node, depth)				--> Activates   the effector node at the specific pos (calls nodedef.mesecons.effector.action_on), higher depths are executed later
-- mesecon.deactivate(pos, node, depth)				--> Deactivates the effector node at the specific pos (calls nodedef.mesecons.effector.action_off), higher depths are executed later
-- mesecon.changesignal(pos, node, rulename, newstate, depth)	--> Changes     the effector node at the specific pos (calls nodedef.mesecons.effector.action_change), higher depths are executed later

-- CONDUCTORS
-- mesecon.is_conductor(nodename)	--> Returns true if nodename is a conductor
-- mesecon.is_conductor_on(node		--> Returns true if node is a conductor with state = mesecon.state.on
-- mesecon.is_conductor_off(node)	--> Returns true if node is a conductor with state = mesecon.state.off
-- mesecon.get_conductor_on(node_off)	--> Returns the onstate  nodename of the conductor
-- mesecon.get_conductor_off(node_on)	--> Returns the offstate nodename of the conductor
-- mesecon.conductor_get_rules(node)	--> Returns the input+output rules of a conductor (mesecon.rules.default if none specified)

-- HIGH-LEVEL Internals
-- mesecon.is_power_on(pos)				--> Returns true if pos emits power in any way
-- mesecon.is_power_off(pos)				--> Returns true if pos does not emit power in any way
-- mesecon.is_powered(pos)				--> Returns true if pos is powered by a receptor or a conductor

-- RULES ROTATION helpers
-- mesecon.rotate_rules_right(rules)
-- mesecon.rotate_rules_left(rules)
-- mesecon.rotate_rules_up(rules)
-- mesecon.rotate_rules_down(rules)
-- These functions return rules that have been rotated in the specific direction

-- General
function mesecon.get_effector(nodename)
	if  minetest.registered_nodes[nodename]
	and minetest.registered_nodes[nodename].mesecons
	and minetest.registered_nodes[nodename].mesecons.effector then
		return minetest.registered_nodes[nodename].mesecons.effector
	end
end

function mesecon.get_receptor(nodename)
	if  minetest.registered_nodes[nodename]
	and minetest.registered_nodes[nodename].mesecons
	and minetest.registered_nodes[nodename].mesecons.receptor then
		return minetest.registered_nodes[nodename].mesecons.receptor
	end
end

function mesecon.get_conductor(nodename)
	if  minetest.registered_nodes[nodename]
	and minetest.registered_nodes[nodename].mesecons
	and minetest.registered_nodes[nodename].mesecons.conductor then
		return minetest.registered_nodes[nodename].mesecons.conductor
	end
end

function mesecon.get_any_outputrules(node)
	if not node then return nil end

	if mesecon.is_conductor(node.name) then
		return mesecon.conductor_get_rules(node)
	elseif mesecon.is_receptor(node.name) then
		return mesecon.receptor_get_rules(node)
	end
end

function mesecon.get_any_inputrules(node)
	if not node then return nil end

	if mesecon.is_conductor(node.name) then
		return mesecon.conductor_get_rules(node)
	elseif mesecon.is_effector(node.name) then
		return mesecon.effector_get_rules(node)
	end
end

function mesecon.get_any_rules(node)
	return mesecon.mergetable(mesecon.get_any_inputrules(node) or {},
		mesecon.get_any_outputrules(node) or {})
end

-- Receptors
-- Nodes that can power mesecons
function mesecon.is_receptor_on(nodename)
	local receptor = mesecon.get_receptor(nodename)
	if receptor and receptor.state == mesecon.state.on then
		return true
	end
	return false
end

function mesecon.is_receptor_off(nodename)
	local receptor = mesecon.get_receptor(nodename)
	if receptor and receptor.state == mesecon.state.off then
		return true
	end
	return false
end

function mesecon.is_receptor(nodename)
	local receptor = mesecon.get_receptor(nodename)
	if receptor then
		return true
	end
	return false
end

function mesecon.receptor_get_rules(node)
	local receptor = mesecon.get_receptor(node.name)
	if receptor then
		local rules = receptor.rules
		if type(rules) == 'function' then
			return rules(node)
		elseif rules then
			return rules
		end
	end

	return mesecon.rules.default
end

-- Effectors
-- Nodes that can be powered by mesecons
function mesecon.is_effector_on(nodename)
	local effector = mesecon.get_effector(nodename)
	if effector and effector.action_off then
		return true
	end
	return false
end

function mesecon.is_effector_off(nodename)
	local effector = mesecon.get_effector(nodename)
	if effector and effector.action_on then
		return true
	end
	return false
end

function mesecon.is_effector(nodename)
	local effector = mesecon.get_effector(nodename)
	if effector then
		return true
	end
	return false
end

function mesecon.effector_get_rules(node)
	local effector = mesecon.get_effector(node.name)
	if effector then
		local rules = effector.rules
		if type(rules) == 'function' then
			return rules(node)
		elseif rules then
			return rules
		end
	end
	return mesecon.rules.default
end

-- #######################
-- # Signals (effectors) #
-- #######################

-- Activation:
mesecon.queue:add_function("activate", function (pos, rulename)
	local node = mesecon.get_node_force(pos)
	if not node then return end

	local effector = mesecon.get_effector(node.name)

	if effector and effector.action_on then
		effector.action_on(pos, node, rulename)
	end
end)

function mesecon.activate(pos, node, rulename, depth)
	if rulename == nil then
		for _,rule in ipairs(mesecon.effector_get_rules(node)) do
			mesecon.activate(pos, node, rule, depth + 1)
		end
		return
	end
	mesecon.queue:add_action(pos, "activate", {rulename}, nil, rulename, 1 / depth)
end

-- Deactivation
mesecon.queue:add_function("deactivate", function (pos, rulename)
	local node = mesecon.get_node_force(pos)
	if not node then return end

	local effector = mesecon.get_effector(node.name)

	if effector and effector.action_off then
		effector.action_off(pos, node, rulename)
	end
end)

function mesecon.deactivate(pos, node, rulename, depth)
	if rulename == nil then
		for _,rule in ipairs(mesecon.effector_get_rules(node)) do
			mesecon.deactivate(pos, node, rule, depth + 1)
		end
		return
	end
	mesecon.queue:add_action(pos, "deactivate", {rulename}, nil, rulename, 1 / depth)
end

-- Change
mesecon.queue:add_function("change", function (pos, rulename, changetype)
	local node = mesecon.get_node_force(pos)
	if not node then return end

	local effector = mesecon.get_effector(node.name)

	if effector and effector.action_change then
		effector.action_change(pos, node, rulename, changetype)
	end
end)

function mesecon.changesignal(pos, node, rulename, newstate, depth)
	if rulename == nil then
		for _,rule in ipairs(mesecon.effector_get_rules(node)) do
			mesecon.changesignal(pos, node, rule, newstate, depth + 1)
		end
		return
	end

	-- Include "change" in overwritecheck so that it cannot be overwritten
	-- by "active" / "deactivate" that will be called upon the node at the same time.
	local overwritecheck = {"change", rulename}
	mesecon.queue:add_action(pos, "change", {rulename, newstate}, nil, overwritecheck, 1 / depth)
end

-- Conductors

function mesecon.is_conductor_on(node, rulename)
	if not node then return false end

	local conductor = mesecon.get_conductor(node.name)
	if conductor then
		if conductor.state then
			return conductor.state == mesecon.state.on
		end
		if conductor.states then
			if not rulename then
				return mesecon.getstate(node.name, conductor.states) ~= 1
			end
			local bit = mesecon.rule2bit(rulename, mesecon.conductor_get_rules(node))
			local binstate = mesecon.getbinstate(node.name, conductor.states)
			return mesecon.get_bit(binstate, bit)
		end
	end

	return false
end

function mesecon.is_conductor_off(node, rulename)
	if not node then return false end

	local conductor = mesecon.get_conductor(node.name)
	if conductor then
		if conductor.state then
			return conductor.state == mesecon.state.off
		end
		if conductor.states then
			if not rulename then
				return mesecon.getstate(node.name, conductor.states) == 1
			end
			local bit = mesecon.rule2bit(rulename, mesecon.conductor_get_rules(node))
			local binstate = mesecon.getbinstate(node.name, conductor.states)
			return not mesecon.get_bit(binstate, bit)
		end
	end

	return false
end

function mesecon.is_conductor(nodename)
	local conductor = mesecon.get_conductor(nodename)
	if conductor then
		return true
	end
	return false
end

function mesecon.get_conductor_on(node_off, rulename)
	local conductor = mesecon.get_conductor(node_off.name)
	if conductor then
		if conductor.onstate then
			return conductor.onstate
		end
		if conductor.states then
			local bit = mesecon.rule2bit(rulename, mesecon.conductor_get_rules(node_off))
			local binstate = mesecon.getbinstate(node_off.name, conductor.states)
			binstate = mesecon.set_bit(binstate, bit, "1")
			return conductor.states[tonumber(binstate,2)+1]
		end
	end
	return offstate
end

function mesecon.get_conductor_off(node_on, rulename)
	local conductor = mesecon.get_conductor(node_on.name)
	if conductor then
		if conductor.offstate then
			return conductor.offstate
		end
		if conductor.states then
			local bit = mesecon.rule2bit(rulename, mesecon.conductor_get_rules(node_on))
			local binstate = mesecon.getbinstate(node_on.name, conductor.states)
			binstate = mesecon.set_bit(binstate, bit, "0")
			return conductor.states[tonumber(binstate,2)+1]
		end
	end
	return onstate
end

function mesecon.conductor_get_rules(node)
	local conductor = mesecon.get_conductor(node.name)
	if conductor then
		local rules = conductor.rules
		if type(rules) == 'function' then
			return rules(node)
		elseif rules then
			return rules
		end
	end
	return mesecon.rules.default
end

-- some more general high-level stuff

function mesecon.is_power_on(pos, rulename)
	local node = mesecon.get_node_force(pos)
	if node and (mesecon.is_conductor_on(node, rulename) or mesecon.is_receptor_on(node.name)) then
		return true
	end
	return false
end

function mesecon.is_power_off(pos, rulename)
	local node = mesecon.get_node_force(pos)
	if node and (mesecon.is_conductor_off(node, rulename) or mesecon.is_receptor_off(node.name)) then
		return true
	end
	return false
end

-- Turn off an equipotential section starting at `pos`, which outputs in the direction of `link`.
-- Breadth-first search. Map is abstracted away in a voxelmanip.
-- Follow all all conductor paths replacing conductors that were already
-- looked at, activating / changing all effectors along the way.
function mesecon.turnon(pos, link)
	local frontiers = {{pos = pos, link = link}}

	local depth = 1
	while frontiers[1] do
		local f = table.remove(frontiers, 1)
		local node = mesecon.get_node_force(f.pos)

		if not node then
			-- Area does not exist; do nothing
		elseif mesecon.is_conductor_off(node, f.link) then
			local rules = mesecon.conductor_get_rules(node)

			-- Call turnon on neighbors
			for _, r in ipairs(mesecon.rule2meta(f.link, rules)) do
				local np = vector.add(f.pos, r)
				for _, l in ipairs(mesecon.rules_link_rule_all(f.pos, r)) do
					table.insert(frontiers, {pos = np, link = l})
				end
			end

			mesecon.swap_node_force(f.pos, mesecon.get_conductor_on(node, f.link))
		elseif mesecon.is_effector(node.name) then
			mesecon.changesignal(f.pos, node, f.link, mesecon.state.on, depth)
			if mesecon.is_effector_off(node.name) then
				mesecon.activate(f.pos, node, f.link, depth)
			end
		end
		depth = depth + 1
	end
end

-- Turn on an equipotential section starting at `pos`, which outputs in the direction of `link`.
-- Breadth-first search. Map is abstracted away in a voxelmanip.
-- Follow all all conductor paths replacing conductors that were already
-- looked at, deactivating / changing all effectors along the way.
-- In case an onstate receptor is discovered, abort the process by returning false, which will
-- cause `receptor_off` to discard all changes made in the voxelmanip.
-- Contrary to turnon, turnoff has to cache all change and deactivate signals so that they will only
-- be called in the very end when we can be sure that no conductor was found along the path.
--
-- Signal table entry structure:
-- {
--	pos = position of effector,
--	node = node descriptor (name, param1 and param2),
--	link = link the effector is connected to,
--	depth = indicates order in which signals wire fired, higher is later
-- }
function mesecon.turnoff(pos, link)
	local frontiers = {{pos = pos, link = link}}
	local signals = {}

	local depth = 1
	while frontiers[1] do
		local f = table.remove(frontiers, 1)
		local node = mesecon.get_node_force(f.pos)

		if not node then
			-- Area does not exist; do nothing
		elseif mesecon.is_conductor_on(node, f.link) then
			local rules = mesecon.conductor_get_rules(node)
			for _, r in ipairs(mesecon.rule2meta(f.link, rules)) do
				local np = vector.add(f.pos, r)

				-- Check if an onstate receptor is connected. If that is the case,
				-- abort this turnoff process by returning false. `receptor_off` will
				-- discard all the changes that we made in the voxelmanip:
				for _, l in ipairs(mesecon.rules_link_rule_all_inverted(f.pos, r)) do
					if mesecon.is_receptor_on(mesecon.get_node_force(np).name) then
						return false
					end
				end

				-- Call turnoff on neighbors
				for _, l in ipairs(mesecon.rules_link_rule_all(f.pos, r)) do
					table.insert(frontiers, {pos = np, link = l})
				end
			end

			mesecon.swap_node_force(f.pos, mesecon.get_conductor_off(node, f.link))
		elseif mesecon.is_effector(node.name) then
			table.insert(signals, {
				pos = f.pos,
				node = node,
				link = f.link,
				depth = depth
			})
		end
		depth = depth + 1
	end

	for _, sig in ipairs(signals) do
		mesecon.changesignal(sig.pos, sig.node, sig.link, mesecon.state.off, sig.depth)
		if mesecon.is_effector_on(sig.node.name) and not mesecon.is_powered(sig.pos) then
			mesecon.deactivate(sig.pos, sig.node, sig.link, sig.depth)
		end
	end

	return true
end

-- Get all linking inputrules of inputnode (effector or conductor) that is connected to
-- outputnode (receptor or conductor) at position `output` and has an output in direction `rule`
function mesecon.rules_link_rule_all(output, rule)
	local input = vector.add(output, rule)
	local inputnode = mesecon.get_node_force(input)
	local inputrules = mesecon.get_any_inputrules(inputnode)
	if not inputrules then
		return {}
	end
	local rules = {}

	for _, inputrule in ipairs(mesecon.flattenrules(inputrules)) do
		-- Check if input accepts from output
		if  vector.equals(vector.add(input, inputrule), output) then
			table.insert(rules, inputrule)
		end
	end

	return rules
end

-- Get all linking outputnodes of outputnode (receptor or conductor) that is connected to
-- inputnode (effector or conductor) at position `input` and has an input in direction `rule`
function mesecon.rules_link_rule_all_inverted(input, rule)
	local output = vector.add(input, rule)
	local outputnode = mesecon.get_node_force(output)
	local outputrules = mesecon.get_any_outputrules(outputnode)
	if not outputrules then
		return {}
	end
	local rules = {}

	for _, outputrule in ipairs(mesecon.flattenrules(outputrules)) do
		if  vector.equals(vector.add(output, outputrule), input) then
			table.insert(rules, mesecon.invertRule(outputrule))
		end
	end
	return rules
end

function mesecon.is_powered(pos, rule)
	local node = mesecon.get_node_force(pos)
	local rules = mesecon.get_any_inputrules(node)
	if not rules then return false end

	-- List of nodes that send out power to pos
	local sourcepos = {}

	if not rule then
		for _, rule in ipairs(mesecon.flattenrules(rules)) do
			local rulenames = mesecon.rules_link_rule_all_inverted(pos, rule)
			for _, rname in ipairs(rulenames) do
				local np = vector.add(pos, rname)
				local nn = mesecon.get_node_force(np)

				if (mesecon.is_conductor_on(nn, mesecon.invertRule(rname))
				or mesecon.is_receptor_on(nn.name)) then
					table.insert(sourcepos, np)
				end
			end
		end
	else
		local rulenames = mesecon.rules_link_rule_all_inverted(pos, rule)
		for _, rname in ipairs(rulenames) do
			local np = vector.add(pos, rname)
			local nn = mesecon.get_node_force(np)
			if (mesecon.is_conductor_on (nn, mesecon.invertRule(rname))
			or mesecon.is_receptor_on (nn.name)) then
				table.insert(sourcepos, np)
			end
		end
	end

	-- Return FALSE if not powered, return list of sources if is powered
	if (#sourcepos == 0) then return false
	else return sourcepos end
end


code:
-- Un-forceload any forceloaded mapblocks from older versions of Mesecons which
-- used forceloading instead of VoxelManipulators.
local BLOCKSIZE = 16

-- convert block hash --> node position
local function unhash_blockpos(hash)
	return vector.multiply(minetest.get_position_from_hash(hash), BLOCKSIZE)
end

local old_forceloaded_blocks = mesecon.file2table("mesecon_forceloaded")
for hash, _ in pairs(old_forceloaded_blocks) do
	minetest.forceload_free_block(unhash_blockpos(hash))
end
os.remove(minetest.get_worldpath()..DIR_DELIM.."mesecon_forceloaded")


code:
name = mesecons
depends = default


code:
minetest.register_node("mesecons:mesecon_off", {
	drawtype = "raillike",
	tiles = {"jeija_mesecon_off.png", "jeija_mesecon_curved_off.png", "jeija_mesecon_t_junction_off.png", "jeija_mesecon_crossing_off.png"},
	inventory_image = "jeija_mesecon_off.png",
	wield_image = "jeija_mesecon_off.png",
	paramtype = "light",
	is_ground_content = false,
	walkable = false,
	selection_box = {
		type = "fixed",
		fixed = {-0.5, -0.5, -0.5, 0.5, -0.45, 0.5},
	},
	groups = {dig_immediate=3, mesecon=1, mesecon_conductor_craftable=1},
    	description="Mesecons",
	mesecons = {conductor={
		state = mesecon.state.off,
		onstate = "mesecons:mesecon_on"
	}}
})

minetest.register_node("mesecons:mesecon_on", {
	drawtype = "raillike",
	tiles = {"jeija_mesecon_on.png", "jeija_mesecon_curved_on.png", "jeija_mesecon_t_junction_on.png", "jeija_mesecon_crossing_on.png"},
	paramtype = "light",
	is_ground_content = false,
	walkable = false,
	selection_box = {
		type = "fixed",
		fixed = {-0.5, -0.5, -0.5, 0.5, -0.45, 0.5},
	},
	groups = {dig_immediate=3, not_in_creaive_inventory=1, mesecon=1},
	drop = "mesecons:mesecon_off 1",
	light_source = minetest.LIGHT_MAX-11,
	mesecons = {conductor={
		state = mesecon.state.on,
		offstate = "mesecons:mesecon_off"
	}}
})


code:
mesecon.rules = {}
mesecon.state = {}

mesecon.rules.default = {
	{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},
}

mesecon.rules.floor = mesecon.mergetable(mesecon.rules.default, {{x = 0, y = -1, z = 0}})

mesecon.rules.pplate = mesecon.mergetable(mesecon.rules.floor, {{x = 0, y = -2, z = 0}})

mesecon.rules.buttonlike = {
	{x = 1,  y =  0, z =  0},
	{x = 1,  y =  1, z =  0},
	{x = 1,  y = -1, z =  0},
	{x = 1,  y = -1, z =  1},
	{x = 1,  y = -1, z = -1},
	{x = 2,  y =  0, z =  0},
}

mesecon.rules.flat = {
	{x =  1, y = 0, z =  0},
	{x = -1, y = 0, z =  0},
	{x =  0, y = 0, z =  1},
	{x =  0, y = 0, z = -1},
}

mesecon.rules.alldirs = {
	{x =  1, y =  0,  z =  0},
	{x = -1, y =  0,  z =  0},
	{x =  0, y =  1,  z =  0},
	{x =  0, y = -1,  z =  0},
	{x =  0, y =  0,  z =  1},
	{x =  0, y =  0,  z = -1},
}

local rules_wallmounted = {
	xp = mesecon.rotate_rules_down(mesecon.rules.floor),
	xn = mesecon.rotate_rules_up(mesecon.rules.floor),
	yp = mesecon.rotate_rules_up(mesecon.rotate_rules_up(mesecon.rules.floor)),
	yn = mesecon.rules.floor,
	zp = mesecon.rotate_rules_left(mesecon.rotate_rules_up(mesecon.rules.floor)),
	zn = mesecon.rotate_rules_right(mesecon.rotate_rules_up(mesecon.rules.floor)),
}

local rules_buttonlike = {
	xp = mesecon.rules.buttonlike,
	xn = mesecon.rotate_rules_right(mesecon.rotate_rules_right(mesecon.rules.buttonlike)),
	yp = mesecon.rotate_rules_down(mesecon.rules.buttonlike),
	yn = mesecon.rotate_rules_up(mesecon.rules.buttonlike),
	zp = mesecon.rotate_rules_right(mesecon.rules.buttonlike),
	zn = mesecon.rotate_rules_left(mesecon.rules.buttonlike),
}

local function rules_from_dir(ruleset, dir)
	if dir.x ==  1 then return ruleset.xp end
	if dir.y ==  1 then return ruleset.yp end
	if dir.z ==  1 then return ruleset.zp end
	if dir.x == -1 then return ruleset.xn end
	if dir.y == -1 then return ruleset.yn end
	if dir.z == -1 then return ruleset.zn end
end

mesecon.rules.wallmounted_get = function(node)
	local dir = minetest.wallmounted_to_dir(node.param2)
	return rules_from_dir(rules_wallmounted, dir)
end

mesecon.rules.buttonlike_get = function(node)
	local dir = minetest.facedir_to_dir(node.param2)
	return rules_from_dir(rules_buttonlike, dir)
end

mesecon.state.on = "on"
mesecon.state.off = "off"


code:
-- Dig and place services

mesecon.on_placenode = function(pos, node)
	mesecon.execute_autoconnect_hooks_now(pos, node)

	-- Receptors: Send on signal when active
	if mesecon.is_receptor_on(node.name) then
		mesecon.receptor_on(pos, mesecon.receptor_get_rules(node))
	end

	-- Conductors: Send turnon signal when powered or replace by respective offstate conductor
	-- if placed conductor is an onstate one
	if mesecon.is_conductor(node.name) then
		local sources = mesecon.is_powered(pos)
		if sources then
			-- also call receptor_on if itself is powered already, so that neighboring
			-- conductors will be activated (when pushing an on-conductor with a piston)
			for _, s in ipairs(sources) do
				local rule = vector.subtract(pos, s)
				mesecon.turnon(pos, rule)
			end
			--mesecon.receptor_on (pos, mesecon.conductor_get_rules(node))
		elseif mesecon.is_conductor_on(node) then
			node.name = mesecon.get_conductor_off(node)
			minetest.swap_node(pos, node)
		end
	end

	-- Effectors: Send changesignal and activate or deactivate
	if mesecon.is_effector(node.name) then
		local powered_rules = {}
		local unpowered_rules = {}

		-- for each input rule, check if powered
		for _, r in ipairs(mesecon.effector_get_rules(node)) do
			local powered = mesecon.is_powered(pos, r)
			if powered then table.insert(powered_rules, r)
			else table.insert(unpowered_rules, r) end

			local state = powered and mesecon.state.on or mesecon.state.off
			mesecon.changesignal(pos, node, r, state, 1)
		end

		if (#powered_rules > 0) then
			for _, r in ipairs(powered_rules) do
				mesecon.activate(pos, node, r, 1)
			end
		else
			for _, r in ipairs(unpowered_rules) do
				mesecon.deactivate(pos, node, r, 1)
			end
		end
	end
end

mesecon.on_dignode = function(pos, node)
	if mesecon.is_conductor_on(node) then
		mesecon.receptor_off(pos, mesecon.conductor_get_rules(node))
	elseif mesecon.is_receptor_on(node.name) then
		mesecon.receptor_off(pos, mesecon.receptor_get_rules(node))
	end

	mesecon.execute_autoconnect_hooks_queue(pos, node)
end

function mesecon.on_blastnode(pos, intensity)
	local node = minetest.get_node(pos)
	minetest.remove_node(pos)
	mesecon.on_dignode(pos, node)
	return minetest.get_node_drops(node.name, "")
end

minetest.register_on_placenode(mesecon.on_placenode)
minetest.register_on_dignode(mesecon.on_dignode)

-- Overheating service for fast circuits
local OVERHEAT_MAX = mesecon.setting("overheat_max", 20)
local COOLDOWN_TIME = mesecon.setting("cooldown_time", 2.0)
local COOLDOWN_STEP = mesecon.setting("cooldown_granularity", 0.5)
local COOLDOWN_MULTIPLIER = OVERHEAT_MAX / COOLDOWN_TIME
local cooldown_timer = 0.0
local object_heat = {}

-- returns true if heat is too high
function mesecon.do_overheat(pos)
	local id = minetest.hash_node_position(pos)
	local heat = (object_heat[id] or 0) + 1
	object_heat[id] = heat
	if heat >= OVERHEAT_MAX then
		minetest.log("action", "Node overheats at " .. minetest.pos_to_string(pos))
		object_heat[id] = nil
		return true
	end
	return false
end

function mesecon.do_cooldown(pos)
	local id = minetest.hash_node_position(pos)
	object_heat[id] = nil
end

function mesecon.get_heat(pos)
	local id = minetest.hash_node_position(pos)
	return object_heat[id] or 0
end

function mesecon.move_hot_nodes(moved_nodes)
	local new_heat = {}
	for _, n in ipairs(moved_nodes) do
		local old_id = minetest.hash_node_position(n.oldpos)
		local new_id = minetest.hash_node_position(n.pos)
		new_heat[new_id] = object_heat[old_id]
		object_heat[old_id] = nil
	end
	for id, heat in pairs(new_heat) do
		object_heat[id] = heat
	end
end

local function global_cooldown(dtime)
	cooldown_timer = cooldown_timer + dtime
	if cooldown_timer < COOLDOWN_STEP then
		return -- don't overload the CPU
	end
	local cooldown = COOLDOWN_MULTIPLIER * cooldown_timer
	cooldown_timer = 0
	for id, heat in pairs(object_heat) do
		heat = heat - cooldown
		if heat <= 0 then
			object_heat[id] = nil -- free some RAM
		else
			object_heat[id] = heat
		end
	end
end
minetest.register_globalstep(global_cooldown)


code:
-- SETTINGS
function mesecon.setting(setting, default)
	if type(default) == "boolean" then
		local read = minetest.settings:get_bool("mesecon."..setting)
		if read == nil then
			return default
		else
			return read
		end
	elseif type(default) == "string" then
		return minetest.settings:get("mesecon."..setting) or default
	elseif type(default) == "number" then
		return tonumber(minetest.settings:get("mesecon."..setting) or default)
	end
end


code:
function mesecon.move_node(pos, newpos)
	local node = minetest.get_node(pos)
	local meta = minetest.get_meta(pos):to_table()
	minetest.remove_node(pos)
	minetest.set_node(newpos, node)
	minetest.get_meta(pos):from_table(meta)
end

-- Rules rotation Functions:
function mesecon.rotate_rules_right(rules)
	local nr = {}
	for i, rule in ipairs(rules) do
		table.insert(nr, {
			x = -rule.z,
			y =  rule.y,
			z =  rule.x,
			name = rule.name})
	end
	return nr
end

function mesecon.rotate_rules_left(rules)
	local nr = {}
	for i, rule in ipairs(rules) do
		table.insert(nr, {
			x =  rule.z,
			y =  rule.y,
			z = -rule.x,
			name = rule.name})
	end
	return nr
end

function mesecon.rotate_rules_down(rules)
	local nr = {}
	for i, rule in ipairs(rules) do
		table.insert(nr, {
			x = -rule.y,
			y =  rule.x,
			z =  rule.z,
			name = rule.name})
	end
	return nr
end

function mesecon.rotate_rules_up(rules)
	local nr = {}
	for i, rule in ipairs(rules) do
		table.insert(nr, {
			x =  rule.y,
			y = -rule.x,
			z =  rule.z,
			name = rule.name})
	end
	return nr
end
--

function mesecon.flattenrules(allrules)
--[[
	{
		{
			{xyz},
			{xyz},
		},
		{
			{xyz},
			{xyz},
		},
	}
--]]
	if allrules[1] and
	   allrules[1].x then
		return allrules
	end

	local shallowrules = {}
	for _, metarule in ipairs( allrules) do
	for _,     rule in ipairs(metarule ) do
		table.insert(shallowrules, rule)
	end
	end
	return shallowrules
--[[
	{
		{xyz},
		{xyz},
		{xyz},
		{xyz},
	}
--]]
end

function mesecon.rule2bit(findrule, allrules)
	--get the bit of the metarule the rule is in, or bit 1
	if (allrules[1] and
	    allrules[1].x) or
	    not findrule then
		return 1
	end
	for m,metarule in ipairs( allrules) do
	for _,    rule in ipairs(metarule ) do
		if vector.equals(findrule, rule) then
			return m
		end
	end
	end
end

function mesecon.rule2metaindex(findrule, allrules)
	--get the metarule the rule is in, or allrules
	if allrules[1].x then
		return nil
	end

	if not(findrule) then
		return mesecon.flattenrules(allrules)
	end

	for m, metarule in ipairs( allrules) do
	for _,     rule in ipairs(metarule ) do
		if vector.equals(findrule, rule) then
			return m
		end
	end
	end
end

function mesecon.rule2meta(findrule, allrules)
	if #allrules == 0 then return {} end

	local index = mesecon.rule2metaindex(findrule, allrules)
	if index == nil then
		if allrules[1].x then
			return allrules
		else
			return {}
		end
	end
	return allrules[index]
end

function mesecon.dec2bin(n)
	local x, y = math.floor(n / 2), n % 2
	if (n > 1) then
		return mesecon.dec2bin(x)..y
	else
		return ""..y
	end
end

function mesecon.getstate(nodename, states)
	for state, name in ipairs(states) do
		if name == nodename then
			return state
		end
	end
	error(nodename.." doesn't mention itself in "..dump(states))
end

function mesecon.getbinstate(nodename, states)
	return mesecon.dec2bin(mesecon.getstate(nodename, states)-1)
end

function mesecon.get_bit(binary,bit)
	bit = bit or 1
	local c = binary:len()-(bit-1)
	return binary:sub(c,c) == "1"
end

function mesecon.set_bit(binary,bit,value)
	if value == "1" then
		if not mesecon.get_bit(binary,bit) then
			return mesecon.dec2bin(tonumber(binary,2)+math.pow(2,bit-1))
		end
	elseif value == "0" then
		if mesecon.get_bit(binary,bit) then
			return mesecon.dec2bin(tonumber(binary,2)-math.pow(2,bit-1))
		end
	end
	return binary

end

function mesecon.invertRule(r)
	return vector.multiply(r, -1)
end

function mesecon.tablecopy(obj) -- deep copy
	if type(obj) == "table" then
		return table.copy(obj)
	end
	return obj
end

function mesecon.cmpAny(t1, t2)
	if type(t1) ~= type(t2) then return false end
	if type(t1) ~= "table" and type(t2) ~= "table" then return t1 == t2 end

	for i, e in pairs(t1) do
		if not mesecon.cmpAny(e, t2[i]) then return false end
	end

	return true
end

-- does not overwrite values; number keys (ipairs) are appended, not overwritten
function mesecon.mergetable(source, dest)
	local rval = mesecon.tablecopy(dest)

	for k, v in pairs(source) do
		rval[k] = dest[k] or mesecon.tablecopy(v)
	end
	for i, v in ipairs(source) do
		table.insert(rval, mesecon.tablecopy(v))
	end

	return rval
end

function mesecon.register_node(name, spec_common, spec_off, spec_on)
	spec_common.drop = spec_common.drop or name .. "_off"
	spec_common.on_blast = spec_common.on_blast or mesecon.on_blastnode
	spec_common.__mesecon_basename = name
	spec_on.__mesecon_state = "on"
	spec_off.__mesecon_state = "off"

	spec_on = mesecon.mergetable(spec_common, spec_on);
	spec_off = mesecon.mergetable(spec_common, spec_off);

	minetest.register_node(name .. "_on", spec_on)
	minetest.register_node(name .. "_off", spec_off)
end

-- swap onstate and offstate nodes, returns new state
function mesecon.flipstate(pos, node)
	local nodedef = minetest.registered_nodes[node.name]
	local newstate
	if (nodedef.__mesecon_state == "on") then newstate = "off" end
	if (nodedef.__mesecon_state == "off") then newstate = "on" end

	minetest.swap_node(pos, {name = nodedef.__mesecon_basename .. "_" .. newstate,
		param2 = node.param2})

	return newstate
end

-- File writing / reading utilities
local wpath = minetest.get_worldpath()
function mesecon.file2table(filename)
	local f = io.open(wpath..DIR_DELIM..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)
end

function mesecon.table2file(filename, table)
	local f = io.open(wpath..DIR_DELIM..filename, "w")
	f:write(minetest.serialize(table))
	f:close()
end

-- Block position "hashing" (convert to integer) functions for voxelmanip cache
local BLOCKSIZE = 16

-- convert node position --> block hash
local function hash_blockpos(pos)
	return minetest.hash_node_position({
		x = math.floor(pos.x/BLOCKSIZE),
		y = math.floor(pos.y/BLOCKSIZE),
		z = math.floor(pos.z/BLOCKSIZE)
	})
end

-- Maps from a hashed mapblock position (as returned by hash_blockpos) to a
-- table.
--
-- Contents of the table are:
-- “vm” → the VoxelManipulator
-- “va” → the VoxelArea
-- “data” → the data array
-- “param1” → the param1 array
-- “param2” → the param2 array
-- “dirty” → true if data has been modified
--
-- Nil if no VM-based transaction is in progress.
local vm_cache = nil

-- Starts a VoxelManipulator-based transaction.
--
-- During a VM transaction, calls to vm_get_node and vm_swap_node operate on a
-- cached copy of the world loaded via VoxelManipulators. That cache can later
-- be committed to the real map by means of vm_commit or discarded by means of
-- vm_abort.
function mesecon.vm_begin()
	vm_cache = {}
end

-- Finishes a VoxelManipulator-based transaction, freeing the VMs and map data
-- and writing back any modified areas.
function mesecon.vm_commit()
	for hash, tbl in pairs(vm_cache) do
		if tbl.dirty then
			local vm = tbl.vm
			vm:set_data(tbl.data)
			vm:write_to_map()
			vm:update_map()
		end
	end
	vm_cache = nil
end

-- Finishes a VoxelManipulator-based transaction, freeing the VMs and throwing
-- away any modified areas.
function mesecon.vm_abort()
	vm_cache = nil
end

-- Gets the cache entry covering a position, populating it if necessary.
local function vm_get_or_create_entry(pos)
	local hash = hash_blockpos(pos)
	local tbl = vm_cache[hash]
	if not tbl then
		local vm = minetest.get_voxel_manip(pos, pos)
		local min_pos, max_pos = vm:get_emerged_area()
		local va = VoxelArea:new{MinEdge = min_pos, MaxEdge = max_pos}
		tbl = {vm = vm, va = va, data = vm:get_data(), param1 = vm:get_light_data(), param2 = vm:get_param2_data(), dirty = false}
		vm_cache[hash] = tbl
	end
	return tbl
end

-- Gets the node at a given position during a VoxelManipulator-based
-- transaction.
function mesecon.vm_get_node(pos)
	local tbl = vm_get_or_create_entry(pos)
	local index = tbl.va:indexp(pos)
	local node_value = tbl.data[index]
	if node_value == core.CONTENT_IGNORE then
		return nil
	else
		local node_param1 = tbl.param1[index]
		local node_param2 = tbl.param2[index]
		return {name = minetest.get_name_from_content_id(node_value), param1 = node_param1, param2 = node_param2}
	end
end

-- Sets a node’s name during a VoxelManipulator-based transaction.
--
-- Existing param1, param2, and metadata are left alone.
function mesecon.vm_swap_node(pos, name)
	local tbl = vm_get_or_create_entry(pos)
	local index = tbl.va:indexp(pos)
	tbl.data[index] = minetest.get_content_id(name)
	tbl.dirty = true
end

-- Gets the node at a given position, regardless of whether it is loaded or
-- not, respecting a transaction if one is in progress.
--
-- Outside a VM transaction, if the mapblock is not loaded, it is pulled into
-- the server’s main map data cache and then accessed from there.
--
-- Inside a VM transaction, the transaction’s VM cache is used.
function mesecon.get_node_force(pos)
	if vm_cache then
		return mesecon.vm_get_node(pos)
	else
		local node = minetest.get_node_or_nil(pos)
		if node == nil then
			-- Node is not currently loaded; use a VoxelManipulator to prime
			-- the mapblock cache and try again.
			minetest.get_voxel_manip(pos, pos)
			node = minetest.get_node_or_nil(pos)
		end
		return node
	end
end

-- Swaps the node at a given position, regardless of whether it is loaded or
-- not, respecting a transaction if one is in progress.
--
-- Outside a VM transaction, if the mapblock is not loaded, it is pulled into
-- the server’s main map data cache and then accessed from there.
--
-- Inside a VM transaction, the transaction’s VM cache is used.
--
-- This function can only be used to change the node’s name, not its parameters
-- or metadata.
function mesecon.swap_node_force(pos, name)
	if vm_cache then
		return mesecon.vm_swap_node(pos, name)
	else
		-- This serves to both ensure the mapblock is loaded and also hand us
		-- the old node table so we can preserve param2.
		local node = mesecon.get_node_force(pos)
		node.name = name
		minetest.swap_node(pos, node)
	end
end

-- Autoconnect Hooks
-- Nodes like conductors may change their appearance and their connection rules
-- right after being placed or after being dug, e.g. the default wires use this
-- to automatically connect to linking nodes after placement.
-- After placement, the update function will be executed immediately so that the
-- possibly changed rules can be taken into account when recalculating the circuit.
-- After digging, the update function will be queued and executed after
-- recalculating the circuit. The update function must take care of updating the
-- node at the given position itself, but also all of the other nodes the given
-- position may have (had) a linking connection to.
mesecon.autoconnect_hooks = {}

-- name: A unique name for the hook, e.g. "foowire". Used to name the actionqueue function.
-- fct: The update function with parameters function(pos, node)
function mesecon.register_autoconnect_hook(name, fct)
	mesecon.autoconnect_hooks[name] = fct
	mesecon.queue:add_function("autoconnect_hook_"..name, fct)
end

function mesecon.execute_autoconnect_hooks_now(pos, node)
	for _, fct in pairs(mesecon.autoconnect_hooks) do
		fct(pos, node)
	end
end

function mesecon.execute_autoconnect_hooks_queue(pos, node)
	for name in pairs(mesecon.autoconnect_hooks) do
		mesecon.queue:add_action(pos, "autoconnect_hook_"..name, {node})
	end
end


code:
-- This file registers aliases for the /give /giveme commands.

minetest.register_alias("mesecons:removestone", "mesecons_random:removestone")
minetest.register_alias("mesecons:power_plant", "mesecons_powerplant:power_plant")
minetest.register_alias("mesecons:powerplant", "mesecons_powerplant:power_plant")
minetest.register_alias("mesecons:meselamp", "mesecons_lamp:lamp_off")
minetest.register_alias("mesecons:mesecon", "mesecons:wire_00000000_off")
minetest.register_alias("mesecons:object_detector", "mesecons_detector:object_detector_off")
minetest.register_alias("mesecons:wireless_inverter", "mesecons_wireless:wireless_inverter_on")
minetest.register_alias("mesecons:wireless_receiver", "mesecons_wireless:wireless_receiver_off")
minetest.register_alias("mesecons:wireless_transmitter", "mesecons_wireless:wireless_transmitter_off")
minetest.register_alias("mesecons:switch", "mesecons_switch:mesecon_switch_off")
minetest.register_alias("mesecons:button", "mesecons_button:button_off")
minetest.register_alias("mesecons:piston", "mesecons_pistons:piston_normal_off")
minetest.register_alias("mesecons:blinky_plant", "mesecons_blinkyplant:blinky_plant_off")
minetest.register_alias("mesecons:mesecon_torch", "mesecons_torch:mesecon_torch_on")
minetest.register_alias("mesecons:torch", "mesecons_torch:mesecon_torch_on")
minetest.register_alias("mesecons:hydro_turbine", "mesecons_hydroturbine:hydro_turbine_off")
minetest.register_alias("mesecons:pressure_plate_stone", "mesecons_pressureplates:pressure_plate_stone_off")
minetest.register_alias("mesecons:pressure_plate_wood", "mesecons_pressureplates:pressure_plate_wood_off")
minetest.register_alias("mesecons:mesecon_socket", "mesecons_temperest:mesecon_socket_off")
minetest.register_alias("mesecons:mesecon_inverter", "mesecons_temperest:mesecon_inverter_on")
minetest.register_alias("mesecons:movestone", "mesecons_movestones:movestone")
minetest.register_alias("mesecons:sticky_movestone", "mesecons_movestones:sticky_movestone")
minetest.register_alias("mesecons:noteblock", "mesecons_noteblock:noteblock")
minetest.register_alias("mesecons:microcontroller", "mesecons_microcontroller:microcontroller0000")
minetest.register_alias("mesecons:delayer", "mesecons_delayer:delayer_off_1")
minetest.register_alias("mesecons:solarpanel", "mesecons_solarpanel:solar_panel_off")

--Backwards compatibility
minetest.register_alias("mesecons:mesecon_off", "mesecons:wire_00000000_off")
minetest.register_alias("mesecons_pistons:piston_sticky", "mesecons_pistons:piston_sticky_on")
minetest.register_alias("mesecons_pistons:piston_normal", "mesecons_pistons:piston_normal_on")
minetest.register_alias("mesecons_pistons:piston_up_normal", "mesecons_pistons:piston_up_normal_on")
minetest.register_alias("mesecons_pistons:piston_down_normal", "mesecons_pistons:piston_down_normal_on")
minetest.register_alias("mesecons_pistons:piston_up_sticky", "mesecons_pistons:piston_up_sticky_on")
minetest.register_alias("mesecons_pistons:piston_down_sticky", "mesecons_pistons:piston_down_sticky_on")


code:
name = mesecons_alias
depends = mesecons


code:
The blinky plants toggles between on and off state every three seconds. Can be used to make clocks. Also works after having restarted the game.
It stops blinking in an inactive block, then starts again when the block becomes active.


code:
-- The BLINKY_PLANT

local toggle_timer = function (pos)
	local timer = minetest.get_node_timer(pos)
	if timer:is_started() then
		timer:stop()
	else
		timer:start(mesecon.setting("blinky_plant_interval", 3))
	end
end

local on_timer = function (pos)
	local node = minetest.get_node(pos)
	if(mesecon.flipstate(pos, node) == "on") then
		mesecon.receptor_on(pos)
	else
		mesecon.receptor_off(pos)
	end
	toggle_timer(pos)
end

mesecon.register_node("mesecons_blinkyplant:blinky_plant", {
	description="Blinky Plant",
	drawtype = "plantlike",
	inventory_image = "jeija_blinky_plant_off.png",
	paramtype = "light",
	is_ground_content = false,
	walkable = false,
	sounds = default.node_sound_leaves_defaults(),
	selection_box = {
		type = "fixed",
		fixed = {-0.3, -0.5, -0.3, 0.3, -0.5+0.7, 0.3},
	},
	on_timer = on_timer,
	on_rightclick = toggle_timer,
	on_construct = toggle_timer
},{
	tiles = {"jeija_blinky_plant_off.png"},
	groups = {dig_immediate=3},
	mesecons = {receptor = { state = mesecon.state.off }}
},{
	tiles = {"jeija_blinky_plant_on.png"},
	groups = {dig_immediate=3, not_in_creative_inventory=1},
	mesecons = {receptor = { state = mesecon.state.on }}
})

minetest.register_craft({
	output = "mesecons_blinkyplant:blinky_plant_off 1",
	recipe = {	{"","group:mesecon_conductor_craftable",""},
			{"","group:mesecon_conductor_craftable",""},
			{"group:sapling","group:sapling","group:sapling"}}
})


code:
name = mesecons_blinkyplant
depends = mesecons


code: This receptor can be attached to walls. It turns on for 1 second if it's punched.


code:
-- WALL BUTTON
-- A button that when pressed emits power for 1 second
-- and then turns off again

mesecon.button_turnoff = function (pos)
	local node = minetest.get_node(pos)
	if node.name ~= "mesecons_button:button_on" then -- has been dug
		return
	end
	minetest.swap_node(pos, {name = "mesecons_button:button_off", param2 = node.param2})
	minetest.sound_play("mesecons_button_pop", {pos = pos})
	local rules = mesecon.rules.buttonlike_get(node)
	mesecon.receptor_off(pos, rules)
end

minetest.register_node("mesecons_button:button_off", {
	drawtype = "nodebox",
	tiles = {
	"jeija_wall_button_sides.png",
	"jeija_wall_button_sides.png",
	"jeija_wall_button_sides.png",
	"jeija_wall_button_sides.png",
	"jeija_wall_button_sides.png",
	"jeija_wall_button_off.png"
	},
	paramtype = "light",
	paramtype2 = "facedir",
	is_ground_content = false,
	legacy_wallmounted = true,
	walkable = false,
	on_rotate = mesecon.buttonlike_onrotate,
	sunlight_propagates = true,
	selection_box = {
	type = "fixed",
		fixed = { -6/16, -6/16, 5/16, 6/16, 6/16, 8/16 }
	},
	node_box = {
		type = "fixed",
		fixed = {
		{ -6/16, -6/16, 6/16, 6/16, 6/16, 8/16 },	-- the thin plate behind the button
		{ -4/16, -2/16, 4/16, 4/16, 2/16, 6/16 }	-- the button itself
	}
	},
	groups = {dig_immediate=2, mesecon_needs_receiver = 1},
	description = "Button",
	on_rightclick = function (pos, node)
		minetest.swap_node(pos, {name = "mesecons_button:button_on", param2=node.param2})
		mesecon.receptor_on(pos, mesecon.rules.buttonlike_get(node))
		minetest.sound_play("mesecons_button_push", {pos=pos})
		minetest.get_node_timer(pos):start(1)
	end,
	sounds = default.node_sound_stone_defaults(),
	mesecons = {receptor = {
		state = mesecon.state.off,
		rules = mesecon.rules.buttonlike_get
	}},
	on_blast = mesecon.on_blastnode,
})

minetest.register_node("mesecons_button:button_on", {
	drawtype = "nodebox",
	tiles = {
		"jeija_wall_button_sides.png",
		"jeija_wall_button_sides.png",
		"jeija_wall_button_sides.png",
		"jeija_wall_button_sides.png",
		"jeija_wall_button_sides.png",
		"jeija_wall_button_on.png"
		},
	paramtype = "light",
	paramtype2 = "facedir",
	is_ground_content = false,
	legacy_wallmounted = true,
	walkable = false,
	on_rotate = false,
	light_source = minetest.LIGHT_MAX-7,
	sunlight_propagates = true,
	selection_box = {
		type = "fixed",
		fixed = { -6/16, -6/16, 5/16, 6/16, 6/16, 8/16 }
	},
	node_box = {
	type = "fixed",
	fixed = {
		{ -6/16, -6/16,  6/16, 6/16, 6/16, 8/16 },
		{ -4/16, -2/16, 11/32, 4/16, 2/16, 6/16 }
	}
    },
	groups = {dig_immediate=2, not_in_creative_inventory=1, mesecon_needs_receiver = 1},
	drop = 'mesecons_button:button_off',
	description = "Button",
	sounds = default.node_sound_stone_defaults(),
	mesecons = {receptor = {
		state = mesecon.state.on,
		rules = mesecon.rules.buttonlike_get
	}},
	on_timer = mesecon.button_turnoff,
	on_blast = mesecon.on_blastnode,
})

minetest.register_craft({
	output = "mesecons_button:button_off 2",
	recipe = {
		{"group:mesecon_conductor_craftable","default:stone"},
	}
})


code:
name = mesecons_button
depends = mesecons, mesecons_receiver


code:
There is no crafting recipe as this should only be available for server admins. Quite similar to the Minecraft counterpart. Executes server commands.
It works in inactive blocks.


code:
minetest.register_chatcommand("say", {
	params = "<text>",
	description = "Say <text> as the server",
	privs = {server=true},
	func = function(name, param)
		minetest.chat_send_all(name .. ": " .. param)
	end
})

minetest.register_chatcommand("tell", {
	params = "<name> <text>",
	description = "Say <text> to <name> privately",
	func = function(name, param)
		local found, _, target, message = param:find("^([^%s]+)%s+(.*)$")
		if found == nil then
			minetest.chat_send_player(name, "Invalid usage: " .. param)
			return
		end
		if not minetest.get_player_by_name(target) then
			minetest.chat_send_player(name, "Invalid target: " .. target)
		end
		minetest.chat_send_player(target, name .. " whispers: " .. message, false)
	end
})

minetest.register_chatcommand("hp", {
	params = "<name> <value>",
	description = "Set health of <name> to <value> hitpoints",
	privs = {ban=true},
	func = function(name, param)
		local found, _, target, value = param:find("^([^%s]+)%s+(%d+)$")
		if found == nil then
			minetest.chat_send_player(name, "Invalid usage: " .. param)
			return
		end
		local player = minetest.get_player_by_name(target)
		if player then
			player:set_hp(value)
		else
			minetest.chat_send_player(name, "Invalid target: " .. target)
		end
	end
})

local function initialize_data(meta)
	local commands = minetest.formspec_escape(meta:get_string("commands"))
	meta:set_string("formspec",
		"invsize[9,5;]" ..
		"textarea[0.5,0.5;8.5,4;commands;Commands;"..commands.."]" ..
		"label[1,3.8;@nearest, @farthest, and @random are replaced by the respective player names]" ..
		"button_exit[3.3,4.5;2,1;submit;Submit]")
	local owner = meta:get_string("owner")
	if owner == "" then
		owner = "not owned"
	else
		owner = "owned by " .. owner
	end
	meta:set_string("infotext", "Command Block\n" ..
		"(" .. owner .. ")\n" ..
		"Commands: "..commands)
end

local function construct(pos)
	local meta = minetest.get_meta(pos)

	meta:set_string("commands", "tell @nearest Commandblock unconfigured")

	meta:set_string("owner", "")

	initialize_data(meta)
end

local function after_place(pos, placer)
	if placer then
		local meta = minetest.get_meta(pos)
		meta:set_string("owner", placer:get_player_name())
		initialize_data(meta)
	end
end

local function receive_fields(pos, formname, fields, sender)
	if not fields.submit then
		return
	end
	local meta = minetest.get_meta(pos)
	local owner = meta:get_string("owner")
	if owner ~= "" and sender:get_player_name() ~= owner then
		return
	end
	meta:set_string("commands", fields.commands)

	initialize_data(meta)
end

local function resolve_commands(commands, pos)
	local players = minetest.get_connected_players()

	-- No players online: remove all commands containing
	-- @nearest, @farthest and @random
	if #players == 0 then
		commands = commands:gsub("[^\r\n]+", function (line)
			if line:find("@nearest") then return "" end
			if line:find("@farthest") then return "" end
			if line:find("@random") then return "" end
			return line
		end)
		return commands
	end

	local nearest, farthest = nil, nil
	local min_distance, max_distance = math.huge, -1
	for index, player in pairs(players) do
		local distance = vector.distance(pos, player:getpos())
		if distance < min_distance then
			min_distance = distance
			nearest = player:get_player_name()
		end
		if distance > max_distance then
			max_distance = distance
			farthest = player:get_player_name()
		end
	end
	local random = players[math.random(#players)]:get_player_name()
	commands = commands:gsub("@nearest", nearest)
	commands = commands:gsub("@farthest", farthest)
	commands = commands:gsub("@random", random)
	return commands
end

local function commandblock_action_on(pos, node)
	if node.name ~= "mesecons_commandblock:commandblock_off" then
		return
	end

	minetest.swap_node(pos, {name = "mesecons_commandblock:commandblock_on"})

	local meta = minetest.get_meta(pos)
	local owner = meta:get_string("owner")
	if owner == "" then
		return
	end

	local commands = resolve_commands(meta:get_string("commands"), pos)
	for _, command in pairs(commands:split("\n")) do
		local pos = command:find(" ")
		local cmd, param = command, ""
		if pos then
			cmd = command:sub(1, pos - 1)
			param = command:sub(pos + 1)
		end
		local cmddef = minetest.chatcommands[cmd]
		if not cmddef then
			minetest.chat_send_player(owner, "The command "..cmd.." does not exist")
			return
		end
		local has_privs, missing_privs = minetest.check_player_privs(owner, cmddef.privs)
		if not has_privs then
			minetest.chat_send_player(owner, "You don't have permission "
					.."to run "..cmd
					.." (missing privileges: "
					..table.concat(missing_privs, ", ")..")")
			return
		end
		cmddef.func(owner, param)
	end
end

local function commandblock_action_off(pos, node)
	if node.name == "mesecons_commandblock:commandblock_on" then
		minetest.swap_node(pos, {name = "mesecons_commandblock:commandblock_off"})
	end
end

local function can_dig(pos, player)
	local meta = minetest.get_meta(pos)
	local owner = meta:get_string("owner")
	return owner == "" or owner == player:get_player_name()
end

minetest.register_node("mesecons_commandblock:commandblock_off", {
	description = "Command Block",
	tiles = {"jeija_commandblock_off.png"},
	inventory_image = minetest.inventorycube("jeija_commandblock_off.png"),
	is_ground_content = false,
	groups = {cracky=2, mesecon_effector_off=1},
	on_construct = construct,
	after_place_node = after_place,
	on_receive_fields = receive_fields,
	can_dig = can_dig,
	sounds = default.node_sound_stone_defaults(),
	mesecons = {effector = {
		action_on = commandblock_action_on
	}},
	on_blast = mesecon.on_blastnode,
})

minetest.register_node("mesecons_commandblock:commandblock_on", {
	tiles = {"jeija_commandblock_on.png"},
	is_ground_content = false,
	groups = {cracky=2, mesecon_effector_on=1, not_in_creative_inventory=1},
	light_source = 10,
	drop = "mesecons_commandblock:commandblock_off",
	on_construct = construct,
	after_place_node = after_place,
	on_receive_fields = receive_fields,
	can_dig = can_dig,
	sounds = default.node_sound_stone_defaults(),
	mesecons = {effector = {
		action_off = commandblock_action_off
	}},
	on_blast = mesecon.on_blastnode,
})


code:
name = mesecons_commandblock
depends = mesecons


code: The delayer delays the signal from the input for a determined time. The time can be set by punching the delayer. Possible delays are: 0.1 seconds, 0.3 seconds, 0.5 seconds and 1 second. You may try to use it for creating songs with the noteblock. It works in unloaded blocks.