code:
# If disabled each compass can be calibrated one time only
ccompass_recalibrate (Allow compass recalibration) bool true

# If disabled (default) all nodes are allowed to be compass target
ccompass_restrict_target (Restrict nodes usable for calibration) bool false

# List of technical node names allowed for compass calibration, separated by ','
ccompass_restrict_target_nodes (Nodes list allowed for calibration) string

# Enable aliasses to replace other compass mods
ccompass_aliasses (Enable compatibility aliasses) bool false

# Nodes able to teleport to destination on punch, separated by ','
ccompass_teleport_nodes (Nodes list for teleport on punch) string default:mese


code:
# Auto detect text files and perform LF normalization
* text=auto

repo_name: korkut/MeseCraft

license: mit


code:
# Compiled Lua sources
luac.out

# luarocks build files
*.src.rock
*.zip
*.tar.gz

# Object files
*.o
*.os
*.ko
*.elf

# Precompiled Headers
*.gch
*.pch

# Libraries
*.lib
*.a
*.la
*.lo
*.def
*.exp

# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib

# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex


code:
if not minetest.get_modpath("doc") then
	return
end

local S = minetest.get_translator(minetest.get_current_modname())

doc.add_category("commoditymarket",
{
	name = S("Commodity Markets"),
	description = S("Game-wide marketplaces where goods can be bought and sold at prices of your choice."),
	build_formspec = doc.entry_builders.text_and_gallery,
})

doc.add_entry("commoditymarket", "ui_inventory", {
	name = S("User Interface: Inventory"),
	data = { text =
S("Each player's account has an inventory that serves as a holding area for items that are destined to be sold or that have been bought by the player but not yet retrieved. This inventory is a bit different from the standard Minetest inventory in that it doesn't hold item \"stacks\", it just tracks the total number of that item present. Some markets allow for extremely large quantities of an item to be stored here for sale."
.."\n\n"..
"To add an item to your market inventory for eventual sale either shift-click on the item in your player inventory or drag the item stack to the inventory slot below the main market inventory list. Some markets may have restrictions on what items can be bought and sold, if an item is not valid for that market it won't go into the market's inventory. Some items are considered \"currency\" and will add to your account's currency balance instead of being listed in your market inventory."
.."\n\n"..
"Tools cannot be added to the market inventory if they have any wear on them. The market also can't handle items with attached metadata such as books that have had text added to them."
.."\n\n"..
"To remove an item from your market inventory, double-click in it in the market inventory list. As much of the item as can fit into your player inventory will be transferred to you, with any remainder staying behind in the market inventory. To withdraw currency from your market balance type the amount you'd like to withdraw in the field next to the \"Withdraw\" button. The currency will be converted into items and added to your player inventory, with whatever cannot be converted remaining behind in your market balance.")
}})

doc.add_entry("commoditymarket", "ui_orders", {
	name = S("User Interface: Orders"),
	data = { text =
S(
"At the core of how a market operates are \"buy\" and \"sell\" orders. A buy order is an announcement to the world that you are interested in purchasing a certain quantity of item and are willing to pay a certain amount of currency in exchange for each unit of that item. Conversely, a sell order is an announcement to the world that you are interested in selling a certain quantity of item and will accept a certain amount of currency in exchange for each unit of that item."
.."\n\n"..
"The market price of an item is determined by where the existing buy and sell orders for that item intersect. When you offer to buy an item for a price that someone is offering to sell it at, the item is transferred to you and currency is transferred from your account to theirs to cover the cost. The market will keep track of the most recent price that an item was successfully sold for, but note that this information is for historical interest only - there's no guarantee that anyone is currently willing to match the historical price."
.."\n\n"..
"When an item is selected in the upper list, the currently existing buy and sell orders for that item will be displayed in the lower list. Sell orders are listed first in descending price, followed by buy orders in ascending price. The current market price will be somewhere in between the lowest sell order and the highest buy order. If you wish to cancel a buy or sell order that you've placed for an item, double-click on the order and the item or currency that you put into that order will be returned to your inventory."
.."\n\n"..
"If you place a buy order and there are already sell orders for the item that meet or are below your price, some or all of your buy order might be immediately fulfilled. Your purchases will be made at the price that the sell orders have been set to - if you were willing to pay 15 units of currency per item but someone was already offering to sell for 2 units of currency per item, you only pay 2 units for each of that offer's items. If there aren't enough compatible sell orders to fulfill your buy order, the remainder will be placed into the market and made available for future sellers to see and fulfill if they agree to your price. Your buy order will immediately deduct the currency required for it from your account's balance, but if you cancel your order you will get that currency back - it's not gone until the order is actually fulfilled."
.."\n\n"..
"If you place a sell order and there are already buy orders that meet or exceed your price, some or all of your sell order may be immediately fulfilled. You'll be paid the price that the buyers are offering rather than the amount you're demanding. If any of your sell offer is left unfulfilled, the sell order will be added to the market for future buyers to see. The items for this offer will be immediately taken from your market inventory but if you cancel your order you will get those items back.")
}})


code:
local S = minetest.get_translator(minetest.get_current_modname())

local mcl_formspec_itemslots

if minetest.get_modpath("mcl_init") then
	if mcl_formspec then
		mcl_formspec_itemslots = mcl_formspec.get_itemslot_bg
	end
end

local truncate_item_names_to = 30

-- Large textures can screw with the formspecs.
-- See https://github.com/minetest/minetest/issues/9300 for a feature request that would simplify and improve icon generation, if supported.
-- In the meantime, here's some methods for overriding item icons to manually work around this:
local override_item_icon = {}
commoditymarket.override_item_icon = function(item_name, new_icon_texture)
	override_item_icon[item_name] = new_icon_texture
end
local override_image_icon = {}
commoditymarket.override_image_icon = function(old_icon_texture, new_icon_texture)
	override_image_icon[old_icon_texture] = new_icon_texture
end
-- And a setting for disabling icons entirely:
local global_enable_item_icons = minetest.settings:get_bool("commoditymarket_enable_item_icons", true)

--[inventorycube{<top>{<left>{<right>
--Escaping does not apply here and `^` is replaced by `&` in texture names instead.
--Example:
--    [inventorycube{grass.png{dirt.png&grass_side.png{dirt.png&grass_side.png
--Creates an inventorycube with `grass.png`, `dirt.png^grass_side.png` and `dirt.png^grass_side.png` textures
local process_inventory_cube = function(texture_string)
	if not texture_string:sub(1,14) == "[inventorycube" then
		return texture_string
	end
	local split = texture_string:split("{")
	local left = split[3] -- the "front" of the cube we're seeing in the inventory list
	if left == nil then -- in case something weird happens, don't crash.
		return texture_string
	end
	left = left:gsub("&", "^")
	return left
end

local get_icon = function(item)
	local def = minetest.registered_items[item]
	local returnstring = "unknown_item.png"
	if def == nil then
		return returnstring
	end

	local override = override_item_icon[item]
	if override then
		return override
	end

	local inventory_image = def.inventory_image
	if inventory_image and inventory_image ~= "" then
		returnstring = inventory_image
	else
		local tiles = def.tiles
		if tiles then
			local tilecount = #tiles
			-- Textures of node; +Y, -Y, +X, -X, +Z, -Z
			local selected_tile = tiles[math.min(5,tilecount)]
			if type(selected_tile) == "string" then
				returnstring = selected_tile
			else
				local tile_name = selected_tile.name
				if tile_name then
					returnstring = tile_name
				end
			end
		end
	end
	returnstring = process_inventory_cube(returnstring)

	-- Formspec tables can't handle image compositing and modifiers
	local found_caret = returnstring:find("%^")
	if found_caret then
		returnstring = returnstring:sub(1, found_caret-1)
	end

	override = override_image_icon[returnstring]
	if override then
		return override
	end

	return minetest.formspec_escape(returnstring)
end
-- Exposed so that the purge_unknowns command can use it.
commoditymarket.get_icon = get_icon

local truncate_string = function(target, length)
	if target:len() > length then
		return target:sub(1,length-2).."..."
	end
	return target
end

local get_item_description = function(item)
	local def = minetest.registered_items[item]
	if def then
		local description = def.description
		if description then
			return minetest.formspec_escape(description:gsub("\n", " "))
		end
	end
	return S("Unknown Item")
end

-- Inventory formspec
-------------------------------------------------------------------------------------

local inventory_item_comp = function(invitem1, invitem2) return invitem1.item < invitem2.item end
local inventory_desc_comp = function(invitem1, invitem2) return invitem1.description < invitem2.description end

local get_account_formspec = function(market, account)
	local show_itemnames = account.show_itemnames == "true"
	local show_icons = global_enable_item_icons and ((account.show_icons or "true") == "true")
	local market_def = market.def

	local inventory = {}
	local inventory_count = 0
	for item, quantity in pairs(account.inventory) do
		local icon
		if show_icons then
			icon = get_icon(item)
		end
		table.insert(inventory, {item=item, quantity=quantity, icon=icon, description=get_item_description(item)})
		inventory_count = inventory_count + quantity
	end
	if show_itemnames then
		table.sort(inventory, inventory_item_comp)
	else
		table.sort(inventory, inventory_desc_comp)
	end

	local formspec = {
		"size[10,10]"
		.."tabheader[0,0;tabs;"..market_def.description..","..S("Your Inventory")..","..S("Market Orders")..";2;false;true]"
	}
	formspec[#formspec+1] = "tablecolumns["

	if show_icons then
		formspec[#formspec+1] = "image"
		for i=1, #inventory, 2 do
			formspec[#formspec+1] = ","..i.."="..inventory[i].icon
		end
		formspec[#formspec+1] = ";"
	end
	if show_itemnames then
		formspec[#formspec+1] = "text;"
	end
	formspec[#formspec+1] = "text;text,align=center"
	if show_icons then
		formspec[#formspec+1] = ";image"
		for i=2, #inventory, 2 do
			formspec[#formspec+1] = ","..i.."="..inventory[i].icon
		end
	end
	if show_itemnames then
		formspec[#formspec+1] = ";text"
	end
	formspec[#formspec+1] = ";text;text,align=center]"
		.."tooltip[inventory;"..S("All the items you've transfered to the market to sell and the items you've\npurchased with buy orders. Double-click on an item to bring it back into your\npersonal inventory.").."]"
		.."table[0,0;9.75,4;inventory;"
	if show_icons then
		formspec[#formspec+1] = "0,"
	end
	if show_itemnames then
		formspec[#formspec+1] = S("Item")..","
	end
	formspec[#formspec+1] = S("Description")..","..S("Quantity")
	if show_icons then
		formspec[#formspec+1] = ",0"
	end
	if show_itemnames then
		formspec[#formspec+1] = ","..S("Item")
	end
	formspec[#formspec+1] = ","..S("Description")..","..S("Quantity")

	for i, entry in ipairs(inventory) do
		if show_icons then
			formspec[#formspec+1] = "," .. i
		end
		if show_itemnames then
			formspec[#formspec+1] = "," .. truncate_string(entry.item, truncate_item_names_to)
		end
		-- no need to formspec_escape description here, it gets done when it's initially added to the inventory table
		formspec[#formspec+1] = "," .. entry.description .. "," .. entry.quantity
	end

	formspec[#formspec+1] = "]container[1,4.5]list[detached:commoditymarket:" .. market.name .. ";add;0,0;1,1;]"
		.."label[1,0;"..S("Drop items here to\nadd to your account").."]"
		.."listring[current_player;main]listring[detached:commoditymarket:" .. market.name .. ";add]"

	if mcl_formspec_itemslots then
		formspec[#formspec+1] = mcl_formspec_itemslots(0,0,1,1)
	end

	if market_def.inventory_limit then
		formspec[#formspec+1] = "label[3,0;"..S("Inventory limit:").."\n" .. inventory_count.."/" .. market_def.inventory_limit .. "]"
			.. "tooltip[3,0;1.5,1;"..S("You can still receive purchased items if you've exceeded your inventory limit,\nbut you won't be able to transfer items from your personal inventory into\nthe market until you've emptied it back down below the limit again.").."]"
	end
	formspec[#formspec+1] = "label[4.9,0;"..S("Balance:") .. "\n" .. market_def.currency_symbol .. account.balance .. "]"
		.."tooltip[4.9,0;3.5,1;"..S("Enter the amount of currency you'd like to withdraw then click the 'Withdraw'\nbutton to convert it into items and transfer it to your personal inventory.").."]"
		.."field[6.1,0.325;1,1;withdrawamount;;]"
		.."field_close_on_enter[withdrawamount;false]"
		.."button[6.7,0;1.2,1;withdraw;"..S("Withdraw").."]"
		.."container_end[]"
		.."container[1,5.75]list[current_player;main;0,0;8,1;]"
		.."list[current_player;main;0,1.25;8,3;8]"

	if mcl_formspec_itemslots then
		formspec[#formspec+1] = mcl_formspec_itemslots(0,0,8,1) .. mcl_formspec_itemslots(0,1.25,8,3)
	end

	formspec[#formspec+1] =	"container_end[]"

	return table.concat(formspec)
end

-- Market formspec
--------------------------------------------------------------------------------------------------------

local compare_market_item = function(mkt1, mkt2)
	return mkt1.item < mkt2.item
end
local compare_market_desc = function(mkt1, mkt2)
	-- TODO: see https://github.com/minetest/minetest/issues/8398 for sorting localized strings
	return get_item_description(mkt1.item) < get_item_description(mkt2.item)
end
local compare_buy_volume = function(mkt1, mkt2)
	return mkt1.buy_volume > mkt2.buy_volume
end
local compare_buy_max = function(mkt1, mkt2)
	return ((mkt1.buy_orders[#mkt1.buy_orders] or {}).price or -2^30) > ((mkt2.buy_orders[#mkt2.buy_orders] or {}).price or -2^30)
end
local compare_sell_volume = function(mkt1, mkt2)
	return mkt1.sell_volume > mkt2.sell_volume
end
local compare_sell_min = function(mkt1, mkt2)
	return ((mkt1.sell_orders[#mkt1.sell_orders] or {}).price or 2^31) < ((mkt2.sell_orders[#mkt2.sell_orders] or {}).price or 2^31)
end
local compare_last_price = function(mkt1, mkt2)
	return (mkt1.last_price or 2^31) < (mkt2.last_price or 2^31)
end

local sort_marketlist = function(item_list, account)
	-- I think tonumber is now redundant here, leaving it in in case upgrading a world that has text recorded in this field for an existing player account
	local sort_by = tonumber(account.sort_markets_by_column)
	if sort_by == nil then return end
	local show_itemnames = account.show_itemnames == "true"
	local show_icons = global_enable_item_icons and ((account.show_icons or "true") == "true")

	local icon_displace = 0
	if show_icons then
		icon_displace = 1
	end
	local itemname_displace = 0
	if show_itemnames then
		itemname_displace = 1
	end

	--	"Icon,Item,Description,#00FF00,Buy Vol,Buy Max,#FF0000,Sell Vol,Sell Min,Last Price"
	if sort_by == 1 + icon_displace and show_itemnames then
		table.sort(item_list, compare_market_item)
	elseif sort_by == 1 + icon_displace + itemname_displace then
		table.sort(item_list, compare_market_desc)
	elseif sort_by == 3 + icon_displace + itemname_displace then
		table.sort(item_list, compare_buy_volume)
	elseif sort_by == 4 + icon_displace + itemname_displace then
		table.sort(item_list, compare_buy_max)
	elseif sort_by == 6 + icon_displace + itemname_displace then
		table.sort(item_list, compare_sell_volume)
	elseif sort_by == 7 + icon_displace + itemname_displace then
		table.sort(item_list, compare_sell_min)
	elseif sort_by == 8 + icon_displace + itemname_displace then
		table.sort(item_list, compare_last_price)
	elseif sort_by == 9 + icon_displace + itemname_displace then
		table.sort(item_list, function(mkt1, mkt2)
			-- Define locally so that account is available
			return (account.inventory[mkt1.item] or 0) > (account.inventory[mkt2.item] or 0)
		end)
	end
end

local make_marketlist = function(market, account)
	local market_list = {}
	local search_filter = account.search or ""
	for item, row in pairs(market.orders_for_items) do
		if (search_filter == "" or string.find(item, search_filter)) then
			if account.filter_participating == "true" then
				local found = false
				for _, order in ipairs(row.buy_orders) do
					if account == order.account then
						found = true
						break
					end
				end
				if not found then
					for _, order in ipairs(row.sell_orders) do
						if account == order.account then
							found = true
							break
						end
					end
				end
				if found then
					table.insert(market_list, row)
				end
			else
				table.insert(market_list, row)
			end
		end
	end
	sort_marketlist(market_list, account)
	return market_list
end

local get_account_name = function(target_account, this_account, anonymous)
	if anonymous and target_account ~= this_account then
		return ""
	end
	return target_account.name
end

local get_market_formspec = function(market, account)
	local market_def = market.def
	local selected = account.selected
	local market_list = make_marketlist(market, account)
	local show_itemnames = account.show_itemnames == "true"
	local show_icons = global_enable_item_icons and ((account.show_icons or "true") == "true")
	local anonymous = market_def.anonymous

	local formspec = {
		"size[10,10]"
		.."tabheader[0,0;tabs;"..market_def.description..","..S("Your Inventory")..","..S("Market Orders")..";3;false;true]"
	}

	-- column definitions
	formspec[#formspec+1] = "tablecolumns["
	if show_icons then
		formspec[#formspec+1] = "image" -- icon
		for i, row in ipairs(market_list) do
			formspec[#formspec+1] = "," .. i .. "=" .. get_icon(row.item)
		end
		formspec[#formspec+1] = ";"
	end	if show_itemnames then
		formspec[#formspec+1] = "text;" -- itemname
	end
	formspec[#formspec+1] = "text;" -- description
		.."color,span=2;"
		.."text,align=right,tooltip="..S("Number of items there's demand for in the market.")..";"
		.."text,align=right,tooltip="..S("Maximum price being offered to buy one of these.")..";"
		.."color,span=2;"
		.."text,align=right,tooltip="..S("Number of items available for sale in the market.")..";"
		.."text,align=right,tooltip="..S("Minimum price being demanded to sell one of these.")..";"
		.."text,align=right,tooltip="..S("Price paid for one of these the last time one was sold.")..";"
		.."text,align=right,tooltip="..S("Quantity of this item that you have in your inventory ready to sell.").."]"
		.."table[0,0;9.75,5;summary;"
	if show_icons then
		formspec[#formspec+1] = "0,"-- icon
	end

	-- header row
	if show_itemnames then
		formspec[#formspec+1] = "Item," -- itemname
	end
	formspec[#formspec+1] = S("Description")..",#00FF00,"..S("Buy Vol")..","..S("Buy Max")
		..",#FF0000,"..S("Sell Vol")..","..S("Sell Min")..","..S("Last Price")..","..S("Inventory")

	local selected_idx
	local selected_row

	-- Show list of item market summaries
	for i, row in ipairs(market_list) do
		if show_icons then
			formspec[#formspec+1] = ","..i -- icon
		end

		if show_itemnames then
			formspec[#formspec+1] = "," .. truncate_string(row.item, truncate_item_names_to)
		end

		formspec[#formspec+1] = "," .. get_item_description(row.item)
		.. ",#00FF00,"
		.. row.buy_volume
		.. "," .. ((row.buy_orders[#row.buy_orders] or {}).price or "-")
		.. ",#FF0000,"
		.. row.sell_volume
		.. "," .. ((row.sell_orders[#row.sell_orders] or {}).price or "-")
		.. "," .. (row.last_price or "-")
		.. "," .. (account.inventory[row.item] or "-")

		-- we happen to be processing the row that matches the item this player has selected. Record that.
		if selected == row.item then
			selected_row = row
			selected_idx = i + 1
		end

	end
	-- a row that's visible is marked as the selected item, so make it selected in the formspec
	if selected_row then
		formspec[#formspec+1] = ";"..selected_idx
	end
	formspec[#formspec+1] = "]"

	-- search field
	formspec[#formspec+1] =	"container[2.5,5]field_close_on_enter[search_filter;false]"
		.."field[0,0.85;2.5,1;search_filter;;"..minetest.formspec_escape(account.search or "").."]"
		.."image_button[2.05,0.65;0.8,0.8;commoditymarket_search.png;apply_search;]"
		.."image_button[2.7,0.65;0.8,0.8;commoditymarket_clear.png;clear_search;]"
		.."checkbox[1.77,0;filter_participating;"..S("My orders")..";".. account.filter_participating .."]"
		.."tooltip[filter_participating;"..S("Select this to show only the markets where you have either a buy or a sell order pending.").."]"
		.."tooltip[search_filter;"..S("Enter substring to search item identifiers for.").."]"
		.."tooltip[apply_search;"..S("Apply search to outputs.").."]"
		.."tooltip[clear_search;"..S("Clear search.").."]"
		.."container_end[]"

	-- if a visible item market is selected, show the orders for it in detail
	if selected_row then
		local current_time = minetest.get_gametime()

		local desc_display
		if show_itemnames then
			desc_display = selected
		else
			local def = minetest.registered_items[selected_row.item] or {description=S("Unknown Item")}
			desc_display = minetest.formspec_escape(def.description:gsub("\n", " "))
		end

		-- player inventory for this item and for currency
		formspec[#formspec+1] = "label[0.1,5.1;"..desc_display.."\n"..S("In inventory:").." "
			.. tostring(account.inventory[selected] or 0) .."\n"..S("Balance:").." "..market_def.currency_symbol..account.balance .."]"
		-- buy/sell controls
			.. "container[6.1,5]"
		local sell_limit = market_def.sell_limit
		if sell_limit then
			local total_sell = 0
			for item, orders in pairs(market.orders_for_items) do
				for _, order in ipairs(orders.sell_orders) do
					if order.account == account then
						total_sell = total_sell + order.quantity
					end
				end
			end
			formspec[#formspec+1] = "label[0,0;"..S("Sell limit:").." ".. total_sell .. "/" .. sell_limit .."]"
				.."tooltip[0,0;2,0.25;"..S("This market limits the total number of items a given seller can have for sale at a time.\nYou have @1 items remaining. Cancel old sell orders to free up space.", sell_limit-total_sell).."]"
		end
		-- Buy, sell, quantity and price button
		formspec[#formspec+1] = "tooltip[0,0.25;3.75,1;"..S("Use these fields to enter buy and sell orders for the selected item.").."]"
			.."button[0,0.55;1,1;buy;"..S("Buy").."]field[1.2,0.85;1,1;quantity;"..S("Quantity")..";]"
			.."field[2.1,0.85;1,1;price;"..S("Price per")..";]button[2.7,0.55;1,1;sell;Sell]"
			.."field_close_on_enter[quantity;false]field_close_on_enter[price;false]"
			.."container_end[]"
		-- table of buy and sell orders
			.."tablecolumns[color;text;"
			.."text,align=right,tooltip="..S("The price per item in this order.")..";"
			.."text,align=right,tooltip="..S("The total amount of items in this particular order.")..";"
			.."text,align=right,tooltip="..S("The total amount of items available at this price accounting for the other orders also currently being offered.")..";"
			.."text,tooltip="..S("The name of the player who placed this order.\nDouble-click your own orders to cancel them.")..";"
			.."text,align=right,tooltip="..S("How many days ago this order was placed.").."]"
			.."table[0,6.5;9.75,3.5;orders;#FFFFFF,"..S("Order")..","..S("Price")..","..S("Quantity")..","..S("Total Volume")..","..S("Player")..","..S("Days Old")

		local sell_volume = selected_row.sell_volume
		for i, sell in ipairs(selected_row.sell_orders) do
			formspec[#formspec+1] = ",#FF0000,"..S("Sell")..","
				..sell.price..","
				..sell.quantity..","
				..sell_volume..","
				..get_account_name(sell.account, account, anonymous)..","
				..math.floor((current_time-sell.timestamp)/86400)
			sell_volume = sell_volume - sell.quantity
		end
		local buy_volume = 0
		local buy_orders = selected_row.buy_orders
		local buy_count = #buy_orders
		-- Show buy orders in reverse order
		for i = buy_count, 1, -1  do
			local buy = buy_orders[i]
			buy_volume = buy_volume + buy.quantity
			formspec[#formspec+1] = ",#00FF00,"..S("Buy")..","
				..buy.price..","
				..buy.quantity..","
				..buy_volume..","
				..get_account_name(buy.account, account, anonymous)..","
				..math.floor((current_time-buy.timestamp)/86400)
		end
		formspec[#formspec+1] = "]"
	else
		formspec[#formspec+1] = "label[0.1,5.1;"..S("Select an item to view or place orders.").."]"
	end

	return table.concat(formspec)
end

-------------------------------------------------------------------------------------
-- Information formspec

--{item=item, quantity=quantity, price=price, purchaser=purchaser, seller=seller, timestamp = minetest.get_gametime()}
local log_to_string = function(market, log_entry, account)
	local anonymous = market.def.anonymous
	local purchaser = log_entry.purchaser
	local seller = log_entry.seller
	local purchaser_name
	if purchaser == seller then
		purchaser_name = S("yourself")
	elseif anonymous and purchaser ~= account then
		purchaser_name = S("someone")
	elseif purchaser == account then
		purchaser_name = S("you")
	else
		purchaser_name = purchaser.name
	end
	local seller_name
	if anonymous and seller ~= account then
		seller_name = S("someone")
	elseif seller == account then
		seller_name = S("you")
	else
		seller_name = seller.name
	end
	local colour
	local new
	local last_acknowledged = account.last_acknowledged or 0
	if log_entry.timestamp > last_acknowledged then
		colour = "#FFFF00"
		new = true
	else
		colour = "#FFFFFF"
		new = false
	end
	local show_itemnames = account.show_itemnames == "true"
	local itemname = log_entry.item
	if not show_itemnames then
		local item_def = minetest.registered_items[log_entry.item]
		if item_def then
			itemname = minetest.formspec_escape(item_def.description:gsub("\n", " "))
		end
	end

	local currency_symbol = market.def.currency_symbol
	return colour .. S("On day @1 @2 sold @3 @4 to @5 at @6@7 each for a total of @8@9.",
			math.ceil(log_entry.timestamp/86400), seller_name, log_entry.quantity, itemname,
			purchaser_name, currency_symbol, log_entry.price, currency_symbol, log_entry.quantity*log_entry.price), new
end

local get_info_formspec = function(market, account)
	local formspec = {
		"size[10,10]"
		.."tabheader[0,0;tabs;"..market.def.description..","..S("Your Inventory")..","..S("Market Orders")..";1;false;true]"
		.."textarea[0.75,0.5;9.25,1.5;;"..S("Description:")..";"..market.def.long_description.."]"
		.."label[0.5,2.2;"..S("Your Recent Purchases and Sales:").."]"
		.."textlist[0.5,2.6;8.75,4;log_entries;"
	}
	if next(account.log) then
		local new = false
		for _, log_entry in ipairs(account.log) do
			local log_string, new_log = log_to_string(market, log_entry, account)
			new = new or new_log
			formspec[#formspec+1] = log_string
			formspec[#formspec+1] = ","
		end
		formspec[#formspec] = "]" -- Note: there's no +1 here deliberately, that way the "]" overwrites the last comma added by the loop above.
		if new then
			formspec[#formspec+1] = "button[7.1,6.9;2,0.5;acknowledge_log;"..S("Mark logs as read").."]" ..
			"tooltip[acknowledge_log;"..S("Log entries in yellow are new since last time you marked your log as read.").."]"
		end
	else
		formspec[#formspec+1] = "#CCCCCC"..S("No logged activities in this market yet.").."]"
	end
	local show_itemnames = account.show_itemnames or "false"

	formspec[#formspec+1] = "]container[0.5, 7.5]label[0,0;Settings:]checkbox[0,0.25;show_itemnames;"..S("Show Itemnames")..";"
		..show_itemnames.."]"
	if global_enable_item_icons then
		local show_icons = account.show_icons or "true"
		formspec[#formspec+1] = "checkbox[2,0.25;show_icons;"..S("Show Icons")..";"..show_icons.."]"
	end
	formspec[#formspec+1] = "container_end[]"

	return table.concat(formspec)
end

---------------------------------------------------------------------------------------

commoditymarket.get_formspec = function(market, account)
	local tab = account.tab
	if tab == 1 then
		return get_info_formspec(market, account)
	elseif tab == 2 then
		return get_account_formspec(market, account)
	else
		return get_market_formspec(market, account)
	end
end

------------------------------------------------------------------------------------
-- Handling recieve_fields

local add_to_player_inventory = function(name, item, amount)
	local playerinv = minetest.get_inventory({type="player", name=name})
	local not_full = true
	while amount > 0 and not_full do
		local stack = ItemStack(item .. " " .. amount)
		amount = amount - stack:get_count()
		local leftover = playerinv:add_item("main", stack)
		if leftover:get_count() > 0 then
			amount = amount + leftover:get_count()
			return amount
		end
	end
	return amount
end

minetest.register_on_player_receive_fields(function(player, formname, fields)
	local formname_split = formname:split(":")

	if formname_split[1] ~= "commoditymarket" then
		return false
	end
	local market = commoditymarket.registered_markets[formname_split[2]]
	if not market then
		return false
	end

	local name = formname_split[3]
	if name ~= player:get_player_name() then
		return false
	end

	local account = market:get_account(name)
	local show_icons = global_enable_item_icons and ((account.show_icons or "true") == "true")

	local something_changed = false
	if fields.tabs then
		account.tab = tonumber(fields.tabs)
		something_changed = true
	end
	-- player clicked on an item in the market summary table
	if fields.summary then
		local summaryevent = minetest.explode_table_event(fields.summary)
		if summaryevent.type == "DCL" or summaryevent.type == "CHG" then
			if summaryevent.row == 1 then
				-- header clicked, sort by column
				local column = tonumber(summaryevent.column)
				if not (column == 1 and show_icons) then -- ignore clicks on the icon column header
					account.sort_markets_by_column = column
				end
			else
				-- item clicked, recreate the list to find out which one
				local marketlist = make_marketlist(market, account)
				local selected = marketlist[summaryevent.row-1]
				if selected then
					account.selected = selected.item
				end
			end
		elseif summaryevent.type == "INV" then
			account.selected = nil
		end
		something_changed = true
	end
	if fields.orders then
		local ordersevent = minetest.explode_table_event(fields.orders)
		if ordersevent.type == "DCL" and ordersevent.column > 0 then
			local selected_idx = ordersevent.row - 1 -- account for header
			local selected_row = market.orders_for_items[account.selected] -- sell orders come first
			local sell_orders = selected_row.sell_orders
			local sell_order_count = #sell_orders
			local selected_order
			if selected_idx <= sell_order_count then -- if the index is within the range of sell orders,
				selected_order = sell_orders[selected_idx]
				if selected_order and selected_order.account == account then -- and the order belongs to the current player,
					market:cancel_sell(account.selected, selected_order) -- cancel it
					something_changed = true
				end
			else
				-- otherwise we're in the buy group, shift the index up by sell_order_count and reverse index order
				local buy_orders = selected_row.buy_orders
				local buy_orders_count = #buy_orders
				selected_order = buy_orders[buy_orders_count - (selected_idx - sell_order_count - 1)]
				if selected_order and selected_order.account == account then
					market:cancel_buy(account.selected, selected_order)
					something_changed = true
				end
			end
		end
	end

	if fields.buy then
		local quantity = tonumber(fields.quantity)
		local price = tonumber(fields.price)
		if price ~= nil and quantity ~= nil then
			market:buy(name, account.selected, quantity, price)
			something_changed = true
		end
	end
	if fields.sell then
		local quantity = tonumber(fields.quantity)
		local price = tonumber(fields.price)
		if price ~= nil and quantity ~= nil then
			market:sell(name, account.selected, quantity, price)
			something_changed = true
		end
	end

	-- player clicked in their inventory table, may need to give him his stuff back
	if fields.inventory then
		local invevent = minetest.explode_table_event(fields.inventory)
		if invevent.type == "DCL" and invevent.column > 0 then
			local col_count = 8
			local show_itemnames = account.show_itemnames == "true"
			if not show_itemnames then
				col_count = col_count - 2
			end
			if not show_icons then
				col_count = col_count - 2
			end
			local index = math.floor(((invevent.row-1)*col_count + invevent.column - 1)/(col_count/2)) - 1
			local account = market:get_account(name)
			-- build a local copy of the inventory that would be displayed in the formspec so we can
			-- figure out what item the index we were given is pointing to
			local inventory = {}
			for item, quantity in pairs(account.inventory) do
				table.insert(inventory, {item=item, quantity=quantity, description=get_item_description(item)})
			end
			if show_itemnames then
				table.sort(inventory, inventory_item_comp)
			else
				table.sort(inventory, inventory_desc_comp)
			end
			if inventory[index] then
				local item = inventory[index].item
				local amount = account.inventory[item]
				local remaining = add_to_player_inventory(name, item, amount)
				if remaining == 0 then
					account.inventory[item] = nil
				else
					account.inventory[item] = remaining
				end
				if remaining ~= amount then
					something_changed = true
				end
			end
		end
	end

	if fields.withdraw or fields.key_enter_field == "withdrawamount" then
		local withdrawvalue = tonumber(fields.withdrawamount)
		if withdrawvalue then
			local account = market:get_account(name)
			withdrawvalue = math.min(withdrawvalue, account.balance)
			for _, currency in ipairs(market.def.currency_ordered) do
				this_unit_amount = math.floor(withdrawvalue/currency.amount)
				if this_unit_amount > 0 then
					local remaining = add_to_player_inventory(name, currency.item, this_unit_amount)
					local value_given = (this_unit_amount - remaining) * currency.amount
					account.balance = account.balance - value_given
					withdrawvalue = withdrawvalue - value_given
					something_changed = true
				end
			end
		end
	end

	if fields.search_filter then
		local value = string.lower(fields.search_filter)
		if account.search ~= value then
			account.search = value
		end
	end

	local process_checkbox = function(property_name, fields, account)
		if (fields[property_name] == "true" and account[property_name] ~= "true") or
			(fields[property_name] == "false" and account[property_name] ~= "false") then
			account[property_name] = fields[property_name]
			return true
		end
		return false
	end

	if process_checkbox("filter_participating", fields, account) then something_changed = true end
	if process_checkbox("show_itemnames", fields, account) then something_changed = true end
	if process_checkbox("show_icons", fields, account) then something_changed = true end

	if fields.acknowledge_log then
		account.last_acknowledged = minetest.get_gametime()
		something_changed = true
	end

	if fields.apply_search or fields.key_enter_field == "search_filter" then
		something_changed = true
	end

	if fields.clear_search then
		account.search = ""
		something_changed = true
	end
	if something_changed then
		minetest.show_formspec(name, formname, market:get_formspec(account))
	end
end)


code:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Script to generate the template file and update the translation files.
# Copy the script into the mod or modpack root folder and run it there.
#
# Copyright (C) 2019 Joachim Stolberg, 2020 FaceDeer, 2020 Louis Royer
# LGPLv2.1+
#
# See https://github.com/minetest-tools/update_translations for
# potential future updates to this script.

from __future__ import print_function
import os, fnmatch, re, shutil, errno
from sys import argv as _argv

# Running params
params = {"recursive": False,
    "help": False,
    "mods": False,
    "verbose": False,
    "folders": []
}
# Available CLI options
options = {"recursive": ['--recursive', '-r'],
    "help": ['--help', '-h'],
    "mods": ['--installed-mods'],
    "verbose": ['--verbose', '-v']
}

# Strings longer than this will have extra space added between
# them in the translation files to make it easier to distinguish their
# beginnings and endings at a glance
doublespace_threshold = 60

def set_params_folders(tab: list):
    '''Initialize params["folders"] from CLI arguments.'''
    # Discarding argument 0 (tool name)
    for param in tab[1:]:
        stop_param = False
        for option in options:
            if param in options[option]:
                stop_param = True
                break
        if not stop_param:
            params["folders"].append(os.path.abspath(param))

def set_params(tab: list):
    '''Initialize params from CLI arguments.'''
    for option in options:
        for option_name in options[option]:
            if option_name in tab:
                params[option] = True
                break

def print_help(name):
    '''Prints some help message.'''
    print(f'''SYNOPSIS
    {name} [OPTIONS] [PATHS...]
DESCRIPTION
    {', '.join(options["help"])}
        prints this help message
    {', '.join(options["recursive"])}
        run on all subfolders of paths given
    {', '.join(options["mods"])}
        run on locally installed modules
    {', '.join(options["verbose"])}
        add output information
''')

def main():
    '''Main function'''
    set_params(_argv)
    set_params_folders(_argv)
    if params["help"]:
        print_help(_argv[0])
    elif params["recursive"] and params["mods"]:
        print("Option --installed-mods is incompatible with --recursive")
    else:
        # Add recursivity message
        print("Running ", end='')
        if params["recursive"]:
            print("recursively ", end='')
        # Running
        if params["mods"]:
            print(f"on all locally installed modules in {os.path.abspath('~/.minetest/mods/')}")
            run_all_subfolders("~/.minetest/mods")
        elif len(params["folders"]) >= 2:
            print("on folder list:", params["folders"])
            for f in params["folders"]:
                if params["recursive"]:
                    run_all_subfolders(f)
                else:
                    update_folder(f)
        elif len(params["folders"]) == 1:
            print("on folder", params["folders"][0])
            if params["recursive"]:
                run_all_subfolders(params["folders"][0])
            else:
                update_folder(params["folders"][0])
        else:
            print("on folder", os.path.abspath("./"))
            if params["recursive"]:
                run_all_subfolders(os.path.abspath("./"))
            else:
                update_folder(os.path.abspath("./"))

#group 2 will be the string, groups 1 and 3 will be the delimiters (" or ')
#See https://stackoverflow.com/questions/46967465/regex-match-text-in-either-single-or-double-quote
pattern_lua = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*(["\'])((?:\\\1|(?:(?!\1)).)*)(\1)[\s,\)]', re.DOTALL)
pattern_lua_bracketed = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*\[\[(.*?)\]\][\s,\)]', re.DOTALL)

# Handles "concatenation" .. " of strings"
pattern_concat = re.compile(r'["\'][\s]*\.\.[\s]*["\']', re.DOTALL)

pattern_tr = re.compile(r'(.+?[^@])=(.*)')
pattern_name = re.compile(r'^name[ ]*=[ ]*([^ \n]*)')
pattern_tr_filename = re.compile(r'\.tr$')
pattern_po_language_code = re.compile(r'(.*)\.po$')

#attempt to read the mod's name from the mod.conf file. Returns None on failure
def get_modname(folder):
    try:
        with open(os.path.join(folder, "mod.conf"), "r", encoding='utf-8') as mod_conf:
            for line in mod_conf:
                match = pattern_name.match(line)
                if match:
                    return match.group(1)
    except FileNotFoundError:
        pass
    return None

#If there are already .tr files in /locale, returns a list of their names
def get_existing_tr_files(folder):
    out = []
    for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
        for name in files:
            if pattern_tr_filename.search(name):
                out.append(name)
    return out

# A series of search and replaces that massage a .po file's contents into
# a .tr file's equivalent
def process_po_file(text):
    # The first three items are for unused matches
    text = re.sub(r'#~ msgid "', "", text)
    text = re.sub(r'"\n#~ msgstr ""\n"', "=", text)
    text = re.sub(r'"\n#~ msgstr "', "=", text)
    # comment lines
    text = re.sub(r'#.*\n', "", text)
    # converting msg pairs into "=" pairs
    text = re.sub(r'msgid "', "", text)
    text = re.sub(r'"\nmsgstr ""\n"', "=", text)
    text = re.sub(r'"\nmsgstr "', "=", text)
    # various line breaks and escape codes
    text = re.sub(r'"\n"', "", text)
    text = re.sub(r'"\n', "\n", text)
    text = re.sub(r'\\"', '"', text)
    text = re.sub(r'\\n', '@n', text)
    # remove header text
    text = re.sub(r'=Project-Id-Version:.*\n', "", text)
    # remove double-spaced lines
    text = re.sub(r'\n\n', '\n', text)
    return text

# Go through existing .po files and, if a .tr file for that language
# *doesn't* exist, convert it and create it.
# The .tr file that results will subsequently be reprocessed so
# any "no longer used" strings will be preserved.
# Note that "fuzzy" tags will be lost in this process.
def process_po_files(folder, modname):
    for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
        for name in files:
            code_match = pattern_po_language_code.match(name)
            if code_match == None:
                continue
            language_code = code_match.group(1)
            tr_name = modname + "." + language_code + ".tr"
            tr_file = os.path.join(root, tr_name)
            if os.path.exists(tr_file):
                if params["verbose"]:
                    print(f"{tr_name} already exists, ignoring {name}")
                continue
            fname = os.path.join(root, name)
            with open(fname, "r", encoding='utf-8') as po_file:
                if params["verbose"]:
                    print(f"Importing translations from {name}")
                text = process_po_file(po_file.read())
                with open(tr_file, "wt", encoding='utf-8') as tr_out:
                    tr_out.write(text)

# from https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python/600612#600612
# Creates a directory if it doesn't exist, silently does
# nothing if it already exists
def mkdir_p(path):
    try:
        os.makedirs(path)
    except OSError as exc: # Python >2.5
        if exc.errno == errno.EEXIST and os.path.isdir(path):
            pass
        else: raise

# Converts the template dictionary to a text to be written as a file
# dKeyStrings is a dictionary of localized string to source file sets
# dOld is a dictionary of existing translations and comments from
# the previous version of this text
def strings_to_text(dkeyStrings, dOld, mod_name):
    lOut = [f"# textdomain: {mod_name}\n"]

    dGroupedBySource = {}

    for key in dkeyStrings:
        sourceList = list(dkeyStrings[key])
        sourceList.sort()
        sourceString = "\n".join(sourceList)
        listForSource = dGroupedBySource.get(sourceString, [])
        listForSource.append(key)
        dGroupedBySource[sourceString] = listForSource

    lSourceKeys = list(dGroupedBySource.keys())
    lSourceKeys.sort()
    for source in lSourceKeys:
        localizedStrings = dGroupedBySource[source]
        localizedStrings.sort()
        lOut.append("")
        lOut.append(source)
        lOut.append("")
        for localizedString in localizedStrings:
            val = dOld.get(localizedString, {})
            translation = val.get("translation", "")
            comment = val.get("comment")
            if len(localizedString) > doublespace_threshold and not lOut[-1] == "":
                lOut.append("")
            if comment != None:
                lOut.append(comment)
            lOut.append(f"{localizedString}={translation}")
            if len(localizedString) > doublespace_threshold:
                lOut.append("")

    unusedExist = False
    for key in dOld:
        if key not in dkeyStrings:
            val = dOld[key]
            translation = val.get("translation")
            comment = val.get("comment")
            # only keep an unused translation if there was translated
            # text or a comment associated with it
            if translation != None and (translation != "" or comment):
                if not unusedExist:
                    unusedExist = True
                    lOut.append("\n\n##### not used anymore #####\n")
                if len(key) > doublespace_threshold and not lOut[-1] == "":
                    lOut.append("")
                if comment != None:
                    lOut.append(comment)
                lOut.append(f"{key}={translation}")
                if len(key) > doublespace_threshold:
                    lOut.append("")
    return "\n".join(lOut) + '\n'

# Writes a template.txt file
# dkeyStrings is the dictionary returned by generate_template
def write_template(templ_file, dkeyStrings, mod_name):
    # read existing template file to preserve comments
    existing_template = import_tr_file(templ_file)

    text = strings_to_text(dkeyStrings, existing_template[0], mod_name)
    mkdir_p(os.path.dirname(templ_file))
    with open(templ_file, "wt", encoding='utf-8') as template_file:
        template_file.write(text)

# Gets all translatable strings from a lua file
def read_lua_file_strings(lua_file):
    lOut = []
    with open(lua_file, encoding='utf-8') as text_file:
        text = text_file.read()
        #TODO remove comments here

        text = re.sub(pattern_concat, "", text)

        strings = []
        for s in pattern_lua.findall(text):
            strings.append(s[1])
        for s in pattern_lua_bracketed.findall(text):
            strings.append(s)

        for s in strings:
            s = re.sub(r'"\.\.\s+"', "", s)
            s = re.sub("@[^@=0-9]", "@@", s)
            s = s.replace('\\"', '"')
            s = s.replace("\\'", "'")
            s = s.replace("\n", "@n")
            s = s.replace("\\n", "@n")
            s = s.replace("=", "@=")
            lOut.append(s)
    return lOut

# Gets strings from an existing translation file
# returns both a dictionary of translations
# and the full original source text so that the new text
# can be compared to it for changes.
def import_tr_file(tr_file):
    dOut = {}
    text = None
    if os.path.exists(tr_file):
        with open(tr_file, "r", encoding='utf-8') as existing_file :
            # save the full text to allow for comparison
            # of the old version with the new output
            text = existing_file.read()
            existing_file.seek(0)
            # a running record of the current comment block
            # we're inside, to allow preceeding multi-line comments
            # to be retained for a translation line
            latest_comment_block = None
            for line in existing_file.readlines():
                line = line.rstrip('\n')
                if line[:3] == "###":
                    # Reset comment block if we hit a header
                    latest_comment_block = None
                    continue
                if line[:1] == "#":
                    # Save the comment we're inside
                    if not latest_comment_block:
                        latest_comment_block = line
                    else:
                        latest_comment_block = latest_comment_block + "\n" + line
                    continue
                match = pattern_tr.match(line)
                if match:
                    # this line is a translated line
                    outval = {}
                    outval["translation"] = match.group(2)
                    if latest_comment_block:
                        # if there was a comment, record that.
                        outval["comment"] = latest_comment_block
                    latest_comment_block = None
                    dOut[match.group(1)] = outval
    return (dOut, text)

# Walks all lua files in the mod folder, collects translatable strings,
# and writes it to a template.txt file
# Returns a dictionary of localized strings to source file sets
# that can be used with the strings_to_text function.
def generate_template(folder, mod_name):
    dOut = {}
    for root, dirs, files in os.walk(folder):
        for name in files:
            if fnmatch.fnmatch(name, "*.lua"):
                fname = os.path.join(root, name)
                found = read_lua_file_strings(fname)
                if params["verbose"]:
                    print(f"{fname}: {str(len(found))} translatable strings")

                for s in found:
                    sources = dOut.get(s, set())
                    sources.add(f"### {os.path.basename(fname)} ###")
                    dOut[s] = sources

    if len(dOut) == 0:
        return None
    templ_file = os.path.join(folder, "locale/template.txt")
    write_template(templ_file, dOut, mod_name)
    return dOut

# Updates an existing .tr file, copying the old one to a ".old" file
# if any changes have happened
# dNew is the data used to generate the template, it has all the
# currently-existing localized strings
def update_tr_file(dNew, mod_name, tr_file):
    if params["verbose"]:
        print(f"updating {tr_file}")

    tr_import = import_tr_file(tr_file)
    dOld = tr_import[0]
    textOld = tr_import[1]

    textNew = strings_to_text(dNew, dOld, mod_name)

    if textOld and textOld != textNew:
        print(f"{tr_file} has changed.")
        shutil.copyfile(tr_file, f"{tr_file}.old")

    with open(tr_file, "w", encoding='utf-8') as new_tr_file:
        new_tr_file.write(textNew)

# Updates translation files for the mod in the given folder
def update_mod(folder):
    modname = get_modname(folder)
    if modname is not None:
        process_po_files(folder, modname)
        print(f"Updating translations for {modname}")
        data = generate_template(folder, modname)
        if data == None:
            print(f"No translatable strings found in {modname}")
        else:
            for tr_file in get_existing_tr_files(folder):
                update_tr_file(data, modname, os.path.join(folder, "locale/", tr_file))
    else:
        print("Unable to find modname in folder " + folder)

# Determines if the folder being pointed to is a mod or a mod pack
# and then runs update_mod accordingly
def update_folder(folder):
    is_modpack = os.path.exists(os.path.join(folder, "modpack.txt")) or os.path.exists(os.path.join(folder, "modpack.conf"))
    if is_modpack:
        subfolders = [f.path for f in os.scandir(folder) if f.is_dir()]
        for subfolder in subfolders:
            update_mod(subfolder + "/")
    else:
        update_mod(folder)
    print("Done.")

def run_all_subfolders(folder):
    for modfolder in [f.path for f in os.scandir(folder) if f.is_dir()]:
        update_folder(modfolder + "/")

main()


code:
commoditymarket = {}

local MP = minetest.get_modpath(minetest.get_current_modname())
dofile(MP.."/formspecs.lua")
dofile(MP.."/market.lua")
dofile(MP.."/doc.lua")


code:
local S = minetest.get_translator(minetest.get_current_modname())

commoditymarket.registered_markets = {}
local log_length_limit = 30

-- from http://lua-users.org/wiki/BinaryInsert
--[[
   table.bininsert( table, value [, comp] )

   Inserts a given value through BinaryInsert into the table sorted by [, comp].

   If 'comp' is given, then it must be a function that receives
   two table elements, and returns true when the first is less
   than the second, e.g. comp = function(a, b) return a > b end,
   will give a sorted table, with the biggest value on position 1.
   [, comp] behaves as in table.sort(table, value [, comp])
   returns the index where 'value' was inserted
]]--
local comp_default = function(a, b) return a < b end
function table.bininsert(t, value, comp)
	-- Initialise compare function
	local comp = comp or comp_default
	--  Initialise numbers
	local iStart, iEnd, iMid, iState = 1, #t, 1, 0
	-- Get insert position
	while iStart <= iEnd do
		-- calculate middle
		iMid = math.floor( (iStart+iEnd)/2 )
		-- compare
		if comp(value, t[iMid]) then
			iEnd, iState = iMid - 1, 0
		else
			iStart, iState = iMid + 1, 1
		end
	end
	local target = iMid+iState
	table.insert(t, target, value)
	return target
end

-- lowest price first
local buy_comp = function(order1, order2)
	local price1 = order1.price
	local price2 = order2.price
	if price1 < price2 then
		return true
	elseif price1 == price2 and order1.timestamp < order2.timestamp then
		return true
	end
	return false
end
-- highest price first
local sell_comp = function(order1, order2)
	local price1 = order1.price
	local price2 = order2.price
	if price1 > price2 then
		return true
	elseif price1 == price2 and order1.timestamp < order2.timestamp then
		return true
	end
	return false
end

---------------------------------

local get_account = function(market, player_name)
	local account = market.player_accounts[player_name]
	if account then
		return account
	end
	account = {}
	account.search = ""
	account.name = player_name
	account.balance = 0 -- currency
	account.inventory = {} -- items stored in the market inventory that aren't part of sell orders yet. stored as "[item] = count"
	account.filter_participating = "false"
	account.log = {} -- might want to use a more sophisticated queue, but this isn't going to be a big list so that's more trouble than it's worth right now.
	market.player_accounts[player_name] = account
	return account
end

-- Caution: the data structures produced by sale logging caused me to discover
-- issue https://github.com/minetest/minetest/issues/8719 with minetest.serialize()
-- I'm working around it by using the code in persistence.lua instead
local log_sale = function(item, quantity, price, purchaser, seller)
	local log_entry = {item=item, quantity=quantity, price=price, purchaser=purchaser, seller=seller, timestamp = minetest.get_gametime()}
	local purchaser_log = purchaser.log
	local seller_log = seller.log
	table.insert(purchaser_log, log_entry)
	if #purchaser_log > log_length_limit then
		table.remove(purchaser_log, 1)
	end
	if (purchaser ~= seller) then
		table.insert(seller_log, log_entry)
		if #seller_log > log_length_limit then
			table.remove(seller_log, 1)
		end
	end
end

local remove_orders_by_account = function(orders, account)
	if not orders then return end
	local i = 1
	while i < #orders do
		local order = orders[i]
		if order.account == account then
			table.remove(orders, i)
		else
			i = i + 1
		end
	end
end

local remove_account = function(player_name)
	local account = player_accounts[player_name]
	if account == nil then
		return
	end

	player_accounts[player_name] = nil
	for item, lists in pairs(market) do
		remove_orders_by_account(lists.buy_orders, account)
		remove_orders_by_account(lists.sell_orders, account)
	end
end

------------------------------------------------------------------------------------------

local add_inventory_to_account = function(market, account, item, quantity)
	if quantity < 1 then
		return false
	end

	if market.def.currency[item] then
		account.balance = account.balance + market.def.currency[item] * quantity
	else
		account.inventory[item] = (account.inventory[item] or 0) + quantity
	end
	return true
end

local remove_inventory_from_account = function(account, item, quantity)
	if quantity < 1 then
		return false
	end

	local inventory = account.inventory
	local current_quantity = inventory[item] or 0
	if current_quantity < quantity then
		return false
	end

	local new_quantity = current_quantity - quantity
	if new_quantity == 0 then
		inventory[item] = nil
	else
		inventory[item] = new_quantity
	end
	return true
end

local remove_order = function(order, array)
	for i, market_order in ipairs(array) do
		if order == market_order then
			table.remove(array, i)
			return true
		end
	end
	return false
end

-----------------------------------------------------------------------------------------------------------

local add_sell = function(market, account, item, price, quantity)
	price = tonumber(price)
	quantity = tonumber(quantity)

	local sell_limit = market.def.sell_limit
	local sell_limit_exceeded
	if sell_limit then
		local total_sell = 0
		for item, orders in pairs(market.orders_for_items) do
			for _, order in ipairs(orders.sell_orders) do
				if order.account == account then
					total_sell = total_sell + order.quantity
				end
			end
		end
		sell_limit_exceeded = total_sell + quantity > sell_limit
	end

	-- validate that this sell order is possible
	if sell_limit_exceeded or price < 0 or quantity < 1 or not remove_inventory_from_account(account, item, quantity) then
		minetest.sound_play({name = "commoditymarket_error", gain = 0.1}, {to_player=account.name})
		if sell_limit_exceeded then
			minetest.chat_send_player(account.name, S("You have too many items listed for sale in this market, please cancel some sell orders to make room for new ones."))
		elseif price < 0 then
			minetest.chat_send_player(account.name, S("You can't sell items for a negative price."))
		elseif quantity < 1 then
			minetest.chat_send_player(account.name, S("You can't sell fewer than one item."))
		else
			minetest.chat_send_player(account.name, S("You don't have enough of that item in your inventory to post this sell order."))
		end
		return false
	end

	local buy_market = market.orders_for_items[item].buy_orders
	local buy_order = buy_market[#buy_market]
	local current_buy_volume = market.orders_for_items[item].buy_volume

	-- go through existing buy orders that are more expensive than or equal to the price
	-- we're demanding, selling them at the order's price until we run out of
	-- buy orders or run out of demand
	while quantity > 0 and buy_order and buy_order.price >= price do
		local quantity_to_sell = math.min(buy_order.quantity, quantity)
		quantity = quantity - quantity_to_sell
		local earned = quantity_to_sell*buy_order.price
		account.balance = account.balance + earned
		add_inventory_to_account(market, buy_order.account, item, quantity_to_sell)
		buy_order.quantity = buy_order.quantity - quantity_to_sell
		current_buy_volume = current_buy_volume - quantity_to_sell

		if buy_order.account ~= account then
			-- don't update the last price if a player is just buying and selling from themselves
			market.orders_for_items[item].last_price = buy_order.price
		end

		log_sale(item, quantity_to_sell, buy_order.price, buy_order.account, account)

		if buy_order.quantity == 0 then
			table.remove(buy_market, #buy_market)
		end
		buy_order = buy_market[#buy_market]
	end
	market.orders_for_items[item].buy_volume = current_buy_volume

	if quantity > 0 then
		local sell_market = market.orders_for_items[item].sell_orders

		-- create the order and insert it into order arrays
		local order = {account=account, price=price, quantity=quantity, timestamp=minetest.get_gametime()}
		table.bininsert(sell_market, order, sell_comp)
		market.orders_for_items[item].sell_volume = market.orders_for_items[item].sell_volume + quantity
	end

	minetest.sound_play({name = "commoditymarket_register_opened", gain = 0.1}, {to_player=account.name})
	return true
end

local cancel_sell = function(market, item, order)
	local account = order.account
	local quantity = order.quantity

	local sell_market = market.orders_for_items[item].sell_orders

	remove_order(order, sell_market)
	market.orders_for_items[item].sell_volume = market.orders_for_items[item].sell_volume - quantity
	add_inventory_to_account(market, account, item, quantity)

	minetest.sound_play({name = "commoditymarket_register_closed", gain = 0.1}, {to_player=account.name})
end

-----------------------------------------------------------------------------------------------------------

local test_buy = function(market, balance, item, price, quantity)
	local sell_market = market.orders_for_items[item].sell_orders
	local test_quantity = quantity
	local test_balance = balance
	local i = 0
	local sell_order = sell_market[#sell_market]
	while test_quantity > 0 and sell_order and sell_order.price <= price do
		local quantity_to_buy = math.min(sell_order.quantity, test_quantity)
		test_quantity = test_quantity - quantity_to_buy
		test_balance = test_balance - quantity_to_buy*sell_order.price
		i = i + 1
		sell_order = sell_market[#sell_market-i]
	end
	local spent = balance - test_balance
	test_balance = test_balance - test_quantity*price
	if test_balance < 0 then
		return false, spent, test_quantity
	end

	return true, spent, test_quantity
end

local add_buy = function(market, account, item, price, quantity)
	price = tonumber(price)
	quantity = tonumber(quantity)
	if price < 0 or quantity < 1 or not test_buy(market, account.balance, item, price, quantity) then
		minetest.sound_play({name = "commoditymarket_error", gain = 0.1}, {to_player=account.name})
		if price < 0 then
			minetest.chat_send_player(account.name, S("You can't pay less than nothing for an item."))
		elseif quantity < 1 then
			minetest.chat_send_player(account.name, S("You have to buy at least one item."))
		else
			minetest.chat_send_player(account.name, S("You can't afford that many of this item."))
		end
		return false
	end

	local sell_market = market.orders_for_items[item].sell_orders
	local sell_order = sell_market[#sell_market]
	local current_sell_volume = market.orders_for_items[item].sell_volume

	-- go through existing sell orders that are cheaper than or equal to the price
	-- we're wanting to offer, buying them up at the offered price until we run out of
	-- sell orders or run out of supply
	while quantity > 0 and sell_order and sell_order.price <= price do
		local quantity_to_buy = math.min(sell_order.quantity, quantity)
		quantity = quantity - quantity_to_buy
		local spent = quantity_to_buy*sell_order.price
		account.balance = account.balance - spent
		sell_order.account.balance = sell_order.account.balance + spent
		sell_order.quantity = sell_order.quantity - quantity_to_buy
		current_sell_volume = current_sell_volume - quantity_to_buy
		add_inventory_to_account(market, account, item, quantity_to_buy)

		if sell_order.account ~= account then
			-- don't update the last price if a player is just buying and selling from themselves
			market.orders_for_items[item].last_price = sell_order.price
		end

		log_sale(item, quantity_to_buy, sell_order.price, account, sell_order.account)

		-- Sell order completely used up, remove it
		if sell_order.quantity == 0 then
			table.remove(sell_market, #sell_market)
		end

		-- get the next sell order
		sell_order = sell_market[#sell_market]
	end
	market.orders_for_items[item].sell_volume = current_sell_volume

	if quantity > 0 then
		local buy_market = market.orders_for_items[item].buy_orders
		-- create the order for the remainder and insert it into order arrays
		local order = {account=account, price=price, quantity=quantity, timestamp=minetest.get_gametime()}
		account.balance = account.balance - quantity*price -- buy orders are pre-paid
		table.bininsert(buy_market, order, buy_comp)
		market.orders_for_items[item].buy_volume = market.orders_for_items[item].buy_volume + quantity
	end

	minetest.sound_play({name = "commoditymarket_register_opened", gain = 0.1}, {to_player=account.name})
	return true
end

local cancel_buy = function(market, item, order)
	local account = order.account
	local quantity = order.quantity
	local price = order.price

	local buy_market = market.orders_for_items[item].buy_orders
	market.orders_for_items[item].buy_volume = market.orders_for_items[item].buy_volume - quantity

	remove_order(order, buy_market)

	account.balance = account.balance + price*quantity

	minetest.sound_play({name = "commoditymarket_register_closed", gain = 0.1}, {to_player=account.name})
end

local initialize_market_item = function(orders_for_items, item)
	if orders_for_items[item] == nil then
		local lists = {}
		lists.buy_orders = {}
		lists.sell_orders = {}
		lists.buy_volume = 0
		lists.sell_volume = 0
		lists.item = item
		-- leave last_price nil to indicate it's never been sold before
		orders_for_items[item] = lists
	end
end

-----------------------------------------------------------------------------------------------------------
-- Chat commands

minetest.register_chatcommand("market.show", {
	params = "marketname",
	privs = {server=true},
	description = S("show market interface"),
	func = function(name, param)
		local market = commoditymarket.registered_markets[param]
		if market == nil then return end
		local formspec = market:get_formspec(market:get_account(name))
		minetest.show_formspec(name, "commoditymarket:"..param..":"..name, formspec)
	end,
})

minetest.register_chatcommand("market.list", {
	params = "",
	privs = {server=true},
	description = S("list all registered markets"),
	func = function(name, param)
		local list = {}
		for marketname, def in pairs(commoditymarket.registered_markets) do
			table.insert(list, marketname)
		end
		table.sort(list)
		minetest.chat_send_player(name, "Registered markets: " .. table.concat(list, ", "))
	end,
})

local remove_market_item = function(market, item)
	local marketitem = market.orders_for_items[item]
	if marketitem then
		local buy_orders = marketitem.buy_orders
		while #buy_orders > 0 do
			market:cancel_buy(item, buy_orders[#buy_orders])
		end
		local sell_orders = marketitem.sell_orders
		while #sell_orders > 0 do
			market:cancel_sell(item, sell_orders[#sell_orders])
		end
		market.orders_for_items[item] = nil
	end
end

minetest.register_chatcommand("market.removeitem", {
	params = "marketname item",
	privs = {server=true},
	description = S("remove item from market. All existing buys and sells will be canceled."),
	func = function(name, param)
		local params = param:split(" ")
		if #params ~= 2 then
			minetest.chat_send_player(name, "Incorrect parameter count")
			return
		end
		local market = commoditymarket.registered_markets[params[1]]
		if market == nil then
			minetest.chat_send_player(name, "No such market: " .. params[1])
			return
		end
		remove_market_item(market, params[2])
	end,
})

minetest.register_chatcommand("market.purge_unknowns", {
	params = "",
	privs = {server=true},
	description = S("removes all unknown items from all markets. All existing buys and sells for those items will be canceled."),
	func = function(name, param)
		for market_name, market in pairs(commoditymarket.registered_markets) do
			local items_to_remove = {}
			local items_to_move = {}
			for item, orders in pairs(market.orders_for_items) do
				local icon = commoditymarket.get_icon(item)
				if icon == "unknown_item.png" then
					table.insert(items_to_remove, item)
				end
			end
			for _, item in ipairs(items_to_remove) do
				minetest.chat_send_player(name, S("Purging item: @1 from market: @2", tostring(item), market_name))
				minetest.log("warning", "[commoditymarket] Purging unknown item: " .. tostring(item) .. " from market: " .. market_name)
				remove_market_item(market, item)
			end
		end
	end,
})

-- Used during development and debugging to find items that break the market formspecs when added
local debugging_commands = false
if debugging_commands then
	minetest.register_chatcommand("market.addeverything", {
		params = "marketname",
		privs = {server=true},
		description = S("Add all registered items to the provided market"),
		func = function(name, param)
			local params = param:split(" ")
			if #params ~= 1 then
				minetest.chat_send_player(name, "Incorrect parameter count")
				return
			end
			local market = commoditymarket.registered_markets[params[1]]
			if market == nil then
				minetest.chat_send_player(name, "No such market: " .. params[1])
				return
			end
			for item_name, def in pairs(minetest.registered_items) do
				initialize_market_item(market.orders_for_items, item_name)
			end
		end,
	})
end

-----------------------------------------------------------------------------------------------------------

-- API exposed to the outside world
local add_inventory = function(self, player_name, item, quantity)
	return add_inventory_to_account(self, get_account(self, player_name), item, quantity)
end
local remove_inventory = function(self, player_name, item, quantity)
	return remove_inventory_from_account(get_account(self, player_name), item, quantity)
end
local sell = function(self, player_name, item, quantity, price)
	return add_sell(self, get_account(self, player_name), item, price, quantity)
end
local buy = function(self, player_name, item, quantity, price)
	return add_buy(self, get_account(self, player_name), item, price, quantity)
end

-- Using this instead of minetest.serialize because of https://github.com/minetest/minetest/issues/8719
local MP = minetest.get_modpath(minetest.get_current_modname())
local persistence_store, persistence_load = dofile(MP.."/persistence.lua")

local worldpath = minetest.get_worldpath()
local load_market_data = function(marketname)
	local filename = worldpath .. "/market_"..marketname..".lua"
	return persistence_load(filename)
end

local save_market_data = function(market)
	local filename = worldpath .. "/market_"..market.name..".lua"
	local data = {}
	data.player_accounts = market.player_accounts
	data.orders_for_items = market.orders_for_items
	persistence_store(filename, data)
	return true
end

local make_doc_entry = function() return end
if minetest.get_modpath("doc") then
	make_doc_entry = function(market_name, market_def)
		local currencies = {}
		for _, currency_item in ipairs(market_def.currency_ordered) do
			local item_def = minetest.registered_items[currency_item.item]
			assert(item_def, "Failed to find item definition for currency item " .. currency_item.item
				.. ". Make sure the item has been registered before registering a market that references it as currency.")
			table.insert(currencies, S("1 @1 = @2@3", item_def.description, market_def.currency_symbol, currency_item.amount))
		end
		local inventory_limit
		if market_def.inventory_limit then
			inventory_limit = S("Market inventory is limited to @1 items.", market_def.inventory_limit)
		else
			inventory_limit = S("Market has unlimited inventory space.")
		end

		local sell_limit
		if market_def.sell_limit then
			sell_limit = S("Total pending sell orders are limited to @1 items.", market_def.inventory_limit)
		else
			sell_limit = S("Market supports unlimited pending sell orders.")
		end

		doc.add_entry("commoditymarket", "market_"..market_name, {
			name = market_def.description,
			data = { text = market_def.long_description
				.."\n\n"
				..S("Currency item values:") .. "\n    " .. table.concat(currencies, "\n    ")
				.."\n\n"
				..inventory_limit
				.."\n"
				..sell_limit
			}
		})
	end
end

commoditymarket.register_market = function(market_name, market_def)
	assert(not commoditymarket.registered_markets[market_name])
	assert(market_def.currency)

	market_def.currency_symbol = market_def.currency_symbol or "¤" -- \u{00A4} -- defaults to the generic currency symbol ("scarab")
	market_def.description = market_def.description or S("Market")
	market_def.long_description = market_def.long_description or S("A market where orders to buy or sell items can be placed and fulfilled.")

	-- Reprocess currency table into a form easier for the withdraw code to work with
	market_def.currency_ordered = {}
	for item, amount in pairs(market_def.currency) do
		table.insert(market_def.currency_ordered, {item=item, amount=amount})
	end
	table.sort(market_def.currency_ordered, function(currency1, currency2) return currency1.amount > currency2.amount end)

	make_doc_entry(market_name, market_def) -- market_def has now been normalized, make documentation for it if doc is installed.

	-- Just in case a developer supplied strings that don't work well in formspecs, escape them now so we don't have to do it
	-- wherever they're used.
	market_def.currency_symbol = minetest.formspec_escape(market_def.currency_symbol)
	market_def.description = minetest.formspec_escape(market_def.description)
	market_def.long_description = minetest.formspec_escape(market_def.long_description)

	local new_market = {}
	new_market.def = market_def
	commoditymarket.registered_markets[market_name] = new_market

	local loaded_data = load_market_data(market_name)
	if loaded_data then
		new_market.player_accounts = loaded_data.player_accounts
		new_market.orders_for_items = loaded_data.orders_for_items
	else
		new_market.player_accounts = {}
		new_market.orders_for_items = {}
	end

	-- If there's a list of initial items in the market def, initialize them. allow_item can trump this.
	local initial_items = market_def.initial_items
	if initial_items then
		-- defer until after to ensure that all initial items have been registered, so we can guard against invalid items
		minetest.after(0,
			function()
				for _, item in ipairs(initial_items) do
					if minetest.registered_items[item] and
						((not market_def.allow_item) or market_def.allow_item(item)) and
						not market_def.currency[item] then
						initialize_market_item(new_market.orders_for_items, item)
					end
				end
			end)
	end
	market_def.initial_items = nil -- don't need this any more

	new_market.name = market_name

	new_market.add_inventory = add_inventory
	new_market.remove_inventory = remove_inventory
	new_market.sell = sell
	new_market.buy = buy
	new_market.cancel_sell = cancel_sell
	new_market.cancel_buy = cancel_buy
	new_market.get_formspec = commoditymarket.get_formspec
	new_market.get_account = get_account
	new_market.save = save_market_data

	-- save markets on shutdown
	minetest.register_on_shutdown(function() new_market:save() end)

	-- and also every ten minutes, to be on the safe side in case Minetest crashes
	-- TODO: a more sophisticated approach that checks whether the market data is "dirty" before actually saving
	local until_next_save = 600
	minetest.register_globalstep(function(dtime)
		until_next_save = until_next_save - dtime
		if until_next_save < 0 then
			new_market:save()
			until_next_save = 600
		end
	end)

	----------------------------------------------------------------------
	-- Detached inventory for adding items into the market

	local inv = minetest.create_detached_inventory("commoditymarket:"..market_name, {
		allow_move = function(inv, from_list, from_index, to_list, to_index, count, player)
			return 0
		end,
		allow_put = function(inv, listname, index, stack, player)
			local item = stack:get_name()

			-- reject unknown items
			if minetest.registered_items[item] == nil then
				return 0
			end

			-- Currency items are always allowed
			if new_market.def.currency[item] then
				return stack:get_count()
			end

			-- only new tools, no used tools
			if stack:get_wear() ~= 0 then
				return 0
			end

			--nothing with metadata permitted
			local meta = stack:get_meta():to_table()
			local fields = meta.fields
			local inventory = meta.inventory
			if (fields and next(fields)) or (inventory and next(inventory)) then
				return 0
			end

			-- If there's no allow_item function defined, allow everything. Otherwise check if the item is allowed
			if (not market_def.allow_item) or market_def.allow_item(item) then
				local allowed_count = stack:get_count()

				if market_def.inventory_limit then
					-- limit additions to the inventory_limit, if there is one
					local current_count = 0
					for _, inventory_quantity in pairs(new_market:get_account(player:get_player_name()).inventory) do
						current_count = current_count + inventory_quantity
					end
					allowed_count = math.min(allowed_count, allowed_count + market_def.inventory_limit - (current_count+allowed_count))
					if allowed_count <= 0 then return 0 end
				end

				--ensures the item is in the market listing if it wasn't before
				initialize_market_item(new_market.orders_for_items, item)
				return allowed_count
			end
			return 0
		end,
		allow_take = function(inv, listname, index, stack, player)
			return 0
		end,
		on_move = function(inv, from_list, from_index, to_list, to_index, count, player)
		end,
		on_take = function(inv, listname, index, stack, player)
		end,
		on_put = function(inv, listname, index, stack, player)
			if listname == "add" then
				local item = stack:get_name()
				local count = stack:get_count()
				new_market:add_inventory(player:get_player_name(), item, count)
				inv:set_list("add", {})
				local name = player:get_player_name()
				local formspec = new_market:get_formspec(new_market:get_account(name))
				minetest.show_formspec(name, "commoditymarket:"..market_name..":"..name, formspec)
			end
		end
	})
	inv:set_size("add", 1)
end

commoditymarket.show_market = function(market_name, player_name)
	local market = commoditymarket.registered_markets[market_name]
	if market == nil then return end
	local formspec = market:get_formspec(market:get_account(player_name))
	minetest.show_formspec(player_name, "commoditymarket:"..market_name..":"..player_name, formspec)
end


code:
name = commoditymarket
description = Provides API support for various in-world commodity markets
optional_depends = doc, mcl_formspec


code:
-- Internal persistence library

--[[ Provides ]]
-- persistence.store(path, ...): Stores arbitrary items to the file at the given path
-- persistence.load(path): Loads files that were previously stored with store and returns them

--[[ Limitations ]]
-- Does not export userdata, threads or most function values
-- Function export is not portable

--[[ License: MIT (see bottom) ]]

-- Private methods
local write, writeIndent, writers, refCount;

-- write thing (dispatcher)
write = function (file, item, level, objRefNames)
	writers[type(item)](file, item, level, objRefNames);
end;

-- write indent
writeIndent = function (file, level)
	for i = 1, level do
		file:write("\t");
	end;
end;

-- recursively count references
refCount = function (objRefCount, item)
	-- only count reference types (tables)
	if type(item) == "table" then
		-- Increase ref count
		if objRefCount[item] then
			objRefCount[item] = objRefCount[item] + 1;
		else
			objRefCount[item] = 1;
			-- If first encounter, traverse
			for k, v in pairs(item) do
				refCount(objRefCount, k);
				refCount(objRefCount, v);
			end;
		end;
	end;
end;

-- Format items for the purpose of restoring
writers = {
	["nil"] = function (file, item)
			file:write("nil");
		end;
	["number"] = function (file, item)
			file:write(tostring(item));
		end;
	["string"] = function (file, item)
			file:write(string.format("%q", item));
		end;
	["boolean"] = function (file, item)
			if item then
				file:write("true");
			else
				file:write("false");
			end
		end;
	["table"] = function (file, item, level, objRefNames)
			local refIdx = objRefNames[item];
			if refIdx then
				-- Table with multiple references
				file:write("multiRefObjects["..refIdx.."]");
			else
				-- Single use table
				file:write("{\n");
				for k, v in pairs(item) do
					writeIndent(file, level+1);
					file:write("[");
					write(file, k, level+1, objRefNames);
					file:write("] = ");
					write(file, v, level+1, objRefNames);
					file:write(";\n");
				end
				writeIndent(file, level);
				file:write("}");
			end;
		end;
	["function"] = function (file, item)
			-- Does only work for "normal" functions, not those
			-- with upvalues or c functions
			local dInfo = debug.getinfo(item, "uS");
			if dInfo.nups > 0 then
				file:write("nil --[[functions with upvalue not supported]]");
			elseif dInfo.what ~= "Lua" then
				file:write("nil --[[non-lua function not supported]]");
			else
				local r, s = pcall(string.dump,item);
				if r then
					file:write(string.format("loadstring(%q)", s));
				else
					file:write("nil --[[function could not be dumped]]");
				end
			end
		end;
	["thread"] = function (file, item)
			file:write("nil --[[thread]]\n");
		end;
	["userdata"] = function (file, item)
			file:write("nil --[[userdata]]\n");
		end;
}

return function (path, ...)
	local file, e;
	if type(path) == "string" then
		-- Path, open a file
		file, e = io.open(path, "w");
		if not file then
			return error(e);
		end
	else
		-- Just treat it as file
		file = path;
	end
	local n = select("#", ...);
	-- Count references
	local objRefCount = {}; -- Stores reference that will be exported
	for i = 1, n do
		refCount(objRefCount, (select(i,...)));
	end;
	-- Export Objects with more than one ref and assign name
	-- First, create empty tables for each
	local objRefNames = {};
	local objRefIdx = 0;
	file:write("-- Persistent Data\n");
	file:write("local multiRefObjects = {\n");
	for obj, count in pairs(objRefCount) do
		if count > 1 then
			objRefIdx = objRefIdx + 1;
			objRefNames[obj] = objRefIdx;
			file:write("{};"); -- table objRefIdx
		end;
	end;
	file:write("\n} -- multiRefObjects\n");
	-- Then fill them (this requires all empty multiRefObjects to exist)
	for obj, idx in pairs(objRefNames) do
		for k, v in pairs(obj) do
			file:write("multiRefObjects["..idx.."][");
			write(file, k, 0, objRefNames);
			file:write("] = ");
			write(file, v, 0, objRefNames);
			file:write(";\n");
		end;
	end;
	-- Create the remaining objects
	for i = 1, n do
		file:write("local ".."obj"..i.." = ");
		write(file, (select(i,...)), 0, objRefNames);
		file:write("\n");
	end
	-- Return them
	if n > 0 then
		file:write("return obj1");
		for i = 2, n do
			file:write(" ,obj"..i);
		end;
		file:write("\n");
	else
		file:write("return\n");
	end;
	file:close();
end, function (path)
	local f, e = loadfile(path);
	if f then
		return f();
	else
		return nil, e;
	end;
end

--[[
 Copyright (c) 2010 Gerhard Roethlin

 Permission is hereby granted, free of charge, to any person
 obtaining a copy of this software and associated documentation
 files (the "Software"), to deal in the Software without
 restriction, including without limitation the rights to use,
 copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the
 Software is furnished to do so, subject to the following
 conditions:

 The above copyright notice and this permission notice shall be
 included in all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 OTHER DEALINGS IN THE SOFTWARE.
]]


code:
This mod implements marketplaces where players can post buy and sell offers for various items, allowing for organic market forces to determine the relative values of the resources in a world.

The basic market interface is the same across all markets and market types, but this mod allows for a variety of different ways that markets can be configured to support different playstyles. Markets can have restrictions on what they will allow to be bought and sold, different types of "currency", and can share a common inventory across multiple locations or can be localized to just one spot at the discretion of the server owner.

![](screenshot.png)

## Currency

Each market has one or more "currency" items defined that are treated differently from the other items that can be bought and sold there. Currency items are translated into a player's currency balance rather than being bought and sold directly.

For example, some of the markets offered by the "commoditymarket_fantasy" mod have this currency definition:

	{
		["default:gold_ingot"] = 1000,
		["commoditymarket_fantasy:gold_coin"] = 1
	}

When a gold ingot is added to the player's market account it turns into 1000 units of currency. When a gold coin is added it turns into 1 unit of currency. You can't buy and sell gold directly in this market, it is instead the "standard" by which the value of other items is measured.

There's no reason that all markets in a given world have to use the same currency. Having variety in currency types adds flavour to the world and also introduces opportunities for enterprising traders to make a profit by moneychanging between different marketplaces.

## Account Inventory

In addition to tracking a player's currency balance, each player's account has an inventory that serves as a holding area for items that are destined to be sold or that have been bought by the player but not yet retrieved. This inventory is a bit different from the standard Minetest inventory in that it doesn't hold individual item "stacks", allowing for larger quantities of items to be accumulated than would otherwise be practical. If a player needs to buy 20,000 stone bricks for a major construction project then their account's inventory will hold that.

To prevent abuse of the market inventory as a free storage space, or just to add some unique flavor to a particular market, a limit on the inventory's size can be added. This limit only affects transfers from a player's personal inventory into the market inventory; the limit can be exceeded by incoming items being sold to the player.

Note that tools cannot be added to the market inventory if they have any wear on them, nor can the market handle items with attached metadata (such as books that have had text added to them).

## Placing a "Buy" Order

A buy order is an offer to give a certain amount of currency in exchange for a particular type of item. To place a buy order go to the "Market Orders" tab of the market's interface and select the item from the list of items on the market. If the item isn't listed it may be that the market is simply "unaware" of the item's existence; try placing an example of the item into your personal inventory and if the item is permitted on the market a new entry will be added to Market Orders.

Enter the quantity and price you desire and then click the "buy" button to place a buy order.

If there are already "sell" orders for the item when you place a buy order, some or all of your buy order might be immediately fulfilled provided you are offering a sufficient price. Your purchases will be made at the price that the sell orders have been set to - if you were willing to pay 15 units of currency per item but someone was already offering to sell for 2 units of currency per item, you only pay 2 units for each of that offer's items.

If there aren't enough compatible sell orders to fulfill your buy order, the remainder will be placed into the market and made available for future sellers to see and fulfill if they agree to your price. Your buy order will immediately deduct the currency required for it from your account's balance, but if you cancel your order you will get that currency back - it's not gone until the order is actually fulfilled.

Double-click on your order in the orders list to cancel it.

## Placing a "Sell" Order

Sell orders are an offer of a certain amount of an item and a price you're willing to accept in exchange for them. They're placed in a similar manner to buy orders, except by clicking the "sell" button instead of the "buy" button.

If there are already buyers with buy orders that meet or exceed your price, some or all of your sell order may be immediately fulfilled. You'll be paid the price that the buyers are offering rather than the amount you're demanding.

If any of your sell offer is left unfulfilled, the sell order will be added to the market for future buyers to see. The items for this offer will be immediately taken from your market inventory but if you cancel your order you will get those items back.

Double-click on your order in the orders list to cancel it.

## Commands

This mod has several commands that a server administrator can use:

* `market.removeitem marketname item` -- cancels all existing buy and sell orders for an item and removes its entry from the market tab. This is useful if you've changed what items are permitted in a particular market and need to clear out items that are no longer allowed.
* `market.show marketname` -- opens the market's formspec
* `market.list` -- lists the marketnames of all registered markets
* `market.purge_unknowns` -- executes "removeitem" for all markets on all items that don't have a definition. Useful for clearing out items that are no longer defined due to a mod being updated or removed.
* `market.addeverything marketname` - Adds all registered items to a market's listings. NOTE: this is intended as a debugging tool, not for use in a live server, as it doesn't filter out items that a player cannot actually harvest in-world.

## Registering a market

The mod "[commoditymarket_fantasy](https://github.com/FaceDeer/commoditymarket_fantasy)" contains a number of pre-defined markets that provide examples of what's possible with this mod. They include:

* King's Market - a basic sort of "commoner's marketplace", only open during the day
* Night Market - the shadier side of commerce, only open during the night
* Trader's Caravan - a type of market that players can build and place themselves, with a small inventory capacity.
* Goblin Exchange - a strange marketplace that uses coal as a currency
* Undermarket - where dark powers make their trades, using Mese as a currency

All of these except for the Trader's Caravan are intended to be placed in specific locations by server administrators or mapgen, they don't have crafting recipes. Modifying these markets or creating your own from scratch should hopefully be a fairly straightforward task.

### Market definition API

```
local market_def = {
	description = "Night Market", -- A short name for this market, appears as the text of the "info" tab of the market's UI
	long_description = "When the sun sets and the stalls of the King's Market close, other vendors are just waking up to share their wares. The Night Market is not as voluminous as the King's Market but accepts a wider range of wares. It accepts the same gold coinage of the realm, one thousand coins to the gold ingot.", -- A longer description with flavor text and other information to present to the user, shown in the info tab. Optional.
	currency = {
		["default:gold_ingot"] = 1000,
		["commoditymarket:gold_coins"] = 1
	}, -- List all items that get translated into "currency" here, along with their conversion rates. Take care to ensure there's no way for a player to multiply their money when crafting currency items into each other (eg, if there was some way to get more than 1000 coin items out of a gold ingot, in this case)
	currency_symbol = "☼", -- Used in various places in the UI. If not defined, defaults to "¤" (the generic currency symbol)
	inventory_limit = 10000, -- Optional, when set this prevents the player from adding items to their market inventory when it's over this limit
	sell_limit = 10000, -- Optional, when set this prevents sell orders from being added if the player already has this many items for sale
	initial_items = {"default:cobble", "default:wood"}, -- Optional, a list of items that the market will be initialized with on startup. Players can add other items during play.
	allow_item = function(item) return true end, -- Optional, this function is used to determine whether the market permits a player to add a particular item to its inventory.
	anonymous = true, -- If set to true then the player won't be able to see the names associated with other player's orders, only their own.
}

commoditymarket.register_market("market_name", market_def)
```

Once a market is defined, use `commoditymarket.show_market(market_name, player_name)` to show the market interface to a player.


code:
#Some item images are very large and break the market formspec.
#See https://github.com/minetest/minetest/issues/9300
#Alternately, use:
#commoditymarket.override_item_icon(item_name, new_icon_texture)
#or
#commoditymarket.override_image_icon(old_icon_texture, new_icon_texture)
#to override a troublesome image directly.
commoditymarket_enable_item_icons (Enable item icon images in market formspecs) bool true