init repo

This commit is contained in:
Rainer 2025-08-11 16:15:02 +02:00
commit 692f547daf
26 changed files with 1098 additions and 0 deletions

21
LICENSE.txt Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright © 2021 Jordan Irwin (AntumDeluge)
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

154
README.md Normal file
View file

@ -0,0 +1,154 @@
## Server Shops
### Description:
Shops intended to be set up by [Minetest](https://www.minetest.net/) server administrators.
No craft recipe is given as this for administrators, currently a shop can only be set up with the `/giveme` command. The two shop nodes are `server_shop:shop_small` & `server_shop:shop_large` (they function identically).
![screenshot](screenshot.png)
### Usage:
#### Registering Shops via API:
There are two types of shops, seller & buyer. A seller shop can be registered with `server_shop.register_seller(id, name, products)`. A buyer with `server_shop.register_buyer(id, name, products)`. `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. `products` is the shop list definition. Shop lists are defined in a table of tuples in `{itemname, value}` format. `itemname` is the technical string name of an item (e.g. `default:wood`). `value` is the number representation of what the item is worth.
*Example:*
```lua
-- register seller
server_shop.register_seller("frank", "Frank's Shop", {{"default:wood", 2}})
-- register buyer
server_shop.register_buyer("julie", "Julie's Shop", {
{"default:copper_lump", 5},
{"default:iron_lump", 6},
})
```
#### Registering Shops via Configuration:
Shops can optionally be configured in `<world_path>/server_shops.json` file. To register a shop, set `type` to "sell" or "buy". `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 an array of products sold at the shop in format "name,value".
*Example:*
```json
[
{
"type":"sell",
"id":"frank",
"name":"Frank's Shop",
"products":[["default:wood",2]]
},
{
"type":"buy",
"id":"julie",
"name":"Julie's Shop",
"products":
[
["default:copper_lump",5],
["default:iron_lump",6],
]
},
]
```
#### Registering Shops via Chat Command:
The `server_shop` chat command is available to administrators with the `server` privilege. This is used for administering shops & updating configuration.
Usage:
```
/server_shop <command> [<params>]
# Commands:
/server_shop reload
- reloads data from configuration file
/server_shop register <id> <type> <name> [product1=value,product2=value,...]
- registers a shop & updates configuration file
- parameters:
- id: shop identifier
- type: can be "buy" or "sell"
- name: displayed shop name ("_" is replaced with " ")
- product list: comma-separated list in format "item=value"
/server_shop unregister <id>
- unregisters a shop & updates configuration file
- parameters:
- id: shop identifier
```
#### Registering Currencies:
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 new currencies in `server_shops.json`, set `type` to "currencies". `value` is a table of item names & worth:
```json
{
"type":"currencies",
"value":
{
"currency:minegeld":1,
"currency:minegeld_5":5,
},
},
```
You can also register a currency suffix to be displayed in the formspec. Simply set the string 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",
},
```
By default, if the [currency][mod.currency] mod is installed, the minegeld notes will be registered as currency. This can be disabled by setting `server_shop.use_currency_defaults` to `false` in `minetest.conf`.
#### Setting up Shops in Game:
Server admins use the chat command `/giveme server_shop:shop_small` or `/giveme server_shop:shop_large` 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 registered shop ID you want associated with this node ("frank" or "julie" for the examples above) & the list will be populated with the registered products & prices.
#### Using Seller Shops:
To make purchases, player first deposits registered currency items into the deposit slot. Select an item in the products list & press the "Buy" button. If there is adequate money deposited, player will receive the item & the cost will be deducted from the deposited amount. To retrieve any money not spent, press the "Refund" button. If the formspec is closed while there is still a deposit balance, the remaining money will be refunded back to the player. If there is not room in the player's inventory, the remaining balance will be dropped on the ground.
#### Using Buyer Shops:
For buyer shops, the product list shows what items can be sold to this shop & how much money a player will receive for each item. To sell to the shop, place an item in the deposit slot. The slot will only accept items that the owner will purchase. Press the "Sell" button to recieve the value of the item(s).
### Licensing:
- Code: [MIT](LICENSE.txt)
- Textures: CC0
### Dependencies:
- Required:
- simple_models
- wdata
- Optional:
- [currency][mod.currency]
- sounds
### Links:
- [![ContentDB](https://content.minetest.net/packages/AntumDeluge/server_shop/shields/title/)](https://content.minetest.net/packages/AntumDeluge/server_shop/)
- [GitHub repo](https://github.com/AntumMT/mod-server_shop)
- [Forum](https://forum.minetest.net/viewtopic.php?t=26645)
- [Reference](https://antummt.github.io/mod-server_shop/reference/latest/)
- [Changelog](changelog.txt)
- [TODO](TODO.txt)
[mod.currency]: https://content.minetest.net/packages/VanessaE/currency/

20
TODO.txt Normal file
View file

@ -0,0 +1,20 @@
TODO:
- Security:
- might be an issue if admin changes shop ID while player is using
- Functionality:
- optimize how refunds are given (e.g. if there is no room for 50s, but room for 1s, then give out 1s instead)
- Aestethics:
- make colorable with unifieddyes
- fix stepping sound for "large" node
- Misc.:
- make usable with "folks" mod
- set shop name from formspec instead of registration so shops with different names can use same content
- add player shops
- fix so unknown items don't mess up shop (may become a problem when shops are registered live)
- allow shops to be configured live
- register chat command "/server_shop" to register live
- "/server_shop add <buyer>|<seller> <id> <item> <price>"
- "/server_shop remove <buyer>|<seller> <id> <item>
- use a spinner to select custom amount (example in terraform: https://github.com/x2048/terraform/blob/4b36d36/init.lua#L316 )
- support groups in buyer shops

108
api.lua Normal file
View file

@ -0,0 +1,108 @@
--- Server Shops API
--
-- @topic api.lua
local ss = server_shop
local S = core.get_translator(ss.modname)
local registered_currencies = {}
ss.currency_suffix = nil
ss.currency_is_registered = function()
for k, v in pairs(registered_currencies) do return true end
return false
end
ss.get_currencies = function()
return table.copy(registered_currencies)
end
ss.register_currency = function(item, value)
if not core.registered_items[item] then
ss.log("warning", "Registriere unbekanntes Item als Währung: " .. item)
end
value = tonumber(value)
if not value or value <= 0 then
ss.log("error", "Währungswert für " .. item .. " muss eine positive Zahl sein.")
return
end
registered_currencies[item] = value
ss.log("action", item .. " als Währung mit Wert " .. value .. " registriert.")
end
if ss.use_currency_defaults then
if not core.get_modpath("currency") then
ss.log("warning", "Mod 'currency' nicht gefunden, Standardwährung wird nicht geladen.")
else
local all_currency = {
{"currency:minegeld", 100}, {"currency:minegeld_5", 500},
{"currency:minegeld_10", 1000}, {"currency:minegeld_50", 5000},
{"currency:minegeld_100", 10000}, {"currency:minegeld_cent_5", 5},
{"currency:minegeld_cent_10", 10}, {"currency:minegeld_cent_25", 25},
}
for _, c in ipairs(all_currency) do
ss.register_currency(c[1], c[2])
end
ss.currency_suffix = "MG"
end
end
ss.get_shop = function(pos)
if not pos then return nil end
local meta = core.get_meta(pos)
if meta:get_string("owner") == "" then return nil end
return {
name = meta:get_string("name"),
owner = meta:get_string("owner"),
prices = core.deserialize(meta:get_string("prices")) or {},
inv = meta:get_inventory(),
}
end
ss.update_infotext = function(pos)
local shop = ss.get_shop(pos)
if not shop then return end
local text = shop.name
local inv_list = shop.inv:get_list("main")
local inv_size = shop.inv:get_size("main")
for i=1, inv_size do
local item = inv_list[i]
local price_int = shop.prices[i]
if not item:is_empty() and price_int and price_int > 0 then
local count = item:get_count()
local description = item:get_description()
local price_str = string.format("%.2f", price_int / 100)
text = text .. "\n" .. count .. "x " .. description .. ": " .. price_str .. " " .. (ss.currency_suffix or "")
end
end
core.get_meta(pos):set_string("infotext", text)
local pos_top = {x=pos.x, y=pos.y + 1, z=pos.z}
if core.get_node(pos_top).name == ss.modname..":shop_large_top" then
core.get_meta(pos_top):set_string("infotext", text)
end
end
ss.is_shop_admin = function(player)
if not player then return false end
return core.check_player_privs(player, "server")
end
ss.is_shop_owner = function(pos, player)
if not player or not pos then
return false
end
local player_name = player:get_player_name()
local meta = core.get_meta(pos)
local owner_name = meta:get_string("owner")
local is_owner = (player_name == owner_name)
return is_owner
end

90
changelog.txt Normal file
View file

@ -0,0 +1,90 @@
v1.6.2
------
- updated for simple_models v2021-08-26
v1.6.1
------
- updated for simple_models v2021-08-23
v1.6
----
- model moved to simple_models mod
- added node sounds
- uses wdata for reading/writing shops config
- added "server_shop" chat command:
- reload: reloads shops configuration
- register: registers new shop & updates configuration
- unregister: unregister shop id & updates configuration
v1.5
----
- added "textdomain" line to localization template
- added Spanish translation
- added "tall" shop nodes
- fixed buyer shop not being registered when currency & default mods not available
- "buy" & "sell" functions are deteremined by shop "type" instead of separate registered nodes
- default currencies not registered if currency mod not available
- "get_currencies", "get_shops", & "get_shop" return table copy
- added methods:
- server_shop.unregister
- server_shop.shop_type
v1.4
----
- fixed custom suffix not reflected in product list & messages
- custom quantity can be set
- unregistered items are pruned from shops after server startup
- fixed buyer shop not refunding on formspec close
- added setting to disable auto-refunding on formspec close
- currencies can be registered with any whole number value
- "currencies" key in server_shops.json used to register multiple currencies
- buyer shops now have "sell" button instead of automatically selling when item is dropped
- added localization support
- money is placed directly into player inventory when selling instead of using deposit medium
v1.3
----
- added buyer shops
- changed json product list type to indexed array
v1.2
----
- 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
v1.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
v1.0
----
- created node
- created simple textures
- formspec displays items & prices of associated shop
- minegeld notes can be deposited & refunded
- shops are configured from world directory
- players with "server" priv or node owners can set ID
- players with "server" priv or node owners can dig
- implemented deposit, purchase, & refund functionality

0
command.lua Normal file
View file

80
deposit.lua Normal file
View file

@ -0,0 +1,80 @@
--- Server Shops Deposit Logics
--
-- @topic deposit.lua
local ss = server_shop
local transaction = dofile(ss.modpath .. "/transaction.lua")
-- Einzahl-Slot für die Kassenreserve des Besitzers (unverändert)
core.create_detached_inventory(ss.modname .. ":cash_deposit", {
on_put = function(inv, listname, index, stack, player)
local pmeta = player:get_meta()
local pos = core.deserialize(pmeta:get_string(ss.modname .. ":pos"))
if not pos then return stack:get_count() end
if ss.is_shop_owner(pos, player) or ss.is_shop_admin(player) then
local value = transaction.calculate_currency_value(stack)
if value > 0 then
local shop_meta = core.get_meta(pos)
shop_meta:set_int("cash_reserve", shop_meta:get_int("cash_reserve") + value)
inv:set_stack(listname, index, nil)
ss.show_formspec(pos, player)
return 0
end
end
return stack:get_count()
end,
}):set_size("main", 1)
-- Einzahl-Slot für das Guthaben des Kunden (angepasst)
core.create_detached_inventory(ss.modname .. ":customer_deposit", {
on_put = function(inv, listname, index, stack, player)
local pmeta = player:get_meta()
local pos = core.deserialize(pmeta:get_string(ss.modname .. ":pos"))
local stack_name = stack:get_name()
if stack_name == "bank_accounts:debit_card" or stack_name == "bank_accounts:credit_card" then
local current_credit = pmeta:get_int(ss.modname .. ":session_credit") or 0
if current_credit > 0 then
local refund_stacks, remainder = transaction.calculate_refund(current_credit)
for _, r_stack in ipairs(refund_stacks) do
transaction.give_product(player, r_stack)
end
end
pmeta:set_string(ss.modname..":payment_method", stack_name)
pmeta:set_int(ss.modname..":session_credit", 0)
if pos then
ss.show_formspec(pos, player)
end
return stack:get_count()
end
local value = transaction.calculate_currency_value(stack)
if value > 0 then
pmeta:set_string(ss.modname..":payment_method", "cash")
pmeta:set_int(ss.modname .. ":session_credit", (pmeta:get_int(ss.modname .. ":session_credit") or 0) + value)
inv:set_stack(listname, index, nil)
if pos then
ss.show_formspec(pos, player)
end
return 0
end
return stack:get_count()
end,
on_take = function(inv, listname, index, stack, player)
local pmeta = player:get_meta()
local pos = core.deserialize(pmeta:get_string(ss.modname .. ":pos"))
if stack:get_name() == "bank_accounts:debit_card" or stack:get_name() == "bank_accounts:credit_card" then
pmeta:set_string(ss.modname..":payment_method", nil)
if pos then
ss.show_formspec(pos, player)
end
end
return stack
end,
}):set_size("main", 1)

229
formspec.lua Normal file
View file

@ -0,0 +1,229 @@
--- Server Shops Formspec
--
-- @topic formspec
local ss = server_shop
local S = core.get_translator(ss.modname)
local transaction = dofile(ss.modpath .. "/transaction.lua")
ss.get_formspec = function(pos, player)
local shop = ss.get_shop(pos)
if not shop then
return "size[5,1]label[0,0;" .. S("Ungültiger Shop!") .. "]"
end
local pmeta = player:get_meta()
local is_owner = ss.is_shop_owner(pos, player) or ss.is_shop_admin(player)
local current_tab = pmeta:get_string(ss.modname .. ":tab")
if current_tab == "" and is_owner then current_tab = "inventory" end
local inv_size = shop.inv:get_size("main")
local fs_width = 14
local fs_height = 12.5 -- Angepasst für 20 slots
local pos_fs = pos.x .. "," .. pos.y .. "," .. pos.z
local formspec = "formspec_version[4]" .. "size[" .. fs_width .. "," .. fs_height .. "]" .. "label[0.5,0.4;" .. core.formspec_escape(shop.name) .. "]"
if is_owner then
formspec = formspec .. "tabheader[0.2,0;tabs;" .. S("Inventar & Preise") .. "," .. S("Einstellungen") .. "," .. S("Kundenansicht") .. ";" .. (current_tab == "inventory" and "1" or current_tab == "settings" and "2" or "3") .. ";true;true]"
if current_tab == "inventory" then
formspec = formspec .. "label[0.5,0.8;" .. S("Shop-Inventar") .. "]"
.. "list[nodemeta:"..pos_fs..";main;0.5,1.2;5,1;0]"
.. "list[nodemeta:"..pos_fs..";main;7.5,1.2;5,1;5]"
for i=1, 5 do
local x = 0.5 + (i-1) * 1.25; local price_int = shop.prices[i] or 0; local price_str = (price_int > 0) and string.format("%.2f", price_int / 100) or ""
formspec = formspec .. "field["..x..",2.3;1.0,1;price_"..i..";;"..price_str.."]"
end
for i=6, 10 do
local x = 7.5 + (i-6) * 1.25; local price_int = shop.prices[i] or 0; local price_str = (price_int > 0) and string.format("%.2f", price_int / 100) or ""
formspec = formspec .. "field["..x..",2.3;1.0,1;price_"..i..";;"..price_str.."]"
end
if inv_size > 10 then
formspec = formspec .. "list[nodemeta:"..pos_fs..";main;0.5,3.7;5,1;10]"
.. "list[nodemeta:"..pos_fs..";main;7.5,3.7;5,1;15]"
for i=11, 15 do
local x = 0.5 + (i-11) * 1.25; local price_int = shop.prices[i] or 0; local price_str = (price_int > 0) and string.format("%.2f", price_int / 100) or ""
formspec = formspec .. "field["..x..",4.8;1.0,1;price_"..i..";;"..price_str.."]"
end
for i=16, 20 do
local x = 7.5 + (i-16) * 1.25; local price_int = shop.prices[i] or 0; local price_str = (price_int > 0) and string.format("%.2f", price_int / 100) or ""
formspec = formspec .. "field["..x..",4.8;1.0,1;price_"..i..";;"..price_str.."]"
end
end
elseif current_tab == "settings" then
formspec = formspec .. "label[0.5,0.8;" .. S("Shop-Name") .. "]" .. "field[0.5,1.2;7,0.7;shop_name;;" .. shop.name .. "]"
end
end
if not is_owner or current_tab == "customer" then
formspec = formspec .. "label[0.5,0.8;"..S("Zum Kaufen auf einen Artikel klicken:").."]"
local inv_list = shop.inv:get_list("main")
for i=1, 10 do
local item = inv_list[i]
if not item:is_empty() and shop.prices[i] and shop.prices[i] > 0 then
local is_row_1 = (i <= 5); local x = (is_row_1 and 0.5 or 7.5) + ((is_row_1 and i-1 or i-6) * 1.25)
local y_image = 1.2; local y_price = y_image + 1.2; local y_currency = y_price + 0.3
local count = item:get_count(); local price_str = string.format("%.2f", shop.prices[i] / 100)
formspec = formspec .. "item_image_button["..x..","..y_image..";1,1;"..item:get_name()..";buy_slot_"..i..";]" .. "label["..x..","..y_image..";" .. count .. "]" .. "label["..x..","..y_price..";" .. price_str .. "]" .. "label["..x..","..y_currency..";" .. (ss.currency_suffix or "") .. "]"
end
end
if inv_size > 10 then
for i=11, 20 do
local item = inv_list[i]
if not item:is_empty() and shop.prices[i] and shop.prices[i] > 0 then
local is_row_1 = (i <= 15); local x = (is_row_1 and 0.5 or 7.5) + ((is_row_1 and i-11 or i-16) * 1.25)
local y_image = 3.7; local y_price = y_image + 1.2; local y_currency = y_price + 0.3
local count = item:get_count(); local price_str = string.format("%.2f", shop.prices[i] / 100)
formspec = formspec .. "item_image_button["..x..","..y_image..";1,1;"..item:get_name()..";buy_slot_"..i..";]" .. "label["..x..","..y_image..";" .. count .. "]" .. "label["..x..","..y_price..";" .. price_str .. "]" .. "label["..x..","..y_currency..";" .. (ss.currency_suffix or "") .. "]"
end
end
end
end
local player_inv_y = 7.5
formspec = formspec .. "label[0.5," .. (player_inv_y - 0.4) .. ";" .. S("Dein Inventar") .. "]" .. "list[current_player;main;0.5," .. player_inv_y .. ";8,4;]"
local right_col_x = 10.8
if is_owner then
if current_tab == "inventory" then
local cash_reserve_int = core.get_meta(pos):get_int("cash_reserve"); local cash_reserve_str = string.format("%.2f", cash_reserve_int / 100)
formspec = formspec .. "label["..right_col_x..", " .. (player_inv_y - 0.4) .. ";" .. S("Shop-Kasse:") .. "]"
.. "label["..right_col_x..", " .. player_inv_y .. ";" .. cash_reserve_str .. " " .. (ss.currency_suffix or "") .. "]"
.. "button["..right_col_x..", " .. (player_inv_y + 0.5) .. ";2.5,0.8;refund_reserve;"..S("Auszahlen").."]"
end
formspec = formspec .. "button["..right_col_x.."," .. (fs_height - 2) .. ";2.5,0.8;save_all;" .. S("Speichern") .. "]"
end
if not is_owner or (is_owner and current_tab == "customer") then
local payment_method = pmeta:get_string(ss.modname..":payment_method")
local credit_str = "0.00"
if bank_accounts and payment_method == "bank_accounts:debit_card" then
local balance = bank_accounts.get_balance(player:get_player_name())
credit_str = string.format("%.2f", balance)
elseif bank_accounts and payment_method == "bank_accounts:credit_card" then
local credit = bank_accounts.get_credit(player:get_player_name())
credit_str = string.format("%.2f", credit)
else
local session_credit_int = pmeta:get_int(ss.modname..":session_credit") or 0
credit_str = string.format("%.2f", session_credit_int / 100)
end
formspec = formspec .. "label["..right_col_x..", " .. (player_inv_y - 0.4) .. ";" .. S("Einzahlung (MG/Karte):") .. "]"
.. "list[detached:"..ss.modname..":customer_deposit;main;"..right_col_x..","..player_inv_y..";1,1;]"
.. "button["..right_col_x..",".. (player_inv_y + 1.2) ..";2.5,0.8;refund_session;"..S("Auszahlen").."]"
.. "label["..right_col_x + 1.2 ..",".. (player_inv_y + 0.2) ..";" .. S("Guthaben:") .. "]"
.. "label["..right_col_x + 1.2 ..",".. (player_inv_y + 0.6) ..";" .. credit_str .. " " .. (ss.currency_suffix or "") .. "]"
end
formspec = formspec .. "button_exit[" .. right_col_x .. "," .. (fs_height - 1) .. ";2.5,0.8;close;" .. S("Schließen") .. "]"
return formspec
end
ss.show_formspec = function(pos, player)
-- Die Reset-Logik wurde in node.lua -> on_rightclick verschoben
core.show_formspec(player:get_player_name(), ss.modname..":shop", ss.get_formspec(pos, player))
end
core.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= ss.modname..":shop" then return end
if fields.quit then
local pmeta = player:get_meta()
local pos = core.deserialize(pmeta:get_string(ss.modname .. ":pos"))
if ss.refund_on_close and pos then
-- Gilt für jeden Spieler mit Guthaben, auch den Besitzer
local total = pmeta:get_int(ss.modname..":session_credit") or 0
if total > 0 then
local refund_stacks, remainder = transaction.calculate_refund(total)
for _, stack in ipairs(refund_stacks) do transaction.give_product(player, stack) end
pmeta:set_int(ss.modname..":session_credit", remainder)
end
local deposit_inv = core.get_inventory({type="detached", name=ss.modname..":customer_deposit"})
if deposit_inv then
local stack = deposit_inv:get_stack("main", 1)
if not stack:is_empty() and (stack:get_name() == "bank_accounts:debit_card" or stack:get_name() == "bank_accounts:credit_card") then
transaction.give_product(player, stack)
deposit_inv:set_stack("main", 1, nil)
pmeta:set_string(ss.modname..":payment_method", nil)
end
end
end
return
end
local pmeta = player:get_meta()
local pos = core.deserialize(pmeta:get_string(ss.modname .. ":pos"))
if not pos then return end
local is_owner = ss.is_shop_owner(pos, player) or ss.is_shop_admin(player)
-- KORREKTUR: inv_size wird jetzt nur einmal hier geholt, um für alle Schleifen gültig zu sein
local inv = core.get_meta(pos):get_inventory()
local inv_size = inv and inv:get_size("main") or 10
if is_owner and fields.tabs then
local new_tab = "inventory"; if fields.tabs == "2" then new_tab = "settings" elseif fields.tabs == "3" then new_tab = "customer" end
pmeta:set_string(ss.modname .. ":tab", new_tab)
end
for i = 1, inv_size do
if fields["buy_slot_"..i] then
transaction.execute_purchase(pos, player, i)
ss.show_formspec(pos, player)
return
end
end
if fields.refund_session then
local total = pmeta:get_int(ss.modname..":session_credit") or 0
if total > 0 then
local refund_stacks, remainder = transaction.calculate_refund(total);
for _, stack in ipairs(refund_stacks) do transaction.give_product(player, stack) end
pmeta:set_int(ss.modname..":session_credit", remainder)
end
end
if is_owner then
local shop_meta = core.get_meta(pos)
if fields.save_all then
local current_tab = pmeta:get_string(ss.modname .. ":tab") or "inventory"
if current_tab == "settings" then
if fields.shop_name then
shop_meta:set_string("name", fields.shop_name or "")
end
elseif current_tab == "inventory" then
local prices = core.deserialize(shop_meta:get_string("prices")) or {}
local inv_list = inv:get_list("main")
for i = 1, inv_size do
if not inv_list[i]:is_empty() then
if fields["price_"..i] then
local price_input = fields["price_"..i]
if price_input and price_input ~= "" then
local price_num = tonumber((price_input:gsub(",", ".")))
if price_num and price_num > 0 then
prices[i] = math.floor(price_num * 100 + 0.5)
end
end
end
else
prices[i] = nil
end
end
shop_meta:set_string("prices", core.serialize(prices))
end
ss.update_infotext(pos)
end
if fields.refund_reserve then
local total = shop_meta:get_int("cash_reserve")
if total > 0 then
local refund_stacks, remainder = transaction.calculate_refund(total)
for _, stack in ipairs(refund_stacks) do transaction.give_product(player, stack) end
shop_meta:set_int("cash_reserve", remainder)
end
end
end
ss.show_formspec(pos, player)
end)

30
init.lua Normal file
View file

@ -0,0 +1,30 @@
server_shop = {}
local ss = server_shop
ss.modname = core.get_current_modname()
ss.modpath = core.get_modpath(ss.modname)
-- Log-Funktion für saubere Nachrichten
function ss.log(level, msg)
if not msg then
msg = level
level = "action"
end
core.log(level, "[" .. ss.modname .. "] " .. msg)
end
-- Definiert die Ladereihenfolge der einzelnen Skript-Dateien
local scripts = {
"settings",
"api",
"transaction",
"deposit",
"formspec",
"node",
}
for _, script in ipairs(scripts) do
dofile(ss.modpath .. "/" .. script .. ".lua")
end
ss.log("Mod '"..ss.modname.."' wurde erfolgreich geladen.")

46
locale/server_shop.es.tr Normal file
View file

@ -0,0 +1,46 @@
# textdomain:server_shop
# Translators: Jordan Irwin (AntumDeluge)
Shop=Tienda
Buy=Comprar
Sell=Vender
Refund=Reembolsar
Close=Cerrar
Set ID=Pon ID
Deposited: @1=Depositado: @1
Deposited: @1 @2=Depositado: @1 @2
You haven't deposited enough money.=No has depositado suficiente dinero.
You purchased @1 @2 for @3 @4.=Compraste @1 @2 por @3 @4.
You sold @1 @2 for @3 @4.=Vendiste @1 @2 por @3 @4.
WARNING: @1 @2 was dropped on the ground.=AVISO: @1 @2 se cayó en la tierra.
# chat commands
Manage shops configuration.=Administrar configuración de tiendas.
Usage:=Uso:
command=orden
params=parámetros
Must provide a command: @1=Debe producir un orden: @1
"@1" command takes no parameters.=Orden "@1" no requiere parametros.
Too many parameters.=Demasiado parámetros.
Unknown command: @1=Orden desconocido: @1
Must provide ID.=Se requiere ID.
# reload command
Shops configuration loaded.=Configuración de tiendas cargada.
# register command
ID=
name=nombre
product1@=value,product2@=value,...=producto1@=valor,producto2@=valor,...
Must provide type.=Se requiere tipo.
Must provide name.=Se requiere nombre.
Shop type must be "@1" or "@2".=Tipo de tienda debe ser "@1" o "@2".
"@1" is not a recognized item.="@1" no es objeto conocido.
Item value must be a number.=Valor de objeto debe ser número.
Registered shop with ID: @1=Agregó al registro tienda con ID: @1
# unregister command
Cannot unregister shop with ID: @1=No puede quitar del registro tienda con ID: @1
Unregistered shop with ID: @1=Se quitó del registro tienda con ID: @1

46
locale/template.txt Normal file
View file

@ -0,0 +1,46 @@
# textdomain:server_shop
# Translators:
Shop=
Buy=
Sell=
Refund=
Close=
Set ID=
Deposited: @1=
Deposited: @1 @2=
You haven't deposited enough money.=
You purchased @1 @2 for @3 @4.=
You sold @1 @2 for @3 @4.=
WARNING: @1 @2 was dropped on the ground.=
# chat commands
Manage shops configuration.=
Usage:=
command=
params=
Must provide a command: @1=
"@1" command takes no parameters.=
Too many parameters.=
Unknown command: @1=
Must provide ID.=
# reload command
Shops configuration loaded.=
# register command
ID=
name=
product1@=value,product2@=value,...=
Must provide type.=
Must provide name.=
Shop type must be "@1" or "@2".=
"@1" is not a recognized item.=
Item value must be a number.=
Registered shop with ID: @1=
# unregister command
Cannot unregister shop with ID: @1=
Unregistered shop with ID: @1=

9
mod.conf Normal file
View file

@ -0,0 +1,9 @@
name = server_shop
title = Server Shop
description = Player-managed shops with limited inventory.
version = 0.4
author = Rage87
min_minetest_version = 5.11
depends = currency
optional_depends = sounds, bank_accounts
release = 1000

121
node.lua Normal file
View file

@ -0,0 +1,121 @@
--- Server Shops Nodes
--
-- @topic nodes
local ss = server_shop
local S = core.get_translator(ss.modname)
-- #################################################################
-- ## GEMEINSAME FUNKTIONEN
-- #################################################################
local function after_place_node(pos, placer, itemstack)
local meta = core.get_meta(pos)
if itemstack:get_name() == "server_shop:shop_large" then
meta:get_inventory():set_size("main", 20)
else
meta:get_inventory():set_size("main", 10)
end
meta:set_string("owner", placer:get_player_name())
meta:set_string("prices", core.serialize({}))
meta:set_int("cash_reserve", 0)
meta:set_string("name", S("@1's Shop", placer:get_player_name()))
ss.update_infotext(pos)
end
local function on_rightclick(pos, node, player, itemstack, pointed_thing)
if core.get_node({x=pos.x, y=pos.y-1, z=pos.z}).name == ss.modname..":shop_large_bottom" then
pos.y = pos.y - 1
end
local pmeta = player:get_meta()
pmeta:set_string(ss.modname .. ":pos", core.serialize(pos))
if not (ss.is_shop_owner(pos, player) or ss.is_shop_admin(player)) then
pmeta:set_int(ss.modname..":session_credit", 0)
end
ss.show_formspec(pos, player)
end
local function can_dig(pos, player)
local owner_pos = {x=pos.x, y=pos.y, z=pos.z}
if core.get_node({x=pos.x, y=pos.y-1, z=pos.z}).name == ss.modname..":shop_large_bottom" then
owner_pos.y = pos.y - 1
end
return ss.is_shop_owner(owner_pos, player) or ss.is_shop_admin(player)
end
-- #################################################################
-- ## SHOP-DEFINITIONEN
-- #################################################################
-- Kleiner Shop (1 Block)
core.register_node(ss.modname..":shop_small", {
description = S("Shop"),
paramtype2 = "facedir",
groups = {choppy=1, oddly_breakable_by_hand=1},
sounds = default.node_sound_stone_defaults(),
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",
},
after_place_node = after_place_node,
on_rightclick = on_rightclick,
can_dig = can_dig,
})
-- Großer Shop, unterer Teil (enthält alle Daten)
core.register_node(ss.modname..":shop_large_bottom", {
description = S("Großer Shop"),
paramtype2 = "facedir",
drop = ss.modname..":shop_large_bottom",
groups = {choppy=1, oddly_breakable_by_hand=1, not_in_creative_inventory=1},
sounds = default.node_sound_stone_defaults(),
tiles = {
"server_shop_side.png", "server_shop_side.png", "server_shop_side.png",
"server_shop_side.png", "server_shop_side.png", "server_shop_front_large_bottom.png",
},
after_place_node = function(pos, placer)
local meta = core.get_meta(pos)
meta:get_inventory():set_size("main", 20)
meta:set_string("owner", placer:get_player_name())
meta:set_string("prices", core.serialize({}))
meta:set_int("cash_reserve", 0)
meta:set_string("name", S("@1's Großer Shop", placer:get_player_name()))
ss.update_infotext(pos)
local above = {x=pos.x, y=pos.y+1, z=pos.z}
core.set_node(above, {name=ss.modname..":shop_large_top", param2=core.get_node(pos).param2})
end,
on_destruct = function(pos)
local above = {x=pos.x, y=pos.y+1, z=pos.z}
if core.get_node(above).name == ss.modname..":shop_large_top" then
core.set_node(above, {name="air"})
end
end,
on_rightclick = on_rightclick,
can_dig = can_dig,
})
-- Großer Shop, oberer Teil
core.register_node(ss.modname..":shop_large_top", {
description = S("Großer Shop"),
paramtype2 = "facedir",
diggable = false,
drop = "",
groups = {not_in_creative_inventory=1},
sounds = default.node_sound_stone_defaults(),
tiles = {
"server_shop_side.png", "server_shop_side.png", "server_shop_side.png",
"server_shop_side.png", "server_shop_side.png", "server_shop_front_large_top.png",
},
-- KORREKTUR: on_destruct-Funktion hier komplett entfernt, um die Schleife zu verhindern.
on_rightclick = on_rightclick,
})
-- Alias für den give-Befehl
core.register_alias(ss.modname..":shop", ss.modname..":shop_small")
core.register_alias(ss.modname..":shop_large", ss.modname..":shop_large_bottom")

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

7
settings.lua Normal file
View file

@ -0,0 +1,7 @@
--- Server Shops Settings
--
-- @topic settings.lua
-- Liest Einstellungen aus der minetest.conf oder verwendet Standardwerte
server_shop.use_currency_defaults = core.settings:get_bool("server_shop.use_currency_defaults", true)
server_shop.refund_on_close = core.settings:get_bool("server_shop.refund_on_close", true)

6
settingtypes.txt Normal file
View file

@ -0,0 +1,6 @@
# If currency mod is installed, automatically set up values for minegeld notes.
server_shop.use_currency_defaults (Use currency mod defaults) bool true
# Refunds deposited money when formspec is closed.
server_shop.refund_on_close (Refund on close) bool true

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

131
transaction.lua Normal file
View file

@ -0,0 +1,131 @@
--- Server Shops Transaction Logic
--
-- @topic transaction.lua
local ss = server_shop
local S = core.get_translator(ss.modname)
local transaction = {}
function transaction.give_product(player, product)
if not player or not product then return end
local pinv = player:get_inventory()
if not pinv:room_for_item("main", product) then
core.chat_send_player(player:get_player_name(), S("WARNUNG: @1 @2 wurde auf den Boden fallen gelassen.", product:get_count(), product:get_description()))
core.item_drop(product, player, player:get_pos())
else
pinv:add_item("main", product)
end
end
function transaction.calculate_refund(total)
local currencies = ss.get_currencies()
local keys = {}; for k in pairs(currencies) do table.insert(keys, k) end
table.sort(keys, function(kL, kR) return currencies[kL] > currencies[kR] end)
local refund = {}; local remain = total
for _, k in ipairs(keys) do
local v = currencies[k]
if v > 0 then
local count = math.floor(remain / v)
if count > 0 then
local stack = ItemStack(k); stack:set_count(count); table.insert(refund, stack)
remain = remain - (count * v)
end
end
end
return refund, remain
end
function transaction.calculate_currency_value(stack)
local value = 0
for c, v in pairs(ss.get_currencies()) do
if stack:get_name() == c then
return stack:get_count() * v
end
end
return 0
end
function transaction.execute_purchase(pos, player, slot_index)
local pmeta = player:get_meta()
local player_inv = player:get_inventory()
local shop = ss.get_shop(pos)
if not shop then return end
local quantity = 1
-- KORREKTUR: Die fehlerhafte `slot_index - 1` Logik wurde entfernt.
-- Wir verwenden `slot_index` direkt, so wie es in der funktionierenden Version war.
local item_to_buy = shop.inv:get_stack("main", slot_index)
local price_per_item = shop.prices[slot_index]
if item_to_buy:is_empty() or not price_per_item or price_per_item <= 0 then
core.chat_send_player(player:get_player_name(), S("Dieser Artikel ist nicht verfügbar."))
return
end
if item_to_buy:get_count() < quantity then
core.chat_send_player(player:get_player_name(), S("Nicht genügend Artikel auf Lager."))
return
end
local purchase_stack = ItemStack(item_to_buy:get_name())
purchase_stack:set_count(quantity)
if not player_inv:room_for_item("main", purchase_stack) then
core.chat_send_player(player:get_player_name(), S("Nicht genügend Platz im Inventar."))
return
end
local total_cost_cents = price_per_item * quantity
local player_name = player:get_player_name()
local payment_method = pmeta:get_string(ss.modname..":payment_method")
if payment_method == "bank_accounts:debit_card" or payment_method == "bank_accounts:credit_card" then
local account_balance
if payment_method == "bank_accounts:debit_card" then
account_balance = bank_accounts.get_balance(player_name)
else -- credit_card
account_balance = bank_accounts.get_credit(player_name)
end
local total_cost_float = total_cost_cents / 100
if account_balance < total_cost_float then
core.chat_send_player(player_name, S("Dein Konto ist nicht ausreichend gedeckt."))
return
end
if payment_method == "bank_accounts:debit_card" then
bank_accounts.add_balance(player_name, -total_cost_float, "Shop-Kauf", shop.name, shop.owner)
else -- credit_card
bank_accounts.add_credit(player_name, -total_cost_float, "Shop-Kauf", shop.name, shop.owner)
end
else
local credit = pmeta:get_int(ss.modname .. ":session_credit") or 0
if credit < total_cost_cents then
local needed = string.format("%.2f", (total_cost_cents - credit) / 100)
core.chat_send_player(player_name, S("Du hast nicht genug Geld eingezahlt. (Es fehlen @1)", needed .. " " .. ss.currency_suffix))
return
end
pmeta:set_int(ss.modname .. ":session_credit", credit - total_cost_cents)
end
local shop_meta = core.get_meta(pos)
local current_reserve = shop_meta:get_int("cash_reserve")
shop_meta:set_int("cash_reserve", current_reserve + total_cost_cents)
item_to_buy:set_count(item_to_buy:get_count() - quantity)
-- KORREKTUR: `set_stack` verwendet ebenfalls den direkten `slot_index`.
shop.inv:set_stack("main", slot_index, item_to_buy)
transaction.give_product(player, purchase_stack)
local cost_str = string.format("%.2f", total_cost_cents / 100)
core.chat_send_player(player_name, S("Du hast @1 @2 für @3 gekauft.", quantity, item_to_buy:get_description(), cost_str .. " " .. ss.currency_suffix))
ss.update_infotext(pos)
end
return transaction