Update server_shop to v1.1...

Previous update was versioned wrong.

Release: https://github.com/AntumMT/mod-server_shop/releases/tag/v1.1
master
Jordan Irwin 2021-05-11 12:10:02 -07:00
parent d7f811e138
commit 6e3105b67a
11 changed files with 596 additions and 532 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* * [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* * [privilegeareas][] ([WTFPL][lic.privilegeareas] / [CC0][lic.cc0]) -- version: [15eae20 Git][ver.privilegeareas] *2018-11-16*
* [privs][] ([CC0][lic.cc0]) * [privs][] ([CC0][lic.cc0])
* [server_shop][] ([MIT][lic.server_shop]) -- version: [1.1][ver.server_shop] *2021-04-29* * [server_shop][] ([MIT][lic.server_shop]) -- version: [1.1][ver.server_shop] *2021-05-11*
* [spectator_mode][] ([WTFPL][lic.spectator_mode]) -- version: [3648371 Git][ver.spectator_mode] *2020-07-15* * [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* * [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* * [antum][] ([MIT][lic.antum]) -- version: [69b39a4 Git][ver.antum] *2017-08-30*

View File

@ -1,10 +1,17 @@
1.1
- use json format for shops configuration in world directory
- switched id & name parameters positions in register_shop
- show preview image of selected item
- node owners can't set ID unless they have "server" priv
1.0 1.0
- created node - created node
- created simple textures - created simple textures
- formspec displays items & prices of associated shop - formspec displays items & prices of associated shop
- minegeld notes can be deposited & refunded - minegeld notes can be deposited & refunded
- shops are configured from world directory - shops are configured from world directory
- players with "server" priv alter - players with "server" priv or node owners can set ID
- players with "server" priv or node owners can dig - players with "server" priv or node owners can dig
- implemented deposit, purchase, & refund functionality - implemented deposit, purchase, & refund functionality

View File

@ -12,11 +12,11 @@ No craft recipe is given as this for administrators, currently a machine can onl
#### Usage: #### Usage:
Shop lists are registered with the `server_shop.register_shop(name, id, def)` function. `name` is a human-readable string that will be displayed as the shop's title. `id` is a string identifier associated with the shop list. `def` is the shop list definition. Shop lists are defined in a table of tupes in `{itemname, price}` format. 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: Registration example:
``` ```
server_shop.register_shop("Basic", "basic", { server_shop.register_shop("basic", "Basic Shop", {
{ {
{"default:wood", 2}, {"default:wood", 2},
{"default:obsidian", 7}, {"default:obsidian", 7},
@ -24,7 +24,27 @@ server_shop.register_shop("Basic", "basic", {
}) })
``` ```
Shops can optionally be registered in `<world_path>/server_shops.lua` file (this will be changed in the future to use configuration instead of Lua code). Shops can optionally be registered in `<world_path>/server_shops.json` file. Example:
```json
[
{
"id":"frank",
"name":"Frank's Shop",
"sells":
{"default:wood":1}
},
{
"id":"julie",
"name":"Julie's Shop",
"sells":
{
"default:iron_lump":5,
"default:copper_lump":5,
}
},
]
```
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. 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.

View File

@ -2,11 +2,11 @@
TODO: TODO:
- Security: - Security:
- don't allow other players to interfere with transactions - don't allow other players to interfere with transactions
- make loading world directory file more secure (don't use raw Lua)
- "Set ID" button is displayed to other players if admin makes changes while formspec visible - "Set ID" button is displayed to other players if admin makes changes while formspec visible
- Functionality: - Functionality:
- add option to buy multiple of an item - 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) - 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
- Visuals: - Visuals:
- touch up textures - touch up textures
- fix texture tiling - fix texture tiling

View File

@ -0,0 +1,63 @@
--- API
--
local ss = server_shop
local shops = {}
--- Registers a shop list to be accessed via a shop node.
--
-- @function server_shop.register_shop
-- @param id String ID associated with shop list.
-- @param name Human readable name to be displayed.
-- @param def Shop definition (e.g. items & prices)
function ss.register_shop(id, name, def)
if shops[id] then
ss.log("warning", "Overwriting shop with id: " .. id)
end
local new_shop = {name=name, def=def,}
shops[id] = new_shop
ss.log("action", "Registered shop: " .. id)
end
--- Retrieves shop by ID.
--
-- @function server_shop.get_shop
-- @param id String identifier of shop.
-- @return Table of shop contents.
function ss.get_shop(id)
return shops[id]
end
--- Checks if a player has admin rights to for managing shop.
--
-- @function server_shoip.is_shop_admin
-- @param pos Position of shop.
-- @param player Player requesting permissions.
-- @return `true` if player has *server* priv.
function ss.is_shop_admin(pos, player)
if not player then
return false
end
local meta = core.get_meta(pos)
return core.check_player_privs(player, "server")
end
--- Checks if a player is the owner of node.
--
-- @function server_shop.is_shop_owner
-- @param pos Position of shop node.
-- @param player Player to be checked.
-- @return `true` if player is owner.
function ss.is_shop_owner(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

View File

@ -0,0 +1,122 @@
local ss = server_shop
local fs_width = 14
local fs_height = 11
local btn_w = 1.75
local btn_y = 4.6
--- Retrieves shop product list by ID.
--
-- @function get_product_list
-- @local
-- @param id String identifier of shop.
-- @return String of shop contents.
function get_product_list(id)
local products = ""
local shop = ss.get_shop(id)
if shop and shop.def then
for _, p in ipairs(shop.def) do
local item = core.registered_items[p[1]]
if not item then
core.log("warning", "Unknown item \"" .. p[1] .. "\" for shop ID \"" .. id .. "\"")
goto continue
end
local item_name = item.short_description
if not item_name then
item_name = item.description
if not item_name then
item_name = p[1]
end
end
local item_price = p[2]
if not item_price then
core.log("warning", "Price not set for item \"" .. p[1] .. "\" for shop ID \"" .. id .. "\"")
goto continue
end
if products == "" then
products = item_name .. " : " .. tostring(item_price) .. " MG"
else
products = products .. "," .. item_name .. " : " .. tostring(item_price) .. " MG"
end
::continue::
end
end
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")
local formspec = "formspec_version[4]size[" .. tostring(fs_width) .. "," .. tostring(fs_height) .."]"
local shop_name = meta: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
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"))
end
-- get item name for displaying image
local selected_item = nil
local shop = ss.get_shop(id)
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
end
selected_item = shop.def[meta:get_int("selected")]
if selected_item then
selected_item = selected_item[1]
end
end
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]"
if selected_item and selected_item ~= "" then
formspec = formspec
.. "item_image[" .. tostring(fs_width-1.2) .. ",1.5;1,1;" .. selected_item .. "]"
end
formspec = formspec
.. "button[0.2," .. tostring(btn_y) .. ";" .. tostring(btn_w) .. ",0.75;btn_refund;Refund]"
.. "button[" .. tostring(margin_r) .. "," .. tostring(btn_y) .. ";" .. tostring(btn_w) .. ",0.75;btn_buy;Buy]"
.. "list[current_player;main;2.15,5.5;8,4;0]"
.. "button_exit[" .. tostring(margin_r) .. ",10;" .. tostring(btn_w) .. ",0.75;btn_close;Close]"
local formname = "server_shop"
if id and id ~= "" then
formname = formname .. "_" .. id
end
return formspec .. formname
end

View File

@ -1,516 +1,55 @@
server_shop = {} server_shop = {}
server_shop.name = core.get_current_modname() server_shop.modname = core.get_current_modname()
server_shop.modpath = core.get_modpath(server_shop.modname)
local node_name = "server_shop:shop" function server_shop.log(lvl, msg)
if not msg then
msg = lvl
lvl = nil
end
local shops = {} if not lvl then
core.log("[" .. server_shop.modname .. "] " .. msg)
--- Registers a shop list to be accessed via a shop node. else
-- core.log(lvl, "[" .. server_shop.modname .. "] " .. msg)
-- @function server_shop.register_shop
-- @param name Human readable name to be displayed.
-- @param id String ID associated with shop list.
-- @param def Shop definition (e.g. items & prices)
function server_shop.register_shop(name, id, def)
-- FIXME: check if shop is alreay registered
local shop = {}
shop.name = name
shop.id = id
shop.def = def
table.insert(shops, shop)
core.log("action", "[" .. server_shop.name .. "] Registered shop: " .. shop.id)
end
local function get_shop(id)
for _, s in pairs(shops) do
if s.id == id then
return s
end
end end
end end
local function get_shop_name(id) local scripts = {
local shop = get_shop(id) "api",
if shop then "formspec",
return shop.name "node",
end
end
local fs_width = 14
local fs_height = 11
local btn_w = 1.75
local btn_y = 4.6
local function get_product_list(id)
local products = ""
local shop = get_shop(id)
if shop and shop.def then
for _, p in ipairs(shop.def) do
local item = core.registered_items[p[1]]
if not item then
core.log("warning", "Unknown item \"" .. p[1] .. "\" for shop ID \"" .. id .. "\"")
goto continue
end
local item_name = item.short_description
if not item_name then
item_name = item.description
if not item_name then
item_name = p[1]
end
end
local item_price = p[2]
if not item_price then
core.log("warning", "Price not set for item \"" .. p[1] .. "\" for shop ID \"" .. id .. "\"")
goto continue
end
if products == "" then
products = item_name .. " : " .. tostring(item_price) .. " MG"
else
products = products .. "," .. item_name .. " : " .. tostring(item_price) .. " MG"
end
::continue::
end
end
return products
end
--- Checks if a player has admin rights to for managing shop.
--
-- @local
-- @function is_shop_admin
-- @param pos Position of shop.
-- @param player Player requesting permissions.
local function is_shop_admin(pos, player)
if not player then
return false
end
local meta = core.get_meta(pos)
return core.check_player_privs(player, "server")
--or player:get_player_name() == meta:get_string("owner")
end
local function is_shop_owner(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
local function get_formspec(pos, player)
local meta = core.get_meta(pos)
local id = meta:get_string("id")
local deposited = meta:get_int("deposited")
local formspec = "formspec_version[4]size[" .. tostring(fs_width) .. "," .. tostring(fs_height) .."]"
local shop_name = meta:get_string("name"):trim()
if shop_name ~= "" then
formspec = formspec .. "label[0.2,0.4;Shop: " .. shop_name .. "]"
end
if is_shop_admin(pos, 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"))
end
-- get item name for displaying image
local selected_item = nil
local shop = get_shop(id)
if shop then
selected_item = shop.def[meta:get_int("selected")][1]
end
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]"
if selected_item and selected_item ~= "" then
formspec = formspec
.. "item_image[" .. tostring(fs_width-1.2) .. ",1.5;1,1;" .. selected_item .. "]"
end
formspec = formspec
.. "button[0.2," .. tostring(btn_y) .. ";" .. tostring(btn_w) .. ",0.75;btn_refund;Refund]"
.. "button[" .. tostring(margin_r) .. "," .. tostring(btn_y) .. ";" .. tostring(btn_w) .. ",0.75;btn_buy;Buy]"
.. "list[current_player;main;2.15,5.5;8,4;0]"
.. "button_exit[" .. tostring(margin_r) .. ",10;" .. tostring(btn_w) .. ",0.75;btn_close;Close]"
local formname = "server_shop"
if id and id ~= "" then
formname = formname .. "_" .. id
end
return formspec .. formname
end
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. for _, script in ipairs(scripts) do
local function calculate_value(stack) dofile(server_shop.modpath .. "/" .. script .. ".lua")
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 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
--- 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 = 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
--- 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 = get_shop(shop_id)
if shop then
local product = shop.def[idx]
if product then
return product[1]
end
end
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
--- 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
--- Sets the amount of money deposited into machine.
--
-- @local
-- @function set_deposit_balance
-- @param pos Position of shop.
-- @param amount Integer amount to be set.
--[[ UNUSED:
local function set_deposit_balance(pos, amount)
local meta = core.get_meta(pos)
meta:set_int("deposited", amount)
end
]]
--- Retrieves amound of money currently deposited into shop.
--
-- @local
-- @function get_deposit_balance
-- @param pos Position of shop.
local function get_deposit_balance(pos)
return core.get_meta(pos):get_int("deposited")
end
core.register_node(node_name, {
description = "Shop",
--drawtype = "nodebox",
drawtype = "normal",
tiles = {
"server_shop_side.png",
"server_shop_side.png",
"server_shop_side.png",
"server_shop_side.png",
"server_shop_side.png",
"server_shop_front.png",
"server_shop_side.png",
},
--[[
drawtype = "mesh",
mesh = "server_shop.obj",
tiles = {"server_shop_mesh.png",},
selection_box = {
type = "fixed",
fixed = {-0.5, -0.5, -0.5, 0.5, 1.5, 0.5},
},
collision_box = {
type = "fixed",
fixed = {-0.5, -0.5, -0.5, 0.5, 1.45, 0.5},
},
node_box = {
type = "fixed",
fixed = {-0.5, -0.5, -0.5, 0.5, 1.5, 0.5},
},
]]
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", get_formspec(pos))
end,
after_place_node = function(pos, placer)
local meta = core.get_meta(pos)
set_owner(pos, placer:get_player_name())
end,
on_rightclick = function(pos, node, player, itemstack, pointed_thing)
local meta = core.get_meta(pos)
meta:set_string("formspec", get_formspec(pos, player))
local inv = meta:get_inventory()
inv:set_size("deposit", 1)
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 is_shop_owner(pos, player) or is_shop_admin(pos, 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
-- reset selected to default when closed
meta:set_int("selected", meta:get_int("default_selected"))
elseif fields.btn_id and 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: " .. shop_name)
meta:set_string("name", shop_name)
else
meta:set_string("infotext", nil)
meta:set_string("name", nil)
end
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
local refund = calculate_refund(meta:get_int("deposited"))
for _, istack in ipairs(refund) do
give_product(sender, istack)
end
-- reset deposited amount after refund
meta:set_int("deposited", 0)
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", 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", get_formspec(pos, player))
return -1
end
return 0
end
})
-- load configured shops from world directory -- load configured shops from world directory
local shops_file = core.get_worldpath() .. "/server_shops.lua" local shops_file = core.get_worldpath() .. "/server_shops.json"
local fopen = io.open(shops_file, "r") local fopen = io.open(shops_file, "r")
if fopen ~= nil then if fopen ~= nil then
local content = fopen:read("*a")
io.close(fopen) io.close(fopen)
dofile(shops_file)
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
-- FIXME: need safety checks
server_shop.register_shop(shop.id, shop.name, sells)
end
else
-- create file if doesn't exist
fopen = io.open(shops_file, "w")
if fopen == nil then
server_shop.log("error", "Could not create " .. shops_file .. ", directory exists")
else
io.close(fopen)
end
end end

View File

@ -1,6 +1,6 @@
name = server_shop name = server_shop
title = Server Shop title = Server Shop
description = Shops intended to be set up by server administrators. description = Shops intended to be set up by server administrators.
version = 1.0 version = 1.1
author = Jordan Irwin (AntumDeluge) author = Jordan Irwin (AntumDeluge)
depends = currency depends = currency

View File

@ -1,26 +0,0 @@
# Blender v2.79 (sub 0) OBJ File: 'server_shop.blend'
# www.blender.org
mtllib server_shop.mtl
o Cube
v 0.500000 -0.500000 -0.500000
v 0.500000 -0.500000 0.500000
v -0.500000 -0.500000 0.500000
v -0.500000 -0.500000 -0.500000
v 0.500000 1.500000 -0.500000
v 0.500000 1.500000 0.500000
v -0.500000 1.500000 0.500000
v -0.500000 1.500000 -0.500000
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 0.0000
vn 1.0000 0.0000 0.0000
vn -0.0000 -0.0000 1.0000
vn -1.0000 -0.0000 -0.0000
vn 0.0000 0.0000 -1.0000
usemtl Material
s off
f 1//1 2//1 3//1 4//1
f 5//2 8//2 7//2 6//2
f 1//3 5//3 6//3 2//3
f 2//4 6//4 7//4 3//4
f 3//5 7//5 8//5 4//5
f 5//6 1//6 4//6 8//6

View File

@ -0,0 +1,339 @@
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, {
description = "Shop",
--drawtype = "nodebox",
drawtype = "normal",
tiles = {
"server_shop_side.png",
"server_shop_side.png",
"server_shop_side.png",
"server_shop_side.png",
"server_shop_side.png",
"server_shop_front.png",
"server_shop_side.png",
},
--[[
drawtype = "mesh",
mesh = "server_shop.obj",
tiles = {"server_shop_mesh.png",},
selection_box = {
type = "fixed",
fixed = {-0.5, -0.5, -0.5, 0.5, 1.5, 0.5},
},
collision_box = {
type = "fixed",
fixed = {-0.5, -0.5, -0.5, 0.5, 1.45, 0.5},
},
node_box = {
type = "fixed",
fixed = {-0.5, -0.5, -0.5, 0.5, 1.5, 0.5},
},
]]
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())
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)
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)
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
})