619 lines
21 KiB
Lua
619 lines
21 KiB
Lua
-- internationalization boilerplate
|
|
local MP = minetest.get_modpath(minetest.get_current_modname())
|
|
local S, NS = dofile(MP.."/intllib.lua")
|
|
|
|
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 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,
|
|
})
|
|
|
|
minetest.register_chatcommand("market.purge_unknowns", {
|
|
params = "",
|
|
privs = {server=true},
|
|
decription = "removes all unknown items from all markets. All existing buys and sells for those items will be cancelled.",
|
|
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
|
|
if minetest.registered_items[item] == nil then
|
|
table.insert(items_to_remove, item)
|
|
end
|
|
end
|
|
for _, item in ipairs(items_to_remove) do
|
|
remove_market_item(market, item)
|
|
end
|
|
end
|
|
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
|
|
|
|
-- 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 load_market_data = function(marketname)
|
|
local path = minetest.get_worldpath()
|
|
local filename = path .. "/market_"..marketname..".lua"
|
|
return persistence_load(filename)
|
|
end
|
|
|
|
local save_market_data = function(market)
|
|
local path = minetest.get_worldpath()
|
|
local filename = path .. "/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
|
|
|
|
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)
|
|
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 |