659 lines
18 KiB
Lua
659 lines
18 KiB
Lua
|
|
--- Server Shops API
|
|
--
|
|
-- @topic api.lua
|
|
|
|
|
|
local ss = server_shop
|
|
|
|
|
|
local shops = {}
|
|
local registered_currencies = {}
|
|
-- Suffix displayed after deposited amount.
|
|
ss.currency_suffix = nil
|
|
|
|
local update_config = function(shops_data)
|
|
shops_data.shops = shops_data.shops or {}
|
|
|
|
local shops_formatted = {}
|
|
for id, s in pairs(shops_data.shops) do
|
|
local s_type = s.type
|
|
if not s_type then
|
|
s_type = "sell"
|
|
if s.buyer then
|
|
s_type = "buy"
|
|
end
|
|
end
|
|
|
|
shops_formatted[id] = {products=s.products, type=s_type}
|
|
end
|
|
|
|
shops_data.shops = shops_formatted
|
|
wdata.write("server_shops", shops_data)
|
|
end
|
|
|
|
ss.get_shops = function()
|
|
return table.copy(shops)
|
|
end
|
|
|
|
--- Retrieves shop product list.
|
|
--
|
|
-- @function server_shop.get_shop
|
|
-- @tparam string id String identifier of shop.
|
|
-- @tparam bool buyer Denotes whether seller or buyer shops will be parsed (default: false) (deprecated).
|
|
-- @treturn table Table of shop contents.
|
|
ss.get_shop = function(id, buyer)
|
|
if buyer ~= nil then
|
|
ss.log("warning", "get_shop: \"buyer\" parameter is deprecated")
|
|
end
|
|
|
|
local s = shops[id]
|
|
if s then
|
|
s = table.copy(s)
|
|
end
|
|
|
|
return s
|
|
end
|
|
|
|
--- Checks if a shop is registered.
|
|
--
|
|
-- @function server_shop.is_registered
|
|
-- @tparam string id Shop string identifier.
|
|
-- @tparam bool buyer Denotes whether to check seller or buyer shops (default: false) (deprecated).
|
|
-- @treturn bool `true` if the shop ID is found.
|
|
ss.is_registered = function(id, buyer)
|
|
if buyer ~= nil then
|
|
ss.log("warning", "is_registered: \"buyer\" parameter is deprecated")
|
|
end
|
|
|
|
return ss.get_shop(id) ~= nil
|
|
end
|
|
|
|
--- Retrieves shop type string "buyer", "seller", or "unregistered".
|
|
--
|
|
-- @function server_shop.shop_type
|
|
-- @tparam string id
|
|
-- @treturn string
|
|
ss.shop_type = function(id)
|
|
local shop = ss.get_shop(id)
|
|
if shop == nil then
|
|
return "unregistered"
|
|
end
|
|
|
|
if shop.buyer then
|
|
return "buyer"
|
|
end
|
|
|
|
return "seller"
|
|
end
|
|
|
|
--- Checks if a player has admin rights to for managing shop.
|
|
--
|
|
-- @function server_shop.is_shop_admin
|
|
-- @tparam ObjectRef player Player requesting permissions.
|
|
-- @return `true` if player has *server* priv.
|
|
ss.is_shop_admin = function(player)
|
|
if not player then
|
|
return false
|
|
end
|
|
|
|
return core.check_player_privs(player, "server")
|
|
end
|
|
|
|
--- Checks if a player is the owner of node.
|
|
--
|
|
-- @function server_shop.is_shop_owner
|
|
-- @tparam vector pos Position of shop node.
|
|
-- @tparam ObjectRef player Player to be checked.
|
|
-- @treturn bool `true` if player is owner.
|
|
ss.is_shop_owner = function(pos, player)
|
|
if not player then
|
|
return false
|
|
end
|
|
|
|
local meta = core.get_meta(pos)
|
|
return player:get_player_name() == meta:get_string("owner")
|
|
end
|
|
|
|
--- Checks if there are registered currencies in order to give refunds.
|
|
--
|
|
-- @function server_shop.currency_is_registered
|
|
-- @treturn bool `true` if at least one currency item is registered.
|
|
ss.currency_is_registered = function()
|
|
for k, v in pairs(registered_currencies) do
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
--- Retrieves registered currencies & values.
|
|
--
|
|
-- @function server_shop.get_currencies
|
|
-- @treturn table Registered currencies.
|
|
ss.get_currencies = function()
|
|
return table.copy(registered_currencies)
|
|
end
|
|
|
|
--- Registers an item that can be used as currency.
|
|
--
|
|
-- @function server_shop.register_currency
|
|
-- @tparam string item Item name.
|
|
-- @tparam int value Value the item should represent.
|
|
ss.register_currency = function(item, value)
|
|
if not core.registered_items[item] then
|
|
ss.log("warning", "Registering unrecognized item as currency: " .. item)
|
|
end
|
|
|
|
value = tonumber(value)
|
|
if not value then
|
|
ss.log("error", "Currency type for " .. item .. " must be a number. Got \"" .. type(value) .. "\"")
|
|
return
|
|
end
|
|
|
|
local old_value = registered_currencies[item]
|
|
if old_value then
|
|
ss.log("warning", "overwriting value for currency " .. item
|
|
.. " from " .. tostring(old_value)
|
|
.. " to " .. tostring(value))
|
|
end
|
|
|
|
registered_currencies[item] = value
|
|
|
|
ss.log("action", item .. " registered as currency with value of " .. tostring(value))
|
|
end
|
|
|
|
if ss.use_currency_defaults then
|
|
if not core.get_modpath("currency") then
|
|
ss.log("warning", "currency mod not found, not registering default currencies")
|
|
else
|
|
local mg_notes = {
|
|
{"currency:minegeld", 1},
|
|
{"currency:minegeld_5", 5},
|
|
{"currency:minegeld_10", 10},
|
|
{"currency:minegeld_50", 50},
|
|
{"currency:minegeld_100", 100},
|
|
}
|
|
|
|
for _, c in ipairs(mg_notes) do
|
|
ss.register_currency(c[1], c[2])
|
|
end
|
|
|
|
ss.currency_suffix = "MG"
|
|
end
|
|
end
|
|
|
|
--- Checks ID string for invalid characters.
|
|
--
|
|
-- @function server_shop.format_id
|
|
-- @tparam string id Shop identifier string.
|
|
-- @treturn string Formatted string.
|
|
ss.format_id = function(id)
|
|
return id:trim():gsub("%s", "_")
|
|
end
|
|
|
|
--- Used for debugging.
|
|
--
|
|
-- @local
|
|
-- @tparam table products
|
|
-- @tparam[opt] string delim
|
|
local format_shop_list = function(products, delim)
|
|
delim = delim or ", "
|
|
|
|
local p_list = {}
|
|
for _, p in ipairs(products) do
|
|
local p_string = tostring(p[1]) .. ": " .. tostring(p[2])
|
|
if ss.currency_suffix then
|
|
p_string = p_string .. " " .. ss.currency_suffix
|
|
end
|
|
|
|
table.insert(p_list, p_string)
|
|
end
|
|
|
|
return table.concat(p_list, delim)
|
|
end
|
|
|
|
--- Registers a shop.
|
|
--
|
|
-- **Aliases:**
|
|
--
|
|
-- - server\_shop.register\_shop
|
|
--
|
|
-- @function server_shop.register
|
|
-- @tparam string id Shop string identifier.
|
|
-- @tparam table[string,int] products List of products & prices in format `{item_name, price}`.
|
|
-- @tparam[opt] bool buyer
|
|
ss.register = function(id, products, buyer, buyer_old)
|
|
if type(id) ~= "string" then
|
|
ss.log("error", ss.modname .. ".register: invalid \"id\" parameter")
|
|
return
|
|
end
|
|
|
|
local shop_def = {}
|
|
|
|
if type(products) == "string" then
|
|
ss.log("warning", ss.modname .. ".register: string \"products\" parameter deprecated")
|
|
shop_def.products = buyer
|
|
shop_def.buyer = buyer_old == true
|
|
else
|
|
shop_def.products = products
|
|
shop_def.buyer = buyer == true
|
|
end
|
|
|
|
-- allow shops to be initialized without products
|
|
shop_def.products = shop_def.products or {}
|
|
|
|
if type(shop_def.products) ~= "table" then
|
|
ss.log("error", ss.modname .. ".register: invalid \"products\" list")
|
|
return
|
|
end
|
|
|
|
id = ss.format_id(id)
|
|
|
|
if shops[id] ~= nil then
|
|
ss.log("warning", "overwriting shop with ID: " .. id)
|
|
end
|
|
|
|
shops[id] = shop_def
|
|
ss.log("action", "registered " .. ss.shop_type(id) .. " shop with ID: " .. id)
|
|
ss.log("debug", "product list:\n " .. format_shop_list(shops[id].products, "\n "))
|
|
end
|
|
|
|
-- backward compatibility
|
|
ss.register_shop = ss.register
|
|
|
|
--- Registers a shop & updates config.
|
|
--
|
|
-- @function server_shop.register_persist
|
|
-- @tparam string id Shop string identifier.
|
|
-- @tparam table[string,int] products List of products & prices in format `{item_name, price}`.
|
|
-- @tparam[opt] bool buyer
|
|
ss.register_persist = function(id, products, buyer)
|
|
ss.register(id, products, buyer)
|
|
|
|
local shops_data = wdata.read("server_shops") or {}
|
|
shops_data.shops = shops_data.shops or {}
|
|
|
|
local s_type = "sell"
|
|
if buyer then
|
|
s_type = "buy"
|
|
end
|
|
|
|
shops_data.shops[id] = {products=products, type=s_type}
|
|
update_config(shops_data)
|
|
end
|
|
|
|
--- Registers a seller shop.
|
|
--
|
|
-- @function server_shop.register_seller
|
|
-- @tparam string id Shop string identifier.
|
|
-- @tparam table[string,int] products List of products & prices in format `{item_name, price}`.
|
|
ss.register_seller = function(id, products, old_products)
|
|
if type(products) == "string" then
|
|
ss.log("warning", ss.modname .. ".register_seller: string \"products\" parameter deprecated")
|
|
products = old_products
|
|
end
|
|
|
|
return ss.register(id, products)
|
|
end
|
|
|
|
--- Registers a buyer shop.
|
|
--
|
|
-- @function server_shop.register_buyer
|
|
-- @tparam string id Shop string identifier.
|
|
-- @tparam table[string,int] products List of products & prices in format `{item_name, price}`.
|
|
ss.register_buyer = function(id, products, old_products)
|
|
if type(products) == "string" then
|
|
ss.log("warning", ss.modname .. ".register_buyer: string \"products\" parameter deprecated")
|
|
products = old_products
|
|
end
|
|
|
|
return ss.register(id, products, true)
|
|
end
|
|
|
|
--- Unregisters a shop.
|
|
--
|
|
-- @function server_shop.unregister
|
|
-- @tparam string id Shop ID.
|
|
-- @treturn bool `true` if shop was unregistered.
|
|
ss.unregister = function(id)
|
|
if shops[id] ~= nil then
|
|
local stype = ss.shop_type(id)
|
|
shops[id] = nil
|
|
ss.log("action", "unregistered " .. stype .. " shop with ID: " .. id)
|
|
return true
|
|
end
|
|
|
|
ss.log("action", "cannot unregister non-registered shop with ID: " .. id)
|
|
return false
|
|
end
|
|
|
|
--- Unregisters a shop & updates config.
|
|
--
|
|
-- @function server_shop.unregister
|
|
-- @tparam string Shop ID.
|
|
-- @treturn bool `true` if shop was unregistered.
|
|
ss.unregister_persist = function(id)
|
|
local retval = ss.unregister(id)
|
|
|
|
if retval then
|
|
local shops_data = wdata.read("server_shops") or {}
|
|
shops_data.shops = shops_data.shops or {}
|
|
|
|
shops_data.shops[id] = nil
|
|
update_config(shops_data)
|
|
end
|
|
|
|
return retval
|
|
end
|
|
|
|
--- Adds a product to a shop.
|
|
--
|
|
-- @function server_shop:add_product
|
|
-- @tparam string id Shop identifier.
|
|
-- @param product Item technical name (`string`) or product/value pairs (`table`).
|
|
-- @tparam number value Product's represented value.
|
|
-- @tparam[opt] number idx Position in shop list where item should be inserted.
|
|
-- @treturn table Shop definition that was altered or `nil`.
|
|
ss.add_product = function(id, product, value, idx)
|
|
local target_shop = shops[id]
|
|
if not target_shop then
|
|
ss.log("error", "add_product: cannot add to unknown shop ID: " .. tostring(id))
|
|
return
|
|
end
|
|
|
|
local p_type = type(product)
|
|
if p_type ~= "string" and p_type ~= "table" then
|
|
ss.log("error", "add_product: \"product\" must be a string or table of"
|
|
.. " product/value pairs for shop ID: " .. id)
|
|
return
|
|
end
|
|
|
|
target_shop.products = target_shop.products or {}
|
|
if p_type == "table" then
|
|
if value then
|
|
ss.log("warning", "add_product: \"value\" is ignored when product type is table")
|
|
end
|
|
if idx then
|
|
ss.log("warning", "add_product: \"idx\" is ignored when product type is table")
|
|
end
|
|
|
|
for _, p in ipairs(product) do
|
|
table.insert(target_shop.products, p)
|
|
end
|
|
else
|
|
if type(value) ~= "number" then
|
|
ss.log("error", "add_product: \"value\" must be a number for shop ID: " .. id)
|
|
return
|
|
end
|
|
|
|
if not idx or idx > #target_shop.products then
|
|
idx = #target_shop.products + 1
|
|
end
|
|
|
|
for _, p in ipairs(target_shop.products) do
|
|
if product == p[1] then
|
|
ss.log("warning", "add_product: adding duplicate item to shop ID: " .. id)
|
|
break
|
|
end
|
|
end
|
|
|
|
table.insert(target_shop.products, {product, value})
|
|
end
|
|
|
|
return target_shop
|
|
end
|
|
|
|
--- Adds a product to a shop & updates config.
|
|
--
|
|
-- @function server_shop.add_product_persist
|
|
-- @tparam string id Shop identifier.
|
|
-- @param product Item technical name (`string`) or product/value pairs (`table`).
|
|
-- @tparam number value Product's represented value.
|
|
-- @tparam[opt] number idx Position in shop list where item should be inserted.
|
|
ss.add_product_persist = function(id, product, value, idx)
|
|
local target_shop = ss.add_product(id, product, value, idx)
|
|
if target_shop then
|
|
local shops_data = wdata.read("server_shops") or {}
|
|
shops_data.shops = shops_data.shops or {}
|
|
shops_data.shops[id] = target_shop
|
|
update_config(shops_data)
|
|
end
|
|
end
|
|
|
|
local get_product_index = function(id, product)
|
|
local target_shop = ss.get_shop(id)
|
|
|
|
if not target_shop or not target_shop.products or #target_shop.products == 0 then
|
|
return
|
|
end
|
|
|
|
local indexes = {}
|
|
for idx=1, #target_shop.products do
|
|
if product == target_shop.products[idx][1] then
|
|
table.insert(indexes, idx)
|
|
end
|
|
end
|
|
|
|
return #indexes > 0 and indexes or nil
|
|
end
|
|
|
|
--- Removes product(s) from a shop.
|
|
--
|
|
-- @function server_shop.remove_product
|
|
-- @tparam string id Shop identifier.
|
|
-- @tparam string product Item technical name.
|
|
-- @tparam[opt] bool all If `false`, only removes first instance of `product` from shop list (default: `true`).
|
|
-- @return Shop definition that was altered or `nil` & number of items removed.
|
|
ss.remove_product = function(id, product, all)
|
|
all = all ~= false
|
|
|
|
local target_shop = shops[id]
|
|
if not target_shop then
|
|
ss.log("error", "remove_product: cannot remove from unknown shop ID: " .. tostring(id))
|
|
return
|
|
end
|
|
|
|
if type(product) ~= "string" then
|
|
ss.log("error", "remove_product: \"product\" must be a string for shop ID: " .. id)
|
|
return
|
|
end
|
|
|
|
local indexes = get_product_index(id, product)
|
|
if not indexes then
|
|
ss.log("warning", "remove_product: \"" .. product .. "\" was not found in shop ID: " .. id)
|
|
return
|
|
end
|
|
|
|
local count = 0
|
|
if not all then
|
|
table.remove(target_shop.products, indexes[1])
|
|
count = 1
|
|
else
|
|
for i=#indexes, 1, -1 do
|
|
table.remove(target_shop.products, indexes[i])
|
|
count = count + 1
|
|
end
|
|
end
|
|
|
|
return target_shop, count
|
|
end
|
|
|
|
--- Removes product(s) from a shop & updates config.
|
|
--
|
|
-- @function server_shop.remove_product_persist
|
|
-- @tparam string id Shop identifier.
|
|
-- @tparam string product Item technical name.
|
|
-- @tparam[opt] bool all If `false`, only removes first instance of `product` from shop list (default: `true`).
|
|
-- @treturn int Number of items removed.
|
|
ss.remove_product_persist = function(id, product, all)
|
|
local target_shop, count = ss.remove_product(id, product, all)
|
|
if target_shop then
|
|
local shops_data = wdata.read("server_shops") or {}
|
|
shops_data.shops = shops_data.shops or {}
|
|
shops_data.shops[id] = target_shop
|
|
update_config(shops_data)
|
|
end
|
|
|
|
return count
|
|
end
|
|
|
|
|
|
local shops_file = core.get_worldpath() .. "/server_shops.json"
|
|
|
|
local function shop_file_error(msg)
|
|
ss.log("error", shops_file .. ": " .. msg)
|
|
end
|
|
|
|
--- Loads configuration from world path.
|
|
--
|
|
-- Configuration file is <world\_path>/server\_shops.json
|
|
--
|
|
-- @function server_shop.file_load
|
|
ss.file_load = function()
|
|
ss.log("debug", "loading server_shops.json")
|
|
|
|
local shops_data = wdata.read("server_shops") or {}
|
|
|
|
-- update from legacy format
|
|
if #shops_data > 0 then
|
|
ss.log("action", "updating server_shops.json from legacy format ...")
|
|
|
|
local new_shops_data = {shops={}, currencies={}}
|
|
|
|
for _, entry in ipairs(shops_data) do
|
|
if entry.type == "currency" then
|
|
ss.log("warning", "using \"currency\" key in server_shops.json is deprecated,"
|
|
.. " please use \"currencies\"")
|
|
|
|
new_shops_data.currencies[entry.name] = entry.value
|
|
elseif entry.type == "currencies" then
|
|
-- allow "value" to be used instead of "currencies"
|
|
if not entry.currencies then entry.currencies = entry.value end
|
|
|
|
for k, v in pairs(entry.currencies) do
|
|
new_shops_data.currencies[k] = v
|
|
end
|
|
elseif entry.type == "suffix" then
|
|
new_shops_data.suffix = entry.value
|
|
elseif entry.type == "sell" or entry.type == "buy" then
|
|
if type(entry.id) ~= "string" or entry.id:trim() == "" then
|
|
shop_file_error("invalid or undeclared \"id\"; must be non-empty string")
|
|
end
|
|
|
|
new_shops_data.shops[entry.id] = {products=entry.products, type=entry.type}
|
|
elseif not entry.type then
|
|
shop_file_error("mandatory \"type\" parameter not set for shop ID: "
|
|
.. tostring(entry.id))
|
|
else
|
|
shop_file_error("unrecognized type \"" .. entry.type
|
|
.. "\" for shop ID: " .. tostring(entry.id))
|
|
end
|
|
end
|
|
|
|
shops_data = new_shops_data
|
|
|
|
-- backup legacy file
|
|
os.rename(shops_file, shops_file .. ".bak")
|
|
update_config(shops_data)
|
|
end
|
|
|
|
if shops_data.suffix ~= nil then
|
|
if type(shops_data.suffix) == "string" then
|
|
ss.currency_suffix = shops_data.suffix
|
|
else
|
|
shop_file_error("\"suffix\" must be a string")
|
|
end
|
|
end
|
|
|
|
if shops_data.currencies ~= nil then
|
|
if type(shops_data.currencies) == "table" then
|
|
for k, v in pairs(shops_data.currencies) do
|
|
ss.register_currency(k, v)
|
|
end
|
|
else
|
|
shop_file_error("\"currencies\" must be a table")
|
|
end
|
|
end
|
|
|
|
if shops_data.shops ~= nil then
|
|
if type(shops_data.shops) == "table" then
|
|
for id, def in pairs(shops_data.shops) do
|
|
if def.type ~= "sell" and def.type ~= "buy" then
|
|
shop_file_error("shop \"type\" must be either \"sell\" or \"buy\" for ID: " .. id)
|
|
else
|
|
ss.register(id, def.products, def.type == "buy")
|
|
end
|
|
end
|
|
else
|
|
shop_file_error("\"shops\" must be a table")
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Prunes unknown items & updates aliases in shops.
|
|
--
|
|
-- @function server_shop.prune_shops
|
|
-- @tparam[opt] bool persist If `true`, changes will be written to config.
|
|
ss.prune_shops = function(persist)
|
|
persist = persist == true
|
|
|
|
-- show warning if no currencies are registered
|
|
if not ss.currency_is_registered() then
|
|
ss.log("warning", "no currencies registered")
|
|
else
|
|
local have_ones = false
|
|
for k, v in pairs(ss.get_currencies()) do
|
|
have_ones = v == 1
|
|
if have_ones then break end
|
|
end
|
|
|
|
if not have_ones then
|
|
ss.log("warning", "no currency registered with value 1, players may not be refunded all of their money")
|
|
end
|
|
end
|
|
|
|
-- prune unregistered items & items without value
|
|
for id, def in pairs(ss.get_shops()) do
|
|
local s_type = def.buyer and "buyer" or "seller"
|
|
|
|
local pruned = false
|
|
for idx = #def.products, 1, -1 do
|
|
local product = def.products[idx][1]
|
|
local value = def.products[idx][2]
|
|
|
|
if not value then
|
|
ss.log("warning", "pruning item \"" .. product
|
|
.. "\" without value from " .. s_type .. " shop ID: " .. id)
|
|
table.remove(def.products, idx)
|
|
pruned = true
|
|
elseif not core.registered_items[product] then
|
|
local alias_of = core.registered_aliases[product]
|
|
if not alias_of then
|
|
ss.log("warning", "pruning unregistered item \"" .. product
|
|
.. "\" from " .. s_type .. " shop ID: " .. id)
|
|
end
|
|
|
|
table.remove(def.products, idx)
|
|
pruned = true
|
|
|
|
if alias_of then
|
|
ss.log("action", "replacing alias \"" .. product .. "\" with \""
|
|
.. alias_of .. "\" in seller shop ID: " .. id)
|
|
table.insert(def.products, idx, {alias_of, value})
|
|
end
|
|
end
|
|
end
|
|
|
|
if pruned then
|
|
ss.register(id, def.products, def.buyer)
|
|
end
|
|
end
|
|
|
|
if persist then
|
|
local shops_data = wdata.read("server_shops")
|
|
shops_data.shops = shops
|
|
update_config(shops_data)
|
|
end
|
|
end
|