Update server_shop mod to Git commit 993332e...

https://github.com/AntumMT/mod-server_shop/tree/993332e
This commit is contained in:
Jordan Irwin 2021-05-12 21:48:29 -07:00
parent 6e3105b67a
commit cfddf7bde4
11 changed files with 591 additions and 355 deletions

View File

@ -19,7 +19,7 @@ The game includes the mods from the default [minetest_game](https://github.com/m
* [override][] ([MIT][lic.override]) -- version: [0.2 (e6dda7a Git)][ver.override] *2017-08-30*
* [privilegeareas][] ([WTFPL][lic.privilegeareas] / [CC0][lic.cc0]) -- version: [15eae20 Git][ver.privilegeareas] *2018-11-16*
* [privs][] ([CC0][lic.cc0])
* [server_shop][] ([MIT][lic.server_shop]) -- version: [1.1][ver.server_shop] *2021-05-11*
* [server_shop][] ([MIT][lic.server_shop]) -- version: [993332e Git][ver.server_shop] *2021-05-12*
* [spectator_mode][] ([WTFPL][lic.spectator_mode]) -- version: [3648371 Git][ver.spectator_mode] *2020-07-15*
* [whitelist][] ([CC0][lic.cc0]) -- version: [0.1 (b813b19 Git)][ver.whitelist] *2017-08-18*
* [antum][] ([MIT][lic.antum]) -- version: [69b39a4 Git][ver.antum] *2017-08-30*
@ -564,7 +564,7 @@ The game includes the mods from the default [minetest_game](https://github.com/m
[ver.pvp_areas]: https://github.com/everamzah/pvp_areas/tree/6e4d66d
[ver.quartz]: https://github.com/minetest-mods/quartz/tree/72ec06f
[ver.rainbow_ore]: https://github.com/FsxShader2012/rainbow_ore/tree/6e77693
[ver.server_shop]: https://github.com/AntumMT/mod-server_shop/releases/tag/v1.1
[ver.server_shop]: https://github.com/AntumMT/mod-server_shop/tree/993332e
[ver.sfinv_buttons]: http://repo.or.cz/minetest_sfinv_buttons.git/tree/ebb1f7c
[ver.shark]: https://github.com/AntumMT/mod-shark/tree/ef2507b
[ver.sheep]: https://github.com/AntumMT/mod-cmer/tree/7c91aaf

View File

@ -1,4 +1,19 @@
pre
- custom currencies can be registered
- changed format of world "server_shops.json":
- "sells" keyword changed to "products"
- added required "type" keyword can be:
- "sell" to register new seller shop
- "currency" to register new currency
- "suffix" to set a suffix to display after deposited amount
- currencies can be registered using "currency" type:
- subkeys are "name" (string) & "value" (number)
- "currency" mod minegeld notes not registered automatically
- displayed currency suffix can be customized or omitted
- no longer uses node meta for formspec
1.1
- use json format for shops configuration in world directory
- switched id & name parameters positions in register_shop

View File

@ -1,5 +1,6 @@
## Server Shop
---
### Description:
Shops intended to be set up by [Minetest](https://www.minetest.net/) server administrators.
@ -10,12 +11,15 @@ No craft recipe is given as this for administrators, currently a machine can onl
![screenshot](screenshot.png)
#### Usage:
---
### Usage:
#### Registering Shops & Currencies:
Shop lists are registered with the `server_shop.register_shop(id, name, def)` function. `id` is a string identifier associated with the shop list. `name` is a human-readable string that will be displayed as the shop's title. `def` is the shop list definition. Shop lists are defined in a table of tuples in `{itemname, price}` format.
Registration example:
```
```lua
server_shop.register_shop("basic", "Basic Shop", {
{
{"default:wood", 2},
@ -24,20 +28,22 @@ server_shop.register_shop("basic", "Basic Shop", {
})
```
Shops can optionally be registered in `<world_path>/server_shops.json` file. Example:
Shops can optionally be configured in `<world_path>/server_shops.json` file. To register a seller shop (buyers currently not supported), set `type` to "sell". `id` is a string identifier for the shop. `name` is the string displayed in the formspec & when a player points at the node. `products` is a list of products sold at the shop in format "name:value".
Example:
```json
[
{
"type":"sell",
"id":"frank",
"name":"Frank's Shop",
"sells":
{"default:wood":1}
"products":{"default:wood":1}
},
{
"type":"sell",
"id":"julie",
"name":"Julie's Shop",
"sells":
"products":
{
"default:iron_lump":5,
"default:copper_lump":5,
@ -46,30 +52,69 @@ Shops can optionally be registered in `<world_path>/server_shops.json` file. Exa
]
```
Currencies can be registered with `server_shop.register_currency`:
```lua
server_shop.register_currency("currency:minegeld", 1)
server_shop.register_currency("currency:minegeld_5", 5)
```
When registering a new currency in `server_shops.json`, set `type` to "currency". `name` is the item to be used as currency & `value` is the item's worth:
```json
{
"type":"currency",
"name":"currency:minegeld",
"value":1,
},
{
"type":"currency",
"name":"currency:minegeld_5",
"value":5,
},
```
You can also register a currency suffix to be displayed in the formspec. Simply set the value of `server_shop.currency_suffix`:
```lua
server_shop.currency_suffix = "MG"
```
In `server_shops.json`, set `type` to "suffix" & `value` to the string to be displayed:
```json
{
"type":"suffix",
"value":"MG",
},
```
#### Setting up Shops in Game:
Server admins use the chat command `/giveme server_shop:shop` to receive a shop node. After placing the node, the ID can be set with the "Set ID" button & text input field (only players with the "server" privilege can set ID). Set the ID to the shop ID you want associated with this shop node ("basic" for the example above) & the list will be populated with the registered products & prices.
To make purchases, players deposit [minegeld notes][mod.currency] into the deposit slot. Select an item to purchase & press the "Buy" button. If there is adequate money deposited, player will receive the item & the price will be deducted from the deposited amount. Press the "Refund" button to retrieve any money not spent.
To make purchases, players deposit currency items into the deposit slot. Select an item to purchase & press the "Buy" button. If there is adequate money deposited, player will receive the item & the price will be deducted from the deposited amount. Press the "Refund" button to retrieve any money not spent.
***SECURITY WARNING:*** As stated, this mod is in early development. Currently, it is possible to interfere in another player's transactions. So this mod is *not* recommended for use with public servers at this time.
---
### Licensing:
- Code: [MIT](LICENSE.txt)
- Textures: CC0
---
### Dependencies:
- Required:
- [currency][mod.currency]
- Optional:
- none
- Optional:
- [currency][mod.currency]
Compatible with:
---
### Links:
- [GitHub repo](https://github.com/AntumMT/mod-server_shop)
- [Minetest forum](https://forum.minetest.net/viewtopic.php?t=26645)
- [API](https://antummt.github.io/mod-server_shop/docs/api.html)
- [Changelog](CHANGES.txt)
- [TODO](TODO.txt)

View File

@ -1,12 +1,12 @@
TODO:
- Security:
- don't allow other players to interfere with transactions
- "Set ID" button is displayed to other players if admin makes changes while formspec visible
- might be an issue if admin changes shop ID while player is using
- Functionality:
- add option to buy multiple of an item
- optimize how refunds are given (e.g. if there is no room for 50s, but room for 1s, then give out 1s instead)
- add shop for players to receive money for selling items
- allow any currency value, not just 1, 5, 10, 50, & 100
- Visuals:
- touch up textures
- fix texture tiling
@ -14,10 +14,5 @@ TODO:
- Misc.:
- make usable with "folks" mod
- add localization support
- make "get_shop" global function
- allow registering custom types of currency
- clean up code
- check if shop is already registered before re-registering
- let shops with deposit balance be dug, & drop balance on ground
- create shop that gives money for items
- add chat command to reload shops file in world directory
- set shop name from formspec instead of registration so shops with different names can use same content

View File

@ -1,13 +1,62 @@
--- API
--
-- @module api.lua
local ss = server_shop
--- Registered shops.
--
-- @local
-- @table shops
local shops = {}
--- Currencies registered for trade.
--
-- @table server_shop.registered_currencies
ss.registered_currencies = {}
-- Suffix displayed after deposited amount.
ss.currency_suffix = nil
--- Registers an item that can be used as currency.
--
-- TODO:
-- - after registering currency, should re-organize table from highest value to lowest
--
-- @function server_shop.register_currency
-- @tparam string item Item name.
-- @tparam int value Value the item should represent.
function ss.register_currency(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 = ss.registered_currencies[item]
if old_value then
ss.log("warning", "Overwriting value for currency " .. item
.. " from " .. tostring(old_value)
.. " to " .. tostring(value))
end
ss.registered_currencies[item] = value
ss.log("action", item .. " registered as currency with value of " .. tostring(value))
end
--- Registers a shop list to be accessed via a shop node.
--
-- TODO:
-- - log warning if `def` name or value missing or wrong type or
-- if name is empty string
--
-- @function server_shop.register_shop
-- @param id String ID associated with shop list.
-- @param name Human readable name to be displayed.
@ -34,16 +83,14 @@ end
--- Checks if a player has admin rights to for managing shop.
--
-- @function server_shoip.is_shop_admin
-- @param pos Position of shop.
-- @function server_shop.is_shop_admin
-- @param player Player requesting permissions.
-- @return `true` if player has *server* priv.
function ss.is_shop_admin(pos, player)
function ss.is_shop_admin(player)
if not player then
return false
end
local meta = core.get_meta(pos)
return core.check_player_privs(player, "server")
end

View File

@ -0,0 +1,50 @@
local ss = server_shop
local transaction = dofile(ss.modpath .. "/transaction.lua")
--- Calculates the value of an item stack.
--
-- @local
-- @function server_shop.calculate_value
-- @tparam ItemStack stack Item stack.
local function calculate_value(stack)
local value = 0
for c, v in pairs(ss.registered_currencies) do
if stack:get_name() == c then
value = stack:get_count() * v
break
end
end
return value
end
local callbacks = {
allow_put = function(inv, listname, index, stack, player)
-- TODO: move this to `on_put`
local pmeta = player:get_meta()
local to_deposit = calculate_value(stack)
if to_deposit <= 0 then return 0 end
local id = pmeta:get_string(ss.modname .. ":id")
if id:trim() == "" then return 0 end
local pos = core.deserialize(pmeta:get_string(ss.modname .. ":pos"))
if not pos then return 0 end
transaction.set_deposit(id, player, transaction.get_deposit(id, player) + to_deposit)
-- refresh formspec dialog
ss.show_formspec(pos, player)
return -1
end,
}
local inv = core.create_detached_inventory(ss.modname, callbacks)
inv:set_size("deposit", 1)

View File

@ -7,13 +7,43 @@ local fs_height = 11
local btn_w = 1.75
local btn_y = 4.6
--- Retrieves shop name by ID.
--
-- @local
-- @function get_shop_name
-- @tparam string id String identifier of shop.
-- @treturn string Shop's name representation.
local function get_shop_name(id)
local shop = ss.get_shop(id)
if shop then
return shop.name
end
end
--- Retrieves item name/id from shop list.
--
-- @local
-- @function get_shop_index
-- @param shop_id String identifier of shop.
-- @param idx Index of the item to retrieve.
-- @return String item identifier or `nil` if shop does not contain item.
local function get_shop_index(shop_id, idx)
local shop = ss.get_shop(shop_id)
if shop then
local product = shop.def[idx]
if product then
return product[1]
end
end
end
--- Retrieves shop product list by ID.
--
-- @function get_product_list
-- @local
-- @function get_product_list
-- @param id String identifier of shop.
-- @return String of shop contents.
function get_product_list(id)
local function get_product_list(id)
local products = ""
local shop = ss.get_shop(id)
@ -53,29 +83,37 @@ function get_product_list(id)
return products
end
function server_shop.get_formspec(pos, player)
local meta = core.get_meta(pos)
local id = meta:get_string("id")
local deposited = meta:get_int("deposited")
--- Retrieves formspec layout.
--
-- @function server_shop.get_formspec
-- @param pos
-- @param player
function ss.get_formspec(pos, player)
local smeta = core.get_meta(pos)
local pmeta = player:get_meta()
local id = smeta:get_string("id")
local deposited = pmeta:get_int(ss.modname .. ":" .. id .. ":deposited")
local formspec = "formspec_version[4]size[" .. tostring(fs_width) .. "," .. tostring(fs_height) .."]"
local shop_name = meta:get_string("name"):trim()
local shop_name = smeta:get_string("name"):trim()
if shop_name ~= "" then
formspec = formspec .. "label[0.2,0.4;" .. shop_name .. "]"
end
if ss.is_shop_admin(pos, player) then
if ss.is_shop_admin(player) then
formspec = formspec
.. "button[" .. tostring(fs_width-6.2) .. ",0.2;" .. tostring(btn_w) .. ",0.75;btn_id;Set ID]"
.. "field[" .. tostring(fs_width-4.3) .. ",0.2;4.1,0.75;input_id;;" .. id .. "]"
.. "field_close_on_enter[input_id;false]"
end
-- ensure selected value in meta data
if meta:get_int("selected") < 1 then
meta:set_int("selected", meta:get_int("default_selected"))
local selected_idx = 1
if player then
selected_idx = pmeta:get_int(ss.modname .. ":selected")
end
if selected_idx < 1 then
selected_idx = 1
end
-- get item name for displaying image
@ -84,12 +122,11 @@ function server_shop.get_formspec(pos, player)
if shop then
-- make sure we're not out of range of the shop list
local s_idx = meta:get_int("selected")
if s_idx > #shop.def then
s_idx = 1
if selected_idx > #shop.def then
selected_idx = #shop.def
end
selected_item = shop.def[meta:get_int("selected")]
selected_item = shop.def[selected_idx]
if selected_item then
selected_item = selected_item[1]
end
@ -98,10 +135,18 @@ function server_shop.get_formspec(pos, player)
local margin_r = fs_width-(btn_w+0.2)
formspec = formspec
.. "label[0.2,1;Deposited: " .. tostring(deposited) .. " MG]"
.. "list[context;deposit;0.2,1.5;1,1;0]"
.. "textlist[2.15,1.5;9.75,3;products;" .. get_product_list(id) .. ";"
.. tostring(meta:get_int("selected")) .. ";false]"
.. "label[0.2,1;Deposited: " .. tostring(deposited)
if ss.currency_suffix and ss.currency_suffix ~= "" then
formspec = formspec .. " " .. ss.currency_suffix
end
formspec = formspec .. "]"
if id ~= "" then -- don't allow deposits to unregistered stores
formspec = formspec .. "list[detached:" .. ss.modname .. ";deposit;0.2,1.5;1,1;0]"
end
formspec = formspec .. "textlist[2.15,1.5;9.75,3;products;" .. get_product_list(id) .. ";"
.. tostring(selected_idx) .. ";false]"
if selected_item and selected_item ~= "" then
formspec = formspec
@ -120,3 +165,110 @@ function server_shop.get_formspec(pos, player)
return formspec .. formname
end
--- Displays formspec to player.
--
-- @function server_shop.show_formspec
-- @param pos
-- @param player
function ss.show_formspec(pos, player)
core.show_formspec(player:get_player_name(), ss.modname .. ":shop", ss.get_formspec(pos, player))
end
local transaction = dofile(ss.modpath .. "/transaction.lua")
core.register_on_player_receive_fields(function(player, formname, fields)
if formname == ss.modname .. ":shop" then
local pmeta = player:get_meta()
local pos = core.deserialize(pmeta:get_string(ss.modname .. ":pos"))
if not pos then
ss.log("error", "cannot retrieve shop position from player meta data")
return false
end
local smeta = core.get_meta(pos)
local id = smeta:get_string("id")
if fields.quit then
-- return money to player if formspec closed
transaction.give_refund(id, player)
return false
elseif fields.btn_id or (fields.key_enter and fields.key_enter_field == "input_id") then
-- safety check that only server admin can set ID
if not ss.is_shop_admin(player) then
ss.log("warning", "non-admin player " .. player.get_player_name()
.. " attempted to change shop ID at ("
.. tostring(pos.x) .. "," .. tostring(pos.y) .. "," .. tostring(pos.z)
.. ")")
return false
end
-- should not happen
if not fields.input_id then
ss.log("error", "Cannot retrieve ID input")
return false
end
fields.input_id = fields.input_id:trim()
smeta:set_string("id", fields.input_id)
-- set or remove displayed text when pointed at
local shop_name = get_shop_name(fields.input_id)
if shop_name then
smeta:set_string("infotext", shop_name)
smeta:set_string("name", shop_name)
else
smeta:set_string("infotext", nil)
smeta:set_string("name", nil)
end
ss.log("action", "server admin " .. player:get_player_name()
.. " set shop ID at ("
.. tostring(pos.x) .. "," .. tostring(pos.y) .. "," .. tostring(pos.z)
.. ") to \"" .. id .. "\"")
elseif fields.products then
local idx = tonumber(fields.products:sub(5))
if type(idx) == "number" then
pmeta:set_int(ss.modname .. ":selected", idx)
end
elseif fields.btn_refund then
transaction.give_refund(id, player)
elseif fields.btn_buy then
local idx = pmeta:get_int(ss.modname .. ":selected")
local product = get_shop_index(id, idx)
if not product then
ss.log("warning", "Trying to buy undefined item from shop \""
.. tostring(id) .. "\"")
return false
end
-- FIXME: allow purchasing multiples
local product_count = 1
local total = transaction.calculate_price(id, product, product_count)
local deposited = transaction.get_deposit(id, player)
if total > deposited then
core.chat_send_player(player:get_player_name(), "You haven't deposited enough money.")
return false
end
product = ItemStack(product)
product:set_count(product_count)
-- subtract total from deposited money
transaction.set_deposit(id, player, deposited - total)
-- execute transaction
core.chat_send_player(player:get_player_name(), "You purchased " .. tostring(product:get_count())
.. " " .. product:get_description() .. " for " .. tostring(total) .. " MG.")
transaction.give_product(player, product, product_count)
end
-- refresh formspec view
ss.show_formspec(pos, player)
return false
end
end)

View File

@ -1,34 +1,41 @@
server_shop = {}
server_shop.modname = core.get_current_modname()
server_shop.modpath = core.get_modpath(server_shop.modname)
local ss = server_shop
function server_shop.log(lvl, msg)
ss.modname = core.get_current_modname()
ss.modpath = core.get_modpath(ss.modname)
function ss.log(lvl, msg)
if not msg then
msg = lvl
lvl = nil
end
if not lvl then
core.log("[" .. server_shop.modname .. "] " .. msg)
core.log("[" .. ss.modname .. "] " .. msg)
else
core.log(lvl, "[" .. server_shop.modname .. "] " .. msg)
core.log(lvl, "[" .. ss.modname .. "] " .. msg)
end
end
local scripts = {
"api",
"deposit",
"formspec",
"node",
}
for _, script in ipairs(scripts) do
dofile(server_shop.modpath .. "/" .. script .. ".lua")
dofile(ss.modpath .. "/" .. script .. ".lua")
end
-- load configured shops from world directory
local shops_file = core.get_worldpath() .. "/server_shops.json"
local function shop_file_error(msg)
error(shops_file .. ": " .. msg)
end
local fopen = io.open(shops_file, "r")
if fopen ~= nil then
local content = fopen:read("*a")
@ -36,13 +43,52 @@ if fopen ~= nil then
local json = core.parse_json(content)
for _, shop in ipairs(json) do
local sells = {}
for k, v in pairs(shop.sells) do
table.insert(sells, {k, v})
end
if shop.type == "currency" then
if type(shop.value) ~= "number" or shop.value <= 0 then
shop_file_error("invalid or undeclared currency \"value\"; must be a number greater than 0")
end
-- FIXME: need safety checks
server_shop.register_shop(shop.id, shop.name, sells)
ss.register_currency(shop.name, shop.value)
elseif shop.type == "suffix" then
if type(shop.value) ~= "string" or shop.value:trim() == "" then
shop_file_error("invalid or undeclared suffix \"value\"; must be non-empty string")
else
ss.currency_suffix = shop.value
end
elseif shop.type == "sell" then
if type(shop.id) ~= "string" or shop.id:trim() == "" then
shop_file_error("invalid or undeclared \"id\"; must be non-empty string")
elseif type(shop.name) ~= "string" or shop.name:trim() == "" then
shop_file_error("invalid or undeclared \"name\"; must be non-empty string")
elseif type(shop.products) ~= "table" then
shop_file_error("invalid or undeclared \"products\" list; must be non-empty table")
else
shop.id = shop.id:trim()
shop.name = shop.name:trim()
local products = {}
for k, v in pairs(shop.products) do
if type(k) ~= "string" or k == "" then
shop_file_error("shop " .. shop.id .. ": invalid or undeclared product name, must be string")
elseif type(v) ~= "number" or v <= 0 then
shop_file_error("shop " .. shop.id .. ": invalid or undeclared product value ("
.. k .. "), must be number greater than 0")
else
table.insert(products, {k, v})
end
end
if #products == 0 then
ss.log("warning", shop_file .. ": empty shop list for shop id \"" .. shop.id .. "\"")
end
server_shop.register_shop(shop.id, shop.name, products)
end
elseif not shop.type then
error(shops_file .. ": mandatory \"type\" parameter not set")
else
error(shops_file .. ": Unrecognized type: " .. shop.type)
end
end
else
-- create file if doesn't exist

View File

@ -3,4 +3,4 @@ title = Server Shop
description = Shops intended to be set up by server administrators.
version = 1.1
author = Jordan Irwin (AntumDeluge)
depends = currency
optional_depends = currency

View File

@ -2,185 +2,7 @@
local ss = server_shop
local node_name = "server_shop:shop"
local currencies = {
{"currency:minegeld", 1,},
{"currency:minegeld_5", 5,},
{"currency:minegeld_10", 10,},
{"currency:minegeld_50", 50,},
{"currency:minegeld_100", 100,},
}
--- Calculates how much money is being deposited.
local function calculate_value(stack)
local value = 0
for _, c in ipairs(currencies) do
if stack:get_name() == c[1] then
value = stack:get_count() * c[2]
break
end
end
return value
end
local function get_shop_name(id)
local shop = ss.get_shop(id)
if shop then
return shop.name
end
end
--- Retrieves item name/id from shop list.
--
-- @function get_shop_index
-- @local
-- @param shop_id String identifier of shop.
-- @param idx Index of the item to retrieve.
-- @return String item identifier or `nil` if shop does not contain item.
local function get_shop_index(shop_id, idx)
local shop = ss.get_shop(shop_id)
if shop then
local product = shop.def[idx]
if product then
return product[1]
end
end
end
--- Sets the owner of the shop & gives admin privileges.
--
-- @local
-- @function set_owner
-- @param pos Position of shop.
-- @param pname String name of new owner.
local function set_owner(pos, pname)
local meta = core.get_meta(pos)
meta:set_string("owner", pname)
end
--- Calculates the price of item being purchased.
--
-- @function calculate_price
-- @local
-- @param shop_id String identifier of shop.
-- @param item_id String identifier of item (e.g. default:dirt).
-- @param quantity Number of item being purchased.
-- @return Total value of purchase.
local function calculate_price(shop_id, item_id, quantity)
local shop = ss.get_shop(shop_id)
if not shop then
return 0
end
local price_per = 0
for _, i in ipairs(shop.def) do
if i[1] == item_id then
price_per = i[2]
break
end
end
return price_per * quantity
end
--- Calculates money to be returned to player.
--
-- FIXME: not very intuitive
local function calculate_refund(total)
local refund = 0
local hun = math.floor(total / 100)
total = total - (hun * 100)
local fif = math.floor(total / 50)
total = total - (fif * 50)
local ten = math.floor(total / 10)
total = total - (ten * 10)
local fiv = math.floor(total / 5)
total = total - (fiv * 5)
-- at this point, 'total' should always be divisible by whole number
local one = total / 1
total = total - one
if total ~= 0 then
core.log("warning", "Refund did not result in 0 deposited balance")
end
local refund = {}
for _, c in ipairs(currencies) do
local iname = c[1]
local ivalue = c[2]
local icount = 0
if ivalue == 1 then
icount = one
elseif ivalue == 5 then
icount = fiv
elseif ivalue == 10 then
icount = ten
elseif ivalue == 50 then
icount = fif
elseif ivalue == 100 then
icount = hun
end
if icount > 0 then
local stack = ItemStack(iname)
stack:set_count(icount)
table.insert(refund, stack)
end
end
return refund
end
--- Add item(s) to player inventory or drops on ground.
--
-- @function player The player who is receiving the item.
-- @local
-- @param product String identifier of the item.
-- @param quantity Amount to give.
local function give_product(player, product, quantity)
local istack = product
if type(istack) == "string" then
-- create the ItemStack
istack = ItemStack(product)
-- make sure we give at leaset 1
if not quantity then quantity = 1 end
istack:set_count(quantity)
elseif quantity and istack:get_count() ~= quantity then
istack:set_count(quantity)
end
-- add to player inventory or drop on ground
local pinv = player:get_inventory()
if not pinv:room_for_item("main", istack) then
core.chat_send_player(player:get_player_name(), "WARNING: "
.. tostring(istack:get_count()) .. " " .. istack:get_description()
.. " was dropped on the ground.")
core.item_drop(istack, player, player:get_pos())
else
pinv:add_item("main", istack)
end
end
local function give_refund(meta, player)
local refund = calculate_refund(meta:get_int("deposited"))
for _, istack in ipairs(refund) do
give_product(player, istack)
end
-- reset deposited amount after refund
meta:set_int("deposited", 0)
end
core.register_node(node_name, {
core.register_node(ss.modname .. ":shop", {
description = "Shop",
--drawtype = "nodebox",
drawtype = "normal",
@ -212,128 +34,21 @@ core.register_node(node_name, {
]]
groups = {oddly_breakable_by_hand=1,},
paramtype2 = "facedir",
on_construct = function(pos)
local meta = core.get_meta(pos)
-- set which item should be selected when formspec is opened
meta:set_int("default_selected", 1)
meta:set_string("formspec", ss.get_formspec(pos))
end,
after_place_node = function(pos, placer)
local meta = core.get_meta(pos)
set_owner(pos, placer:get_player_name())
-- set node owner
core.get_meta(pos):set_string("owner", placer:get_player_name())
end,
on_rightclick = function(pos, node, player, itemstack, pointed_thing)
local meta = core.get_meta(pos)
meta:set_string("formspec", ss.get_formspec(pos, player))
local inv = meta:get_inventory()
inv:set_size("deposit", 1)
local pmeta = player:get_meta()
-- store node pos in player meta for retrieval in callbacks
pmeta:set_string(ss.modname .. ":pos", core.serialize(pos))
-- store selected index in player meta for retrieval in callbacks
pmeta:set_int(ss.modname .. ":selected", 1)
ss.show_formspec(pos, player)
end,
can_dig = function(pos, player)
local meta = core.get_meta(pos)
local deposited = meta:get_int("deposited")
if deposited > 0 then
core.log(player:get_player_name() .. " attempted to dig " .. node_name
.. " containing " .. tostring(deposited) .. " MG at ("
.. pos.x .. "," .. pos.y .. "," .. pos.z .. ")")
return false
end
return ss.is_shop_owner(pos, player) or ss.is_shop_admin(pos, player)
return ss.is_shop_owner(pos, player) or ss.is_shop_admin(player)
end,
on_dig = function(pos, node, digger)
local deposited = core.get_meta(pos):get_int("deposited")
core.node_dig(pos, node, digger)
if deposited < 0 then
core.log("warning", digger:get_player_name() .. " dug " .. node_name
.. " containing negative deposit balance")
end
end,
on_receive_fields = function(pos, formname, fields, sender)
local meta = core.get_meta(pos)
local pname = sender:get_player_name()
if fields.quit then
-- refund any money still deposited
give_refund(meta, sender)
-- reset selected to default when closed
meta:set_int("selected", meta:get_int("default_selected"))
elseif fields.btn_id and ss.is_shop_admin(pos, sender) then
local new_id = fields.input_id:trim()
-- FIXME: allow to be set to "" in order to remove shop
if new_id ~= "" then
core.log("action", pname .. " sets " .. node_name .. " ID to \"" .. new_id
.. "\" at (" .. pos.x .. "," .. pos.y .. "," .. pos.z .. ")")
meta:set_string("id", new_id)
fields.input_id = new_id
-- set or remove displayed text when pointed at
local shop_name = get_shop_name(new_id)
if shop_name then
meta:set_string("infotext", shop_name)
meta:set_string("name", shop_name)
else
meta:set_string("infotext", nil)
meta:set_string("name", nil)
end
-- make sure selected index is set back to default value
meta:set_int("selected", meta:get_int("default_selected"))
end
elseif fields.products then
-- set selected index in meta data to be retrieved when "buy" button is pressed
meta:set_int("selected", fields.products:sub(5))
elseif fields.btn_refund then
give_refund(meta, sender)
elseif fields.btn_buy then
-- get selected index
local selected = meta:get_int("selected")
local shop_id = meta:get_string("id")
local product = get_shop_index(shop_id, selected)
if not product then
core.log("warning", "Trying to buy undefined item from shop \""
.. tostring(shop_id) .. "\"")
return
end
-- FIXME: allow purchasing multiples
local product_count = 1
local total = calculate_price(shop_id, product, product_count)
local deposited = meta:get_int("deposited")
if total > deposited then
core.chat_send_player(pname, "You haven't deposited enough money.")
return
end
product = ItemStack(product)
product:set_count(product_count)
-- subtract total from deposited money
meta:set_int("deposited", deposited - total)
-- execute transaction
core.chat_send_player(pname, "You purchased " .. tostring(product:get_count())
.. " " .. product:get_description() .. " for " .. tostring(total) .. " MG.")
give_product(sender, product)
end
-- refresh formspec dialog
meta:set_string("formspec", ss.get_formspec(pos, sender))
end,
allow_metadata_inventory_put = function(pos, listname, index, stack, player)
local deposited = calculate_value(stack)
if deposited > 0 then
local meta = core.get_meta(pos)
meta:set_int("deposited", meta:get_int("deposited") + deposited)
-- refresh formspec dialog
meta:set_string("formspec", ss.get_formspec(pos, player))
return -1
end
return 0
end
})

View File

@ -0,0 +1,171 @@
local ss = server_shop
--- Sets deposited amount for shop.
--
-- @local
-- @function set_deposit
-- @tparam string id Shop id.
-- @param player Player for whom deposit is being set.
-- @tparam int amount The amount deposit should be set to.
local function set_deposit(id, player, amount)
local pmeta = player:get_meta()
local deposit_id = ss.modname .. ":" .. id .. ":deposited"
pmeta:set_int(deposit_id, amount)
end
--- Retrieves amount player has deposited at shop.
--
-- @local
-- @function get_deposit
-- @tparam string id Shop id.
-- @param player Player to check.
-- @treturn int Total amount currently deposited.
local function get_deposit(id, player)
return player:get_meta():get_int(ss.modname .. ":" .. id .. ":deposited")
end
--- Add item(s) to player inventory or drops on ground.
--
-- @local
-- @function player The player who is receiving the item.
-- @param product String identifier of the item.
-- @param quantity Amount to give.
local function give_product(player, product, quantity)
local istack = product
if type(istack) == "string" then
-- create the ItemStack
istack = ItemStack(product)
-- make sure we give at leaset 1
if not quantity then quantity = 1 end
istack:set_count(quantity)
elseif quantity and istack:get_count() ~= quantity then
istack:set_count(quantity)
end
-- add to player inventory or drop on ground
local pinv = player:get_inventory()
if not pinv:room_for_item("main", istack) then
core.chat_send_player(player:get_player_name(), "WARNING: "
.. tostring(istack:get_count()) .. " " .. istack:get_description()
.. " was dropped on the ground.")
core.item_drop(istack, player, player:get_pos())
else
pinv:add_item("main", istack)
end
end
--- Calculates money to be returned to player.
--
-- FIXME:
-- - not very intuitive
-- - doesn't allow currency values other than 1, 5, 10, 50, & 100
--
-- @local
-- @function calculate_refund
-- @param total
local function calculate_refund(total)
local refund = 0
local hun = math.floor(total / 100)
total = total - (hun * 100)
local fif = math.floor(total / 50)
total = total - (fif * 50)
local ten = math.floor(total / 10)
total = total - (ten * 10)
local fiv = math.floor(total / 5)
total = total - (fiv * 5)
-- at this point, 'total' should always be divisible by whole number
local one = total / 1
total = total - one
if total ~= 0 then
core.log("warning", "Refund did not result in 0 deposited balance")
end
local refund = {}
for c, v in pairs(ss.registered_currencies) do
local icount = 0
if v == 1 then
icount = one
elseif v == 5 then
icount = fiv
elseif v == 10 then
icount = ten
elseif v == 50 then
icount = fif
elseif v == 100 then
icount = hun
end
if icount > 0 then
local stack = ItemStack(c)
stack:set_count(icount)
table.insert(refund, stack)
end
end
return refund
end
--- Returns remaining deposited money to player.
--
-- @local
-- @function give_refund
-- @tparam string id Shop id.
-- @param player Player to whom refund is given.
local function give_refund(id, player)
local pmeta = player:get_meta()
local deposit_id = ss.modname .. ":" .. id .. ":deposited"
local refund = calculate_refund(pmeta:get_int(deposit_id))
for _, istack in ipairs(refund) do
give_product(player, istack)
end
-- reset deposited amount after refund
pmeta:set_string(deposit_id, nil)
end
--- Calculates the price of item being purchased.
--
-- FIXME: might be broken after shop def change
--
-- @local
-- @function calculate_price
-- @param shop_id String identifier of shop.
-- @param item_id String identifier of item (e.g. default:dirt).
-- @param quantity Number of item being purchased.
-- @return Total value of purchase.
local function calculate_price(shop_id, item_id, quantity)
local shop = ss.get_shop(shop_id)
if not shop then
return 0
end
local price_per = 0
for _, i in ipairs(shop.def) do
if i[1] == item_id then
price_per = i[2]
break
end
end
return price_per * quantity
end
return {
set_deposit = set_deposit,
get_deposit = get_deposit,
give_product = give_product,
give_refund = give_refund,
calculate_price = calculate_price,
}