Initial commit

This commit is contained in:
FaceDeer 2019-07-27 15:12:52 -06:00
commit eb10b9c9b7
24 changed files with 1426 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# Compiled Lua sources
luac.out
# luarocks build files
*.src.rock
*.zip
*.tar.gz
# Object files
*.o
*.os
*.ko
*.obj
*.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

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 FaceDeer
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.

229
default_markets.lua Normal file
View File

@ -0,0 +1,229 @@
local default_modpath = minetest.get_modpath("default")
if not default_modpath then return end
local gold_coins_required = false
local default_items = {"default:axe_bronze","default:axe_diamond","default:axe_mese","default:axe_steel","default:axe_steel","default:axe_stone","default:axe_wood","default:pick_bronze","default:pick_diamond","default:pick_mese","default:pick_steel","default:pick_stone","default:pick_wood","default:shovel_bronze","default:shovel_diamond","default:shovel_mese","default:shovel_steel","default:shovel_stone","default:shovel_wood","default:sword_bronze","default:sword_diamond","default:sword_mese","default:sword_steel","default:sword_stone","default:sword_wood", "default:blueberries", "default:book", "default:bronze_ingot", "default:clay_brick", "default:clay_lump", "default:coal_lump", "default:copper_ingot", "default:copper_lump", "default:diamond", "default:flint", "default:gold_ingot", "default:gold_lump", "default:iron_lump", "default:mese_crystal", "default:mese_crystal_fragment", "default:obsidian_shard", "default:paper", "default:steel_ingot", "default:stick", "default:tin_ingot", "default:tin_lump", "default:acacia_tree", "default:acacia_wood", "default:apple", "default:aspen_tree", "default:aspen_wood", "default:blueberry_bush_sapling", "default:bookshelf", "default:brick", "default:bronzeblock", "default:bush_sapling", "default:cactus", "default:clay", "default:coalblock", "default:cobble", "default:copperblock", "default:desert_cobble", "default:desert_sand", "default:desert_sandstone", "default:desert_sandstone_block", "default:desert_sandstone_brick", "default:desert_stone", "default:desert_stone_block", "default:desert_stonebrick", "default:diamondblock", "default:dirt", "default:glass", "default:goldblock", "default:gravel", "default:ice", "default:junglegrass", "default:junglesapling", "default:jungletree", "default:junglewood", "default:ladder_steel", "default:ladder_wood", "default:large_cactus_seedling", "default:mese", "default:mese_post_light", "default:meselamp", "default:mossycobble", "default:obsidian", "default:obsidian_block", "default:obsidian_glass", "default:obsidianbrick", "default:papyrus", "default:pine_sapling", "default:pine_tree", "default:pine_wood", "default:sand", "default:sandstone", "default:sandstone_block", "default:sandstonebrick", "default:sapling", "default:silver_sand", "default:silver_sandstone", "default:silver_sandstone_block", "default:silver_sandstone_brick", "default:snow", "default:snowblock", "default:steelblock", "default:stone", "default:stone_block", "default:stonebrick", "default:tinblock", "default:tree", "default:wood",}
------------------------------------------------------------------------------
if minetest.settings:get_bool("commoditymarket_enable_kings_market") then
local kings_def = {
description = "King's Market",
long_description = "The largest and most accessible market for the common man, the King's Market uses gold coins as its medium of exchange (or the equivalent in gold ingots - 1000 coins to the ingot). However, as a respectable institution of the surface world, the King's Market operates only during the hours of daylight. The purchase and sale of swords and explosives is prohibited in the King's Market.",
currency = {
["default:gold_ingot"] = 1000,
["commoditymarket:gold_coins"] = 1
},
currency_symbol = "Au",
allow_item = function(item)
if item:sub(1,13) == "default:sword" or item:sub(1,4) == "tnt:" then
return false
end
return true
end,
inventory_limit = 100000,
initial_items = default_items,
}
gold_coins_required = true
commoditymarket.register_market("kings", kings_def)
minetest.register_node("commoditymarket:kings_market", {
description = "King's Market",
_doc_items_longdesc = "",
tiles = {"default_chest_top.png","default_chest_top.png",
"default_chest_side.png","default_chest_side.png",
"commoditymarket_empty_shelf.png","default_chest_side.png^commoditymarket_crown.png",},
paramtype2 = "facedir",
is_ground_content = false,
groups = {choppy = 2, oddly_breakable_by_hand = 1,},
sounds = default.node_sound_wood_defaults(),
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
local timeofday = minetest.get_timeofday()
if timeofday > 0.2 and timeofday < 0.8 then
commoditymarket.show_market("kings", clicker:get_player_name())
else
minetest.chat_send_player(clicker:get_player_name(), "At this time of day the King's Market is closed.")
end
end,
can_dig = function(pos, player)
if player and minetest.check_player_privs(player, "server") then
return true
end
return false
end,
})
end
-------------------------------------------------------------------------------
if minetest.settings:get_bool("commoditymarket_enable_night_market") then
local night_def = {
description = "Night Market",
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.",
currency = {
["default:gold_ingot"] = 1000,
["commoditymarket:gold_coins"] = 1
},
currency_symbol = "Au",
inventory_limit = 10000,
initial_items = default_items,
}
gold_coins_required = true
commoditymarket.register_market("night", night_def)
minetest.register_node("commoditymarket:night_market", {
description = night_def.description,
_doc_items_longdesc = night_def.long_description,
tiles = {"default_chest_top.png","default_chest_top.png",
"default_chest_side.png","default_chest_side.png",
"commoditymarket_empty_shelf.png","default_chest_side.png^commoditymarket_moon.png",},
paramtype2 = "facedir",
is_ground_content = false,
groups = {choppy = 2, oddly_breakable_by_hand = 1,},
sounds = default.node_sound_wood_defaults(),
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
local timeofday = minetest.get_timeofday()
if timeofday < 0.2 or timeofday > 0.8 then
commoditymarket.show_market("night", clicker:get_player_name())
else
minetest.chat_send_player(clicker:get_player_name(), "At this time of day the Night Market is closed.")
end
end,
can_dig = function(pos, player)
if player and minetest.check_player_privs(player, "server") then
return true
end
return false
end,
})
end
-------------------------------------------------------------------------------
if minetest.settings:get_bool("commoditymarket_enable_caravan_market", true) then
-- "Trader's Caravan" - small-capacity market that players can build
local caravan_def = {
description = "Trader's Caravan",
long_description = "Unlike most markets that have well-known fixed locations that travelers congregate to, the network of Trader's Caravans is fluid and dynamic in their locations. A Trader's Caravan can show up anywhere, make modest trades, and then be gone the next time you visit them. These caravans accept gold and gold coins as a currency.",
currency = {
["default:gold_ingot"] = 1000,
["commoditymarket:gold_coins"] = 1
},
currency_symbol = "Au",
inventory_limit = 1000,
initial_items = default_items,
}
gold_coins_required = true
minetest.register_craft({
output = "commoditymarket:caravan_market",
recipe = {
{'', "default:gold_ingot", ''},
{'group:wood', "default:chest_locked", 'group:wood'},
{'group:wood', 'group:wood', 'group:wood'},
}
})
commoditymarket.register_market("caravan", caravan_def)
minetest.register_node("commoditymarket:caravan_market", {
description = "Trader's Caravan",
_doc_items_longdesc = caravan_def.long_description,
tiles = {"default_chest_top.png","default_chest_top.png",
"default_chest_side.png^commoditymarket_caravan.png","default_chest_side.png^commoditymarket_caravan.png",
"commoditymarket_empty_shelf.png","default_chest_side.png^commoditymarket_trade.png",},
paramtype2 = "facedir",
is_ground_content = false,
groups = {choppy = 2, oddly_breakable_by_hand = 1,},
sounds = default.node_sound_wood_defaults(),
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
commoditymarket.show_market("caravan", clicker:get_player_name())
end,
})
end
-------------------------------------------------------------------------------
-- "Goblin Exchange"
if minetest.settings:get_bool("commoditymarket_enable_goblin_market") then
local goblin_def = {
description = "Goblin Exchange",
long_description = "One does not usually associate Goblins with the sort of sophistication that running a market requires. Usually one just associates Goblins with savagery and violence. But they understand the principle of tit-for-tat exchange, and if approached correctly they actually respect the concepts of ownership and debt. However, for some peculiar reason they understand this concept in the context of coal lumps. Goblins deal in the standard coal lump as their form of currency, conceptually divided into 100 coal centilumps (though Goblin brokers prefer to \"keep the change\" when giving back actual coal lumps).",
currency = {
["default:coal_lump"] = 100
},
currency_symbol = "cC",
inventory_limit = 1000,
}
commoditymarket.register_market("goblin", goblin_def)
minetest.register_node("commoditymarket:goblin_market", {
description = goblin_def.description,
_doc_items_longdesc = goblin_def.long_description,
tiles = {"default_chest_top.png","default_chest_top.png",
"default_chest_side.png","default_chest_side.png",
"commoditymarket_empty_shelf.png","default_chest_side.png^commoditymarket_goblin.png",},
paramtype2 = "facedir",
is_ground_content = false,
groups = {choppy = 2, oddly_breakable_by_hand = 1,},
sounds = default.node_sound_wood_defaults(),
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
commoditymarket.show_market("goblin", clicker:get_player_name())
end,
can_dig = function(pos, player)
if player and minetest.check_player_privs(player, "server") then
return true
end
return false
end,
})
end
--------------------------------------------------------------------------------
if minetest.settings:get_bool("commoditymarket_enable_under_market") then
local undermarket_def = {
description = "Undermarket",
long_description = "Deep in the bowels of the world, below even the goblin-infested warrens and ancient delvings of the dwarves, dark and mysterious beings once dwelled. A few still linger to this day, and facilitate barter for those brave souls willing to travel in their lost realms. The Undermarket uses Mese chips as a currency - twenty chips to the Mese fragment. Though traders are loathe to physically break Mese crystals up into units that small, as it renders it useless for other purposes.",
currency = {
["default:mese"] = 9*9*20,
["default:mese_crystal"] = 9*20,
["default:mese_crystal_fragment"] = 20
},
currency_symbol = "\u{20A5}",
inventory_limit = 10000,
}
commoditymarket.register_market("under", undermarket_def)
minetest.register_node("commoditymarket:under_market", {
description = undermarket_def.description,
_doc_items_longdesc = undermarket_def.long_description,
tiles = {"commoditymarket_under_top.png","commoditymarket_under_top.png",
"commoditymarket_under.png","commoditymarket_under.png","commoditymarket_under.png","commoditymarket_under.png"},
paramtype2 = "facedir",
is_ground_content = false,
groups = {choppy = 2, oddly_breakable_by_hand = 1,},
sounds = default.node_sound_stone_defaults(),
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
commoditymarket.show_market("under", clicker:get_player_name())
end,
can_dig = function(pos, player)
if player and minetest.check_player_privs(player, "server") then
return true
end
return false
end,
})
end
------------------------------------------------------------------
if gold_coins_required then
minetest.register_craftitem("commoditymarket:gold_coins", {
description = "Gold Coins",
inventory_image = "commoditymarket_gold_coins.png",
stack_max = 1000,
})
end

1
depends.txt Normal file
View File

@ -0,0 +1 @@
default?

460
formspecs.lua Normal file
View File

@ -0,0 +1,460 @@
-- Inventory formspec
-------------------------------------------------------------------------------------
local inventory_comp = function(invitem1, invitem2) return invitem1.item < invitem2.item end
local get_account_formspec = function(market, account)
local formspec = {
"size[10,10]",
"tabheader[0,0;tabs;"..market.def.description..",Your Inventory,Market Orders;2;false;true]",
"tablecolumns[text;text,align=center;text;text,align=center]",
"table[0,0;9.9,4;inventory;",
"Item,Quantity,Item,Quantity"
}
local inventory = {}
local inventory_count = 0
for item, quantity in pairs(account.inventory) do
table.insert(inventory, {item=item, quantity=quantity})
inventory_count = inventory_count + quantity
end
table.sort(inventory, inventory_comp)
local i = 1
while i <= #inventory do
local n = #formspec+1
formspec[n] = "," .. inventory[i].item
formspec[n+1] = "," .. inventory[i].quantity
i = i + 1
if inventory[i] then
formspec[n+2] = "," .. inventory[i].item
formspec[n+3] = "," .. inventory[i].quantity
end
i = i + 1
end
formspec[#formspec+1] = "]container[1.1,4.5]list[detached:commoditymarket:"..market.name..";add;0,0;1,1;]"
.."label[1,0;Drop items here to\nadd to your account]"
.."listring[current_player;main]listring[detached:commoditymarket:"..market.name..";add]"
if market.def.inventory_limit then
formspec[#formspec+1] = "label[3,0;Inventory limit:\n"..inventory_count.."/"..market.def.inventory_limit.."]"
end
formspec[#formspec+1] = "label[4.9,0;Balance:\n"..market.def.currency_symbol .. account.balance .."]"
.."field[6.1,0.25;1,1;withdrawamount;;]"
.."field_close_on_enter[withdrawamount;false]"
.."button[6.7,0;1.2,1;withdraw;Withdraw]"
.."container_end[]"
formspec[#formspec+1] = "container[1.1,5.75]list[current_player;main;0,0;8,1;]"..
"list[current_player;main;0,1.25;8,3;8]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)
local def1 = minetest.registered_items[mkt1.item] or {}
local def2 = minetest.registered_items[mkt2.item] or {}
return (def1.description or "Unknown Item") < (def2.description or "Unknown 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)
local sort_by = account.sort_markets_by_column or 1
-- "Item,Description,#00FF00,Buy Vol,Buy Max,#FF0000,Sell Vol,Sell Min,Last Price"
if sort_by == 1 then
table.sort(item_list, compare_market_item)
elseif sort_by == 2 then
table.sort(item_list, compare_market_desc)
elseif sort_by == 4 then
table.sort(item_list, compare_buy_volume)
elseif sort_by == 5 then
table.sort(item_list, compare_buy_max)
elseif sort_by == 7 then
table.sort(item_list, compare_sell_volume)
elseif sort_by == 8 then
table.sort(item_list, compare_sell_min)
elseif sort_by == 9 then
table.sort(item_list, compare_last_price)
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_market_formspec = function(market, account)
local selected = account.selected
local formspec = {
"size[10,10]",
"tabheader[0,0;tabs;"..market.def.description..",Your Inventory,Market Orders;3;false;true]",
"tablecolumns[text;"
.."text;"
.."color,span=2;"
.."text,align=right,tooltip=Number of items there's demand for in the market;"
.."text,align=right,tooltip=Maximum price being offered to buy one of these;"
.."color,span=2;"
.."text,align=right,tooltip=Number of items available for sale in the market;"
.."text,align=right,tooltip=Minimum price being demanded to sell one of these;"
.."text,align=right,tooltip=Price paid for one of these the last time one was sold]",
"table[0,0;9.9,5;summary;",
"Item,Description,#00FF00,Buy Vol,Buy Max,#FF0000,Sell Vol,Sell Min,Last Price"
}
local selected_idx
local selected_row
local market_list = make_marketlist(market, account)
-- Show list of item market summaries
for i, row in ipairs(market_list) do
local item_display = row.item
if item_display:len() > 20 then
item_display = item_display:sub(1,18).."..."
end
local n = #formspec+1
formspec[n] = "," .. item_display
local def = minetest.registered_items[row.item] or {}
local desc_display = def.description or "Unknown Item"
if desc_display:len() > 20 then
desc_display = desc_display:sub(1,18).."..."
end
formspec[n+1] = "," .. desc_display
formspec[n+2] = ",#00FF00"
formspec[n+3] = "," .. row.buy_volume
formspec[n+4] = "," .. ((row.buy_orders[#row.buy_orders] or {}).price or "-")
formspec[n+5] = ",#FF0000"
formspec[n+6] = "," .. row.sell_volume
formspec[n+7] = "," .. ((row.sell_orders[#row.sell_orders] or {}).price or "-")
formspec[n+8] = "," .. (row.last_price 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.25,0.6;0.8,0.8;commoditymarket_search.png;apply_search;]"
.."checkbox[1.77,0;filter_participating;My orders;".. account.filter_participating .."]"
.."tooltip[search_filter;Enter substring to search item identifiers for]"
.."tooltip[apply_search;Apply search to outputs]"
.."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()
-- player inventory for this item and for currency
formspec[#formspec+1] = "label[0.1,5.1;"..selected.."\nIn inventory: "
.. tostring(account.inventory[selected] or 0) .."\nBalance: "..market.def.currency_symbol..account.balance .."]"
-- buy/sell controls
formspec[#formspec+1] = "container[6,5]"
formspec[#formspec+1] = "button[0,0.5;1,1;buy;Buy]field[1.3,0.85;1,1;quantity;Quantity;]field[2.3,0.85;1,1;price;Price;]button[3,0.5;1,1;sell;Sell]"
.."field_close_on_enter[quantity;false]field_close_on_enter[price;false]"
formspec[#formspec+1] = "container_end[]"
-- table of buy and sell orders
formspec[#formspec+1] = "tablecolumns[color;text;"
.."text,align=right,tooltip=The price per item in this order;"
.."text,align=right,tooltip=The total amount of items in this particular order;"
.."text,align=right,tooltip=The total amount of items available at this price accounting for the other orders also currently being offered;"
.."text,tooltip=The name of the player who placed this order;"
.."text,align=right,tooltip=How many days ago this order was placed]"
formspec[#formspec+1] = "table[0,6.5;9.9,3.5;orders;#FFFFFF,Order,Price,Quantity,Total Volume,Player,Days Old"
local sell_volume = selected_row.sell_volume
for i, sell in ipairs(selected_row.sell_orders) do
formspec[#formspec+1] = ",#FF0000,Sell,"..sell.price..","..sell.quantity..","..sell_volume
..","..sell.account.name..","..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
buy = buy_orders[i]
buy_volume = buy_volume + buy.quantity
formspec[#formspec+1] = ",#00FF00,Buy,"..buy.price..","..buy.quantity..","..buy_volume
..","..buy.account.name..","..math.floor((current_time-buy.timestamp)/86400)
end
formspec[#formspec+1] = "]"
else
formspec[#formspec+1] = "label[0.1,5.1;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)
local purchaser_name
if log_entry.purchaser == log_entry.seller then
purchaser_name = "themself"
else
purchaser_name = log_entry.purchaser.name
end
return "On day " .. math.ceil(log_entry.timestamp/86400) .. " " .. log_entry.seller.name .. " sold " .. log_entry.quantity .. " "
.. log_entry.item .. " to " .. purchaser_name .. " at " .. market.def.currency_symbol .. log_entry.price
end
local get_info_formspec = function(market, account)
local formspec = {
"size[10,10]",
"tabheader[0,0;tabs;"..market.def.description..",Your Inventory,Market Orders;1;false;true]",
"textarea[0.5,0.5;9.5,1.5;;Description:;"..market.def.long_description.."]",
-- TODO: logging temporarily disabled, it was causing minetest.serialize to generate invalid output for some reason
--"textarea[0.5,2.5;9.5,6;;Your Recent Purchases and Sales:;",
}
-- if next(account.log) then
-- for _, log_entry in ipairs(account.log) do
-- formspec[#formspec+1] = log_to_string(market, log_entry) .. "\n"
-- end
-- else
-- formspec[#formspec+1] = "No logged activites in this market yet"
-- end
-- formspec[#formspec+1] = "]"
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 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
account.sort_markets_by_column = summaryevent.column
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" 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.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.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" then
local index = invevent.row*2 + math.ceil(invevent.column/2) - 4
local account = market:get_account(name)
local inventory = {}
for item, quantity in pairs(account.inventory) do
table.insert(inventory, {item=item, quantity=quantity})
end
table.sort(inventory, inventory_comp)
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
if (fields.filter_participating == "true" and account.filter_participating == "false") or
(fields.filter_participating == "false" and account.filter_participating == "true") then
account.filter_participating = fields.filter_participating
something_changed = true
end
if fields.apply_search or fields.key_enter_field == "search_filter" then
something_changed = true
end
if something_changed then
minetest.show_formspec(name, formname, market:get_formspec(account))
end
end)

18
init.lua Normal file
View File

@ -0,0 +1,18 @@
commoditymarket = {}
local MP = minetest.get_modpath(minetest.get_current_modname())
dofile(MP.."/formspecs.lua")
dofile(MP.."/market.lua")
dofile(MP.."/default_markets.lua")
minetest.register_chatcommand("market.show", {
params = "marketname",
privs = {server=true},
decription = "show market formspec",
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,
})

21
license.txt Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2019 FaceDeer
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.

552
market.lua Normal file
View File

@ -0,0 +1,552 @@
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
local log_sale = function(item, quantity, price, purchaser, seller)
-- TODO: disabled temporarily, the log code should work in theory but in practice minetest.serialize was generating invalid output for some reason.
-- 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 or minetest.registered_items[item] == nil 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)
-- validate that this sell order is possible
if price < 0 or not remove_inventory_from_account(account, item, quantity) then
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
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)
end
-----------------------------------------------------------------------------------------------------------
local test_buy = function(market, balance, item, price, quantity)
if price < 0 or quantity < 1 then return false end
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 not test_buy(market, account.balance, item, price, quantity) then 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
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
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},
decription = "remove item from market. All existing buys and sells will be cancelled.",
func = function(name, param)
local params = param:split(" ")
if #params ~= 2 then
return
end
local market = commoditymarket.registered_markets[params[1]]
if market == nil then
return
end
remove_market_item(market, params[2])
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
-- 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
local load_market_data = function(marketname)
local path = minetest.get_worldpath()
local filename = path .. "\\market_"..marketname..".lua"
local file = loadfile(filename) -- returns nil if the file doesn't exist
if file then
return file()
else
return nil
end
end
local save_market_data = function(market)
local path = minetest.get_worldpath()
local filename = path .. "\\market_"..market.name..".lua"
local file, err = io.open(filename, "w")
if err ~= nil then
minetest.log("error", "[commoditymarket] Could not save market to \"" .. filename .. "\"")
return false
end
local data = {}
data.player_accounts = market.player_accounts
data.orders_for_items = market.orders_for_items
file:write(minetest.serialize(data))
return true
end
commoditymarket.register_market = function(market_name, market_def)
assert(not commoditymarket.registered_markets[market_name])
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 "Market"
market_def.long_description = market_def.long_description or "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)
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)
-- Currency items are always allowed
local item = stack:get_name()
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

1
mod.conf Normal file
View File

@ -0,0 +1 @@
name = commoditymarket

67
readme.md Normal file
View File

@ -0,0 +1,67 @@
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.
## 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, the default market offered by this mod has this currency definition:
{
["default:gold_ingot"] = 1000,
["commoditymarket: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 quanitity 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 fulfil your buy order, the remainder will be placed into the market and made available for future sellers to see and fulfil 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
## Registering a market
The file "default_markets.lua" contains a number of pre-defined markets that provide examples of what's possible with this mod. They can be enabled as-is with game settings and 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, they have on_dig methods that prevent them from being removed and don't have crafting recipes.

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

5
settingtypes.txt Normal file
View File

@ -0,0 +1,5 @@
commoditymarket_enable_kings_market (Enable King's Market) bool false
commoditymarket_enable_night_market (Enable Night Market) bool false
commoditymarket_enable_caravan_market (Enable Trader's Caravan) bool true
commoditymarket_enable_goblin_market (Enable Goblin Exchange) bool false
commoditymarket_enable_under_market (Enable Under Market) bool false

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

8
textures/license.txt Normal file
View File

@ -0,0 +1,8 @@
commoditymarket_gold_coins.png - from https://commons.wikimedia.org/wiki/File:Farm-Fresh_coins.png, by FatCow under the CC-BY 3.0 license
commoditymarket_crown.png - from https://commons.wikimedia.org/wiki/File:Farm-Fresh_crown_gold.png by FatCow under the CC-BY 3.0 Unported license
commoditymarket_moon.png - from https://commons.wikimedia.org/wiki/File:Luneta08.svg by Arturo D. Castillo —Zoram.hakaan— under the CC-BY 3.0 Unported license
commoditymarket_goblin.png - cropped from the "goblins" mod, Copyright 2015 by Francisco "FreeLikeGNU" Athens Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)
commoditymarket_empty_shelf.png - from the moreblocks mod's "moreblocks_empty_shelf", under the zlib license by Hugo Locurcio and contributors
commoditymarket_search.png - Copyright © Diego Martínez (kaeza): CC BY-SA 3.0
commoditymarket_trade.png, commoditymarket_under.png, and commoditymarket_under_top were created by FaceDeer and released under the CC0 public domain license