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