21 Commits

Author SHA1 Message Date
868213081f Fix claim of bought items 2022-07-10 19:40:52 +02:00
969571f559 Fix crash if buyer out of funds 2022-07-10 18:42:25 +02:00
15bba05412 add search in translated string add translate missing string 2022-07-09 18:02:17 +02:00
10b438c2b7 Merge branch 'master' into translate_search 2022-07-09 17:17:25 +02:00
3517409a1a Delete depends.txt and description.txt, update mod.conf 2022-07-06 19:13:05 +02:00
8430e9fab5 WIP: not tested
add search in translated string
add translate missing string
2022-07-05 03:27:00 +02:00
6989f6a00e Improve market summary displaying long item names 2021-04-11 11:59:43 +02:00
9c8f62320e Fix crash if an item doesn't exist anymore 2021-04-09 17:29:18 +02:00
b22b72f5c2 Add items search filter on Buy tab 2020-09-20 15:03:52 +02:00
ce33c159fd Add french translations 2020-09-11 08:21:51 +02:00
c1a527eeea Ajoute le retrait d'espèces depuis l'ATM 2020-01-07 23:04:59 +01:00
57deb8582e Affiche seulement la première ligne de la description des items 2019-03-09 13:21:29 +01:00
81517a5b3d Merge branch 'nalc' into upgrade 2019-03-08 23:46:27 +01:00
0de7a34fcf Supprime l'argent donné pour les nouveaux joueurs
- Ajoute message de chargement du mod dans le journal "action"
2018-12-31 16:03:36 +01:00
d49a7fdc12 Ajoute la possibilité de déposer de l'argent via l'ATM 2018-12-31 13:36:52 +01:00
73407cd6fa Minor transaction log formatting tweak. 2017-06-03 18:28:17 -05:00
b52e813d45 Bug fix: Failed to detect out_of_funds when placing buy order. 2017-06-03 18:24:37 -05:00
5cfb580545 Update README.md based on UI changes. 2017-06-03 18:12:37 -05:00
141de27c97 Cancel remaining buy order when out of funds. 2017-06-03 18:12:09 -05:00
808b3cbf04 Expand formlib & distinguish items by wear in market summary. 2017-06-03 16:40:32 -05:00
2a5a726cc0 Fix tool wear & metadata bugs and improve the UI. 2017-06-01 05:14:13 -05:00
11 changed files with 1724 additions and 1190 deletions

127
README.md
View File

@ -15,87 +15,86 @@ Nodes
Using the Exchange
==================
Main Screen
Overview
-----------
The first screen you see is where you can search and post new buy/sell orders.
Here is an overview of each element:
- Market Summary - Pressing this will take you to the market summary screen.
- Your Orders - This will take you to a screen where you can view and cancel
your existing orders.
- Item - This field is for entering the item name (e.g. default:cobble) of the
item you want to search or post an order for.
- Amount - This field is for entering how many of the item you want to buy/sell
when posting an order. It has no purpose in searches.
- Select Item - This button takes you to a screen for choosing your item
graphically, instead of manually typing an item name.
- Rate - This field is for entering the desired price per item when posting an
order. For buy orders, this is the maximum price - your order will also accept
items that are cheaper. For sell orders, this is the minimum price - your
order will also accept buyers that are willing to pay more. The Rate field has
no effect on searches
- Search - This button searches existing orders for the selected item. If you
have the "Sell" box checked, it will only display buy orders, and will display
them in descending rate. If you have the box unchecked, it will show sell
orders in order of ascending rate.
- Post Order - This posts a new order for the item with the given amount and
rate. If the "Sell" box is checked, this is a sell order, so the exchange will
remove the items from your inventory. If it's unchecked, you are making a buy
order, so it will deduct credits from your account. If there are already
matching orders, it will immediately fill your order up to the amount possible,
and the remainder will stay as a new order.
- Sell - This checkbox determines what kind of orders to search for, and also
what kind of order you are posting.
- Search Results - This will display the results of your search. Clicking on an
element here will automatically fill the "Amount" and "Rate" fields, so that if
you click "Post Order", it will match the order you clicked.
At the top of the exchange form there are four tabs: Market Summary, Buy, Sell,
and Your Orders. Pressing each tab will take you to the indicated screen as
described below.
Market Summary
--------------
This summarizes the various items available on the exchange. From left to right,
the columns display the item name, the description (what is shown in inventory),
the amount requested by buyers, the maximum rate offered by buyers, the amount
offered by sellers, and the minimum rate offered by sellers. It is updated
periodically.
the tool wear if applicable, the amount requested by buyers, the maximum rate
offered by buyers, the amount offered by sellers, and the minimum rate offered
by sellers. It is updated periodically.
Buy
-----------
The upper-left corner shows the open order book for the selected item,
consisting of the three best sell offers (lowest asking price and least tool
wear) followed by the three best buy offers (highest offer price and highest
acceptable tool wear). To the right is an image of the selected item, entry
fields for the quantity and offer price, and a drop-down field for the desired
tool wear: "New" (no wear), "Good" (up to 10% wear), "Worn" (up to 50% wear),
and "Junk" (any amount of wear). To qualify, a seller must offer an item with
wear equal to or less than the indicated amount (e.g. if you select "Good" you
may receive an item with only 5% wear, but not one 15% worn). Similarly, the
offer price is the highest price you are willing to pay; in practice you may
pay less depending on the open sell orders. Orders are always fulfilled at the
price listed in the open order book.
At the bottom of the form is a grid similar to the creative inventory. You can
page through the available items using the "<<" and ">>" buttons and select the
item you wish to buy simply by clicking on it.
To finalize the order, press the "Place Bid" button. Note that it is permitted
to enter an order which exceeds your available funds at the time the order is
placed. As matching sell orders are located, the order will be fulfulled until
there are insufficient funds to cover the next item, at which point the
remainder of the order will be cancelled.
Sell
-----------
As with the Buy screen, the upper-left corner shows the open order book for
the selected item. Instead of a static image, the selected item appears as a
1x1 inventory. At the bottom of the screen is the standard 8x4 player
inventory. To choose the type and quantity of items to sell, move the items
from your player inventory to the exchange inventory. Below the exchange
inventory is an entry field for the desired asking price. To finalize the
order, press the "Sell" button. As with buy orders, the asking price is the
lowest price you are willing to accept, and you may receive a higher price
depending on the open buy orders.
Unsold items in the exchange inventory are returned to the player inventory
when the form is closed, or to the player's Inbox if the inventory is full.
(However, items in the exchange inventory at the time of a server crash, or
other abnormal condition, may be lost.)
Your Orders
-----------
This screen lets you see and cancel your orders. To cancel an order, click the
order and press the "Cancel" button.
Select Item
-----------
This displays a creative-style inventory menu for selecting an item for searches
or posting orders. To select an item, drag it from the inventory to the box near
the bottom of the form
order and press the "Cancel" button. If the order was sell order, the items
held in "escrow" are returned to your player inventory. If any of the items do
not fit in the inventory they are placed in your Inbox instead to be claimed
later.
Buying/Selling
==============
Once you have opened the exchange, you have a few options. If you don't already
know what you want to buy or sell, you can look at the Market Summary to get a
glance at what people are offering. After you have decided on what you are
going to do, return to the exchange page.
going to do, return to the Buy or Sell page.
If you are selling an item, you should check the "Sell" checkbox. Otherwise,
leave it unchecked. Next, you need to select the item you want to deal in. There
are two ways: typing the item name (e.g. default:cobble) in manually to the item
field, or using the "Select Item" menu. If you haven't already decided on a price,
or you want to make sure your order is filled quickly, you can conduct a search.
To do this, click the "Search" button. This will give you a list of results. If
you checked the "Sell" box, then these will be buy orders, and will show the
maximum price per item each buyer is willing to accept. Otherwise, these will be
sell orders, displaying the minimum price each seller will accept. If you click
on a search result, it will automatically fill your Amount and Rate fields to
match.
The Amount and Rate fields are used to decide how much and how expensively you
want to make your order. When selling, the Rate field is the minimum price you
will accept. When buying, it is the maximum. Once everything is filled out how
you want it, press the "Post Order" button. If there are matching offers (when
you post a buy/sell offer, there are one or more sell/buy offers with a price
at least as good), then that part of your offer will immediately be filled. For
example, if you post a buy order for 10 cobblestone at 5 credits each, and there
is a sell offer for 5 cobblestone 3 credits each, it will give you 5 cobble
immediately, and leave an order on the exchange for 5 more cobblestone.
When selling, the Ask field is the minimum price you will accept for each
item. When buying, the Bid field determines the maximum amount you are willing
to pay. If there are matching offers (i.e. there are one or more offers with a
price at least as good and a compatible tool wear level), then that part of
your offer will immediately be filled. For example, if you post a buy order
for 10 cobblestone at 5 credits each, and there is a sell offer for 5
cobblestone at 3 credits each, it will give you 5 cobble immediately at a
total cost of 15 credits (the order-book price), and leave an order on the
exchange for 5 more cobblestone at 5 credits each.
Once your offer is on the exchange, you can view or cancel it from the "Your
Orders" menu.

329
atm.lua
View File

@ -1,152 +1,197 @@
-- A telling machine. Call this file with the exchange argument.
local exchange = ...
local exchange, formlib = ...
local S = minetest.get_translator("global_exchange")
local atm_form = "global_exchange:atm_form"
local atm_pos = {}
local main_menu =[[
size[6,2]
button[2,0;2,1;info;Account Info]
button[4,0;2,1;wire;Wire Monies]
button[1,1;4,1;transaction_log;Transaction Log]
]]
local unique = (function(unique_num)
return function()
unique_num = unique_num + 1
return unique_num
end
end)(0)
local coins = {
[1] = "bitchange:minecoinblock", [2] = "bitchange:minecoin", [3] = "bitchange:mineninth",
[4] = "maptools:gold_coin", [5] = "maptools:silver_coin", [6] = "maptools:copper_coin"
}
local function logout(x,y)
return "button[" .. x .. "," .. y .. ";2,1;logout;Log Out]"
end
local coins_convert = {
[coins[6]]=1, [coins[5]]=9, [coins[4]]=81, [coins[3]]=729, [coins[2]]=6561, [coins[1]]=59049,
["minercantile:copper_coin"]=1, ["minercantile:silver_coin"]=9, ["minercantile:gold_coin"]=81
}
local function withdraw_fs(fs, p_name, amount)
local spos = atm_pos[p_name].x..","..atm_pos[p_name].y..","..atm_pos[p_name].z
local inv = minetest.get_inventory({ type = "node", pos={x=atm_pos[p_name].x, y=atm_pos[p_name].y, z=atm_pos[p_name].z} })
local function label(x,y,text)
return "label[" .. x .. "," .. y .. ";"
.. minetest.formspec_escape(text) .. "]"
end
local msg = ""
local w_amount = tonumber(amount)
if w_amount and math.floor(w_amount) == w_amount and w_amount > 0 then
local succ, err = exchange:give_credits(p_name, 0 - w_amount, S("Cash withdrawal: (-@1)", w_amount))
if succ then
local index = 1
repeat
local m = math.floor(w_amount / coins_convert[coins[index]])
if m > 0 then
inv:add_item( "main", ItemStack({ name = coins[index], count = m }) )
end
w_amount = w_amount - (coins_convert[coins[index]] * m)
index = index + 1
until ( w_amount == 0)
else
msg = err
end
elseif w_amount then
msg = S("Invalid number ! Must be an Integer > 0")
end
local function field(x,y, w,h, name, label, default)
return "field[" .. x .. "," .. y .. ";" .. w .. "," .. h .. ";"
.. name .. ";" .. minetest.formspec_escape(label) .. ";"
.. minetest.formspec_escape(default) .. "]"
end
local unique_num = 1
local function unique()
local ret = unique_num
unique_num = unique_num + 1
return ret
end
local function info_fs(p_name)
local balance = exchange:get_balance(p_name)
fs:size(8,10)
local fs
if not balance then
fs = label(0.5,0.5, "You don't have an account.")
fs:label(0.5, 0.5, S("You don't have an account."))
else
fs = label(0.5,0.5, "Balance: " .. balance)
fs:label(3,8.9, S("Balance: @1", balance))
fs:field(0.75, 1.25, 3.25, 1, "w_amount", S("Desired amount:"))
fs:button(4, 1, 3.25, 1, "withdraw", S("Get !"))
fs:label(1, 2.25, S("Or deposit your coins to credit your account:"))
fs:list(1,3,6,1, "nodemeta:"..spos, "main")
fs:list(0,4.25, 8,4, "current_player", "main")
fs("listring[current_player;main]"..
"listring[nodemeta:"..spos..";main]"
)
fs:label(0,9.75, msg)
end
return "size[4,3]" .. fs .. logout(0.5,2)
fs:button(0,8.75, 2,1, "logout", S("Log Out"))
end
local function wire_fs(p_name)
local function info_fs(fs, p_name)
local balance = exchange:get_balance(p_name)
local fs = "size[4,5]" .. logout(0,4)
fs:size(4,3)
if not balance then
return fs .. label(0.5,0.5, "You don't have an account.")
if balance then
fs:label(0.5,0.5, S("Balance: @1", balance))
else
fs:label(0.5,0.5, S("You don't have an account."))
end
-- To prevent duplicates
return fs .. field(-100, -100, 0,0, "trans_id", "", unique()) ..
label(0.5,0.5, "Balance: " .. balance) ..
field(0.5,1.5, 2,1, "recipient", "Send to:", "") ..
field(0.5,2.5, 2,1, "amount", "Amount", "") ..
"button[2,4;2,1;send;Send]"
fs:button(1,2, 2,1, "logout", S("Log Out"))
end
local function send_fs(p_name, receiver, amt_str)
local fs = "size[7,3]"
local function wire_fs(fs, p_name)
local balance = exchange:get_balance(p_name)
fs:size(4,5)
if balance then
-- To detect duplicate/stale form submission
fs:field(-100, -100, 0,0, "trans_id", "", unique())
fs:label(0.50,0.325, S("Balance: @1", balance))
fs:field(0.75,1.750, 3,1, "recipient", S("Send to:"), "")
fs:field(0.75,3.000, 3,1, "amount", S("Amount"), "")
fs:button(0,4.25, 2,1, "logout", S("Log Out"))
fs:button(2,4.25, 2,1, "send", S("Send"))
else
fs:button(0,4, 2,1, "logout", S("Back"))
fs:label(0.5,0.5, S("You don't have an account."))
end
end
local function send_fs(fs, p_name, receiver, amt_str)
fs:size(10,3)
fs:button(4,2, 2,1, "wire", S("Back"))
local amt = tonumber(amt_str)
local msg = nil
if not amt or amt <= 0 then
return fs .. label(0.5,0.5, "Invalid transfer amount.") ..
"button[0.5,2;2,1;wire;Back]"
msg = S("Invalid transfer amount.")
else
local succ, err = exchange:transfer_credits(p_name, receiver, amt)
if not succ then
msg = "Error: " .. err
else
msg = "Successfully sent " .. amt ..
" credits to " .. receiver .. "."
end
end
local succ, err = exchange:transfer_credits(p_name, receiver, amt)
if not succ then
return fs .. label(0.5,0.5, "Error: " .. err) ..
"button[0.5,2;2,1;wire;Back]"
end
return fs.. label(0.5,0.5, "Successfully sent " ..
amt .. " credits to " .. receiver) ..
"button[0.5,2;2,1;wire;Back]"
fs:label(0.5,0.5, msg)
end
local function log_fs(p_name)
local res = {
"size[8,8]label[0,0;Transaction Log]button[0,7;2,1;logout;Log Out]",
"tablecolumns[text;text]",
"table[0,1;8,6;log_table;Time,Message",
}
local function log_fs(fs, p_name)
fs:size(14,8)
for i, entry in ipairs(exchange:player_log(p_name)) do
i = i*4
res[i] = ","
res[i+1] = tostring(entry.Time)
res[i+2] = ","
res[i+3] = entry.Message
fs:label(0,0, S("Transaction Log"))
fs:element("tablecolumns", "text", "text")
fs("table[0,0.75;13.75,6.75;log_table;Time,Message")
for _, entry in ipairs(exchange:player_log(p_name)) do
fs(","):escape_list(entry.Time, entry.Message)
end
res[#res+1] ="]"
fs("]")
return table.concat(res)
fs:button(6,7.5, 2,1, "logout", S("Log Out"))
end
local function main_menu_fs(fs, p_name)
fs:size(8,2)
fs:button(0,0.125, 4,1, "withdraw", S("Cash deposit and withdrawal"))
fs:button(4,0.125, 2,1, "info", S("Account Info"))
fs:button(6,0.125, 2,1, "wire", S("Wire Monies"))
fs:button(0.50, 1.125, 7, 1, "transaction_log", S("Transaction Log"))
end
local function show_atm_form(fs_fn, p_name, ...)
local fs = formlib.Builder()
fs_fn(fs, p_name, ...)
minetest.show_formspec(p_name, atm_form, tostring(fs))
end
local trans_ids = {}
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= atm_form then return end
if fields.quit then return true end
local p_name = player:get_player_name()
local this_id = fields.trans_id
local this_id = tonumber(fields.trans_id)
if this_id and this_id == trans_ids[p_name] then
if this_id and trans_ids[p_name] and this_id <= trans_ids[p_name] then
-- Ignore duplicate/stale form submittal
return true
end
trans_ids[p_name] = this_id
if fields.logout then
minetest.show_formspec(p_name, atm_form, main_menu)
end
if fields.info then
minetest.show_formspec(p_name, atm_form, info_fs(p_name))
end
if fields.wire then
minetest.show_formspec(p_name, atm_form, wire_fs(p_name))
end
if fields.send then
minetest.show_formspec(p_name, atm_form,
send_fs(p_name, fields.recipient, fields.amount))
end
if fields.transaction_log then
minetest.show_formspec(p_name, atm_form, log_fs(p_name))
show_atm_form(main_menu_fs, p_name)
elseif fields.info then
show_atm_form(info_fs, p_name)
elseif fields.wire then
show_atm_form(wire_fs, p_name)
elseif fields.withdraw then
show_atm_form(withdraw_fs, p_name, fields and fields.w_amount)
elseif fields.send then
show_atm_form(send_fs, p_name, fields.recipient, fields.amount)
elseif fields.transaction_log then
show_atm_form(log_fs, p_name)
end
return true
@ -154,7 +199,7 @@ end)
minetest.register_node("global_exchange:atm_bottom", {
description = "ATM",
description = "ATM",
inventory_image = "global_exchange_atm_icon.png",
wield_image = "global_exchange_atm_hi_front.png",
drawtype = "nodebox",
@ -180,18 +225,19 @@ minetest.register_node("global_exchange:atm_bottom", {
selection_box = {
type = "fixed",
fixed = {
{-0.5, -0.5, -0.5, 0.5, 0.5, 0.5},
{-0.5, 0.5, -0.5, -0.375, 1.125, -0.25},
{0.375, 0.5, -0.5, 0.5, 1.125, -0.25},
{-0.5, 0.5, -0.25, 0.5, 1.5, 0.5},
{-0.5, 1.125, -0.4375, -0.375, 1.25, -0.25},
{0.375, 1.125, -0.4375, 0.5, 1.25, -0.25},
{-0.5, 1.25, -0.375, -0.375, 1.375, -0.25},
{0.375, 1.25, -0.375, 0.5, 1.375, -0.25},
{-0.5, 1.375, -0.3125, -0.375, 1.5, -0.25},
{0.375, 1.375, -0.3125, 0.5, 1.5, -0.25},
{-0.500, -0.500, -0.5000, 0.500, 0.500, 0.50},
{-0.500, 0.500, -0.5000, -0.375, 1.125, -0.25},
{ 0.375, 0.500, -0.5000, 0.500, 1.125, -0.25},
{-0.500, 0.500, -0.2500, 0.500, 1.500, 0.50},
{-0.500, 1.125, -0.4375, -0.375, 1.250, -0.25},
{ 0.375, 1.125, -0.4375, 0.500, 1.250, -0.25},
{-0.500, 1.250, -0.3750, -0.375, 1.375, -0.25},
{ 0.375, 1.250, -0.3750, 0.500, 1.375, -0.25},
{-0.500, 1.375, -0.3125, -0.375, 1.500, -0.25},
{ 0.375, 1.375, -0.3125, 0.500, 1.500, -0.25},
},
},
groups = {cracky=2, atm = 1},
on_place = function(itemstack, placer, pointed_thing)
local under = pointed_thing.under
local pos
@ -223,6 +269,11 @@ minetest.register_node("global_exchange:atm_bottom", {
return itemstack
end
end,
can_dig = function(pos,player)
local meta = minetest.get_meta(pos);
local inv = meta:get_inventory()
return inv:is_empty("main")
end,
on_destruct = function(pos)
local pos2 = {x = pos.x, y = pos.y + 1, z = pos.z}
local n2 = minetest.get_node(pos2)
@ -230,10 +281,49 @@ minetest.register_node("global_exchange:atm_bottom", {
minetest.remove_node(pos2)
end
end,
groups = {cracky=2, atm = 1},
on_construct = function(pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "ATM")
local inv = meta:get_inventory()
inv:set_size("main", 6)
end,
allow_metadata_inventory_put = function(pos, listname, index, stack, player)
local itname = stack:get_name()
if coins_convert[itname] ~= nil then
return stack:get_count()
end
return 0
end,
allow_metadata_inventory_take = function(pos, listname, index, stack, player)
return stack:get_count()
end,
on_metadata_inventory_put = function(pos, listname, index, stack, player)
local itname = stack:get_name()
if coins_convert[itname] ~= nil then
local p_name = player:get_player_name()
local meta = minetest.get_meta(pos)
local inv = meta:get_inventory()
local nb = stack:get_count()
local amount = coins_convert[itname] * nb
local succ, msg = exchange:give_credits(p_name, amount, S("Cash deposit (+@1)", amount))
if succ then
inv:set_stack(listname, index, nil)
minetest.log("action", p_name.." put "..nb.." "..stack:get_name() .. " to ATM at " .. minetest.pos_to_string(pos))
show_atm_form(withdraw_fs, p_name)
--minetest.show_formspec(p_name, atm_form, deposit_fs(p_name))
else
minetest.log("error", p_name.." want to put "..nb.." "..stack:get_name().." to ATM at ".. minetest.pos_to_string(pos).." but: "..msg)
end
end
end,
on_metadata_inventory_take = function(pos, listname, index, stack, player)
minetest.log("action", player:get_player_name().." take "..stack:get_count().." "..stack:get_name().." from ATM at "..minetest.pos_to_string(pos))
end,
on_rightclick = function(pos, _, clicker)
local p_name = clicker:get_player_name()
atm_pos[p_name] = pos
minetest.sound_play("atm_beep", {pos = pos, gain = 0.3, max_hear_distance = 5})
minetest.show_formspec(clicker:get_player_name(), atm_form, main_menu)
show_atm_form(main_menu_fs, clicker:get_player_name())
end,
})
@ -254,15 +344,15 @@ minetest.register_node("global_exchange:atm_top", {
node_box = {
type = "fixed",
fixed = {
{-0.5, -0.5, -0.5, -0.375, 0.125, -0.25},
{0.375, -0.5, -0.5, 0.5, 0.125, -0.25},
{-0.5, -0.5, -0.25, 0.5, 0.5, 0.5},
{-0.5, 0.125, -0.4375, -0.375, 0.25, -0.25},
{0.375, 0.125, -0.4375, 0.5, 0.25, -0.25},
{-0.5, 0.25, -0.375, -0.375, 0.375, -0.25},
{0.375, 0.25, -0.375, 0.5, 0.375, -0.25},
{-0.5, 0.375, -0.3125, -0.375, 0.5, -0.25},
{0.375, 0.375, -0.3125, 0.5, 0.5, -0.25},
{-0.500, -0.500, -0.5000, -0.375, 0.125, -0.25},
{ 0.375, -0.500, -0.5000, 0.500, 0.125, -0.25},
{-0.500, -0.500, -0.2500, 0.500, 0.500, 0.50},
{-0.500, 0.125, -0.4375, -0.375, 0.250, -0.25},
{ 0.375, 0.125, -0.4375, 0.500, 0.250, -0.25},
{-0.500, 0.250, -0.3750, -0.375, 0.375, -0.25},
{ 0.375, 0.250, -0.3750, 0.500, 0.375, -0.25},
{-0.500, 0.375, -0.3125, -0.375, 0.500, -0.25},
{ 0.375, 0.375, -0.3125, 0.500, 0.500, -0.25},
}
},
selection_box = {
@ -278,10 +368,11 @@ minetest.register_node("global_exchange:atm_top", {
minetest.register_craft( {
output = "global_exchange:atm",
recipe = {
{ "default:stone", "default:stone", "default:stone" },
{ "default:stone", "default:stone", "default:stone" },
{ "default:stone", "default:gold_ingot", "default:stone" },
{ "default:stone", "default:stone", "default:stone" },
{ "default:stone", "default:stone", "default:stone" },
}
})
minetest.register_alias("global_exchange:atm", "global_exchange:atm_bottom")
-- vim:set ts=4 sw=4 noet:

View File

@ -1 +0,0 @@
default?

View File

@ -1 +0,0 @@
Adds a server-wide commodities (item) exchange.

View File

@ -1,60 +1,61 @@
local exchange = ...
local exchange, formlib = ...
local mailbox_form = "global_exchange:digital_mailbox"
local mailbox_contents = {}
local selected_index = {}
-- Map from player names to their most recent search result
-- Map from player names to their most recent search result
local function get_mail(p_name)
local mail_maybe = mailbox_contents[p_name]
if mail_maybe then
return mail_maybe
else
mailbox_contents[p_name] = {}
return mailbox_contents[p_name]
if not mail_maybe then
local _,res = exchange:view_inbox(p_name)
mail_maybe = res or {}
mailbox_contents[p_name] = mail_maybe
selected_index[p_name] = math.min(selected_index[p_name] or 0, #mail_maybe)
end
return mail_maybe
end
local function mk_inbox_list(results, x, y, w, h)
local res = {
"textlist[",
tostring(x),
",",
tostring(y),
";",
tostring(w),
",",
tostring(h),
";result_list;"
}
for i, row in ipairs(results) do
res[i*2+8] = row.Amount .. " " .. row.Item
res[i*2+9] = ","
end
res[#res+1] = "]"
return table.concat(res)
local function wear_string(wear)
return "-" .. math.ceil(100 * wear / 65535) .. "%"
end
local function mk_mail_fs(p_name, results, err_str)
fs = "size[6,8]" ..
"label[0,0;Inbox]"
local function mk_inbox_list(fs, results, x, y, w, h)
fs:textlist(x,y, w,h, "result_list", function(add_row)
for i, row in ipairs(results) do
local wear_suffix = nil
if row.Wear > 0 then
wear_suffix = " (" .. wear_string(row.Wear) .. ")"
end
add_row(row.Amount, " ", row.Item, wear_suffix)
end
end)
end
local function mk_mail_fs(fs, p_name, results, err_str)
fs:size(8,8)
fs:label(0,0, "Inbox")
if err_str then
fs = fs .. "label[3,0;Error: " .. err_str .. "]"
fs:label(3,0, "Error: " .. err_str)
end
return fs .. mk_inbox_list(results, 0, 1, 6, 6) ..
"button[0,7;2,1;claim;Claim]"
mk_inbox_list(fs, results, 0, 1, 7.75, 6.25)
fs:button(3,7.35, 2,1, "claim", "Claim")
end
local function show_mail(p_name, results, err_str)
minetest.show_formspec(p_name, mailbox_form, mk_mail_fs(p_name, results, err_str))
local function show_mail(p_name, err_str)
local fs = formlib.Builder()
mk_mail_fs(fs, p_name, get_mail(p_name), err_str)
minetest.show_formspec(p_name, mailbox_form, tostring(fs))
end
@ -63,35 +64,6 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
if fields.quit then return true end
local p_name = player:get_player_name()
local idx = selected_index[p_name]
if fields.claim
and idx then
local row = get_mail(p_name)[idx]
if row then
local stack = ItemStack(row.Item)
stack:set_count(row.Amount)
local p_inv = player:get_inventory()
if not p_inv:room_for_item("main", stack) then
show_mail(p_name, get_mail(p_name), "Not enough room.")
return true
end
local succ, res = exchange:take_inbox(row.Id, row.Amount)
if not succ then
show_mail(p_name, get_mail(p_name), res)
end
stack:set_count(res)
p_inv:add_item("main", stack)
table.remove(get_mail(p_name), idx)
show_mail(p_name, get_mail(p_name))
end
end
if fields.result_list then
local event = minetest.explode_textlist_event(fields.result_list)
@ -101,22 +73,48 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
end
end
if fields.claim then
local idx = selected_index[p_name]
local row = get_mail(p_name)[idx]
if row then
local stack = ItemStack(row.Item)
stack:set_count(row.Amount)
stack:set_wear(row.Wear)
local p_inv = player:get_inventory()
local leftover = p_inv:add_item("main", stack)
local took_amount = row.Amount - leftover:get_count()
mailbox_contents[p_name] = nil
local succ, res = exchange:take_inbox(row.Id, took_amount)
if succ then
show_mail(p_name)
else
show_mail(p_name, res)
end
end
end
return true
end)
minetest.register_node("global_exchange:mailbox", {
description = "Digital Mailbox",
tiles = {"global_exchange_box.png",
tiles = {
"global_exchange_box.png",
"global_exchange_box.png",
"global_exchange_box.png^global_exchange_mailbox_side.png",
},
is_ground_content = false,
stack_max = 1,
groups = {cracky=2},
on_rightclick = function(pos, node, clicker)
local p_name = clicker:get_player_name()
local _,res = exchange:view_inbox(p_name)
mailbox_contents[p_name] = res
minetest.show_formspec(p_name, mailbox_form, mk_mail_fs(p_name, res))
mailbox_contents[p_name] = nil
show_mail(p_name)
end,
})
@ -125,7 +123,8 @@ minetest.register_craft( {
output = "global_exchange:mailbox",
recipe = {
{ "default:stone", "default:gold_ingot", "default:stone" },
{ "default:stone", "default:chest", "default:stone" },
{ "default:stone", "default:stone", "default:stone" },
{ "default:stone", "default:chest", "default:stone" },
{ "default:stone", "default:stone", "default:stone" },
}
})
-- vim:set ts=4 sw=4 noet:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

172
formlib.lua Normal file
View File

@ -0,0 +1,172 @@
local formlib = {}
local builder_methods = {}
function formlib.escape(x)
if x == nil then return "" end
return minetest.formspec_escape(tostring(x))
end
function formlib.bool(x)
-- nil and false are returned as-is, everything else maps to true
return x and true
end
function builder_methods.append(fs, ...)
for i=1,select("#", ...) do
local x = select(i, ...)
if x ~= nil then table.insert(fs, tostring(x)) end
end
return fs
end
function builder_methods.escape(fs, ...)
for i=1,select("#", ...) do
local x = select(i, ...)
if x ~= nil then table.insert(fs, formlib.escape(x)) end
end
return fs
end
function builder_methods.escape_list(fs, ...)
for i=1,select("#", ...) do
local x = select(i, ...)
if i > 1 then fs(",") end
fs:escape(x)
end
return fs
end
function builder_methods.escape_groups(fs, ...)
for i=1,select("#", ...) do
local group = select(i, ...)
if i > 1 then fs(";") end
if type(group) == "table" then
fs:escape_list(unpack(group))
else
fs:escape(group)
end
end
return fs
end
function builder_methods.element(fs, name, ...)
return fs(name, "["):escape_groups(...):append("]")
end
function builder_methods.size(fs, w,h, fixed)
if fixed == nil then
return fs:element("size", {w,h})
else
return fs:element("size", {w,h, formlib.bool(fixed)})
end
end
function builder_methods.bgcolor(fs, color, fullscreen)
if fullscreen == nil then
return fs:element("bgcolor", {color})
else
return fs:element("bgcolor", {color}, {formlib.bool(fullscreen)})
end
end
function builder_methods.list(fs, x,y, w,h, inv_loc, inv_list, start_idx)
return fs:element("list", {inv_loc}, {inv_list}, {x,y}, {w,h}, {start_idx})
end
function builder_methods.button(fs, x,y, w,h, name, text)
return fs:element("button", {x,y}, {w,h}, {name}, {text})
end
function builder_methods.item_image_button(fs, x,y, w,h, name, item, text)
return fs:element("item_image_button", {x,y}, {w,h}, {item}, {name}, {text})
end
function builder_methods.label(fs, x,y, text)
return fs:element("label", {x,y}, {text})
end
function builder_methods.field(fs, x,y, w,h, name, label, default, close_on_enter)
fs:element("field", {x,y}, {w,h}, {name}, {label}, {default})
if close_on_enter ~= nil then
fs:element("field_close_on_enter", {name}, {formlib.bool(close_on_enter)})
end
return fs
end
function builder_methods.box(fs, x,y, w,h, color)
return fs:element("box", {x,y}, {w,h}, {color})
end
function builder_methods.dropdown(fs, x,y, w, name, body_fn, selected_idx)
fs("dropdown["):escape_groups({x,y}, {w}, {name}):append(";")
local first = true
local results = { body_fn(function(...)
if first then first = false else fs(",") end
fs:escape(...)
end) }
if selected_idx ~= nil then fs(";"):escape(selected_idx) end
return fs("]"), unpack(results)
end
function builder_methods.tabheader(fs, x,y, name, body_fn, current_tab, transparent, draw_border)
fs("tabheader["):escape_groups({x,y}, {name}):append(";")
local first = true
local results = { body_fn(function(...)
if first then first = false else fs(",") end
fs:escape(...)
end) }
fs(";"):escape_groups({current_tab}, {formlib.bool(transparent)}, {formlib.bool(draw_border)})
return fs("]"), unpack(results)
end
function builder_methods.textlist(fs, x,y, w,h, name, body_fn, selected_idx, transparent)
fs("textlist["):escape_groups({x,y}, {w,h}, {name}):append(";")
local first = true
local results = { body_fn(function(...)
if first then first = false else fs(",") end
fs:escape(...)
end) }
fs(";"):escape_groups({selected_idx}, {formlib.bool(transparent)})
return fs("]"), unpack(results)
end
function builder_methods.tableoptions(fs, ...)
return fs:element("tableoptions", ...)
end
function builder_methods.tablecolumns(fs, ...)
return fs:element("tablecolumns", ...)
end
function builder_methods.table(fs, x,y, w,h, name, body_fn, selected_idx)
fs("table["):escape_groups({x,y}, {w,h}, {name}):append(";")
local first = true
local results = { body_fn(function(...)
if first then first = false else fs(",") end
fs:escape_list(...)
end) }
if selected_idx ~= nil then fs(";"):escape(selected_idx) end
return fs("]"), unpack(results)
end
function builder_methods.container(fs, x,y, sub_fn, ...)
fs:element("container", {x,y})
local results = { sub_fn(fs, ...) }
return fs("container_end[]"), unpack(results)
end
local builder_meta = {
__metatable = "protected",
__index = builder_methods,
__call = builder_methods.append,
__tostring = table.concat,
}
function formlib.Builder()
local fs = {}
setmetatable(fs, builder_meta)
return fs
end
return formlib
-- vim:set ts=4 sw=4 noet:

View File

@ -1,53 +1,47 @@
local insecure_env = minetest.request_insecure_environment()
assert(insecure_env,
"global_exchange needs to be trusted to run under mod security.")
assert(insecure_env, "global_exchange needs to be trusted to run under mod security.")
local modpath = minetest.get_modpath(minetest.get_current_modname()) .. "/"
local income = tonumber(minetest.setting_get("citizens_income")) or 10
local income_interval = 1200
local income_msg = "You receive your citizen's income (+" .. income .. ")"
local S = minetest.get_translator("global_exchange")
local next_payout = os.time() + income_interval
local exchange =
assert(loadfile(modpath .. "exchange.lua"))(insecure_env).open_exchange(
minetest.get_worldpath() .. "/global_exchange.db"
)
local exchange = assert(loadfile(modpath .. "exchange.lua"))(insecure_env).
open_exchange(minetest.get_worldpath() .. "/global_exchange.db")
local formlib = assert(loadfile(modpath .. "formlib.lua"))()
minetest.register_on_shutdown(function()
exchange:close()
exchange:close()
end)
local function check_giving()
local now = os.time()
if now < next_payout then
return
end
next_payout = now + income_interval
for _, player in ipairs(minetest.get_connected_players()) do
local p_name = player:get_player_name()
local succ = exchange:give_credits(p_name, income,
"Citizen's Income (+" .. income .. ")")
if succ then
minetest.chat_send_player(p_name, income_msg)
end
end
minetest.after(5, check_giving)
local function handle_setbalance_command(caller, name, newbalance)
return exchange:set_balance(name, newbalance)
end
minetest.after(5, check_giving)
minetest.register_privilege("balance", {
description = S("Can use /setbalance"),
give_to_singleplayer = false
})
minetest.register_chatcommand("setbalance", {
params = S("[<name>] <balance>"),
description = S("set a player's trading balance"),
privs = {balance=true},
func = function(caller, param)
local name, balancestr = string.match(param, "([^ ]+) ([0-9]+)")
if not name or not balancestr then
name = caller
balancestr = string.match(param, "([0-9]+)")
if not balancestr then
return false, S("Invalid parameters (see /help setbalance)")
end
end
return handle_setbalance_command(caller, name, tonumber(balancestr))
end,
})
assert(loadfile(modpath .. "atm.lua"))(exchange, formlib)
assert(loadfile(modpath .. "exchange_machine.lua"))(exchange, formlib)
assert(loadfile(modpath .. "digital_mailbox.lua"))(exchange, formlib)
assert(loadfile(modpath .. "atm.lua"))(exchange)
assert(loadfile(modpath .. "exchange_machine.lua"))(exchange)
assert(loadfile(modpath .. "digital_mailbox.lua"))(exchange)
minetest.log("action", "[global_exchange] loaded.")

View File

@ -0,0 +1,82 @@
# textdomain:global_exchange
### exchange.lua ###
Database Busy.=BDD occupée.
Programmer error.=Erreur du programmeur.
Failed to log message.=Echec journalisation message.
Account already exists.=Compte déjà existant.
@1 does not have an account.=@1 n'a pas de compte.
Non-integer credit delta=Delta de crédit non entier
@1 does not have enough money.=@1 n'a pas assez d'argent.
Non-integer credit amount=Montant de crédit non entier
Failed to log sender message=Echec journalisation message de l'émetteur
Failed to log receiver message=Echec journalisation message du récepteur
Noninteger quantity=Quantité non entière
Nonpositive quantity=Quantité non positive
Noninteger rate=Taux non entier
Nonpositive rate=Taux non positif
Noninteger wear=Usure non entière
Invalid wear=Usure invalide
No such order.=Pas un tel ordre.
Order does not exist.=L'ordre n'existe pas.
### init.lua ###
Can use /setbalance=Peut utiliser /setbalance
set a player's trading balance=définir le solde commercial d'un joueur
Invalid parameters (see /help setbalance)=Paramètres invalides (voir /help setbalance)
### atm.lua ###
Cash withdrawal: (-@1)=Retrait d'espèces : (-@1)
Invalid number ! Must be an Integer > 0=Nombre invalide ! Doit être un Entier > 0
You don't have an account.=Vous n'avez pas de compte.
Balance: @1=Balance : @1
Desired amount:=Montant désiré :
Get !=Obtenir !
Or deposit your coins to credit your account:=Ou deposez vos pièces pour créditer votre compte :
Log Out=Sortir
Balance: @1=Balance : @1
Send to:=Envoyer à :
Amount=Montant
Send=Envoie
Back=Retour
Invalid transfer amount.=Montant de transfert invalide.
Cash deposit and withdrawal=Dépot et retrait d'espèces
Account Info=Infos du compte
Wire Monies=Virement bancaire
Transaction Log=Journaux transactions
ATM=Distributeur Automatique d'argent
Cash deposit (+@1)=Dépot d'espèces (+@1)
### exchange_machine.lua ###
Wear=Usure
Buy Vol=Qté achat
Buy Max=Achat Max
Sell Vol=Qté vente
Sell Min=Vente Min
Unknown Item=Item inconnue
No description=Pas de description
New (-0%)=Neuf (-0%)
Good (-10%)=Bon (-10%)
Worn (-50%)=Usé (-50%)
Junk (-100%)=Indésirable (-100%)
Poster=Émetteur
Rate=Taux
Quantity=Quantité
Bid (ea.)=Offre
Place Bid=Faire Offre
Ask (ea.)=Demande
Sell=Vendre
Cancel Order=Annuler Ordre
Market=Marché
Buy=Acheter
My Orders=Mes Ordres
You must input an item=Vous devez saisir un élément
That item does not exist.=Cet élément n'existe pas.
Invalid wear.=Usure invalide.
Invalid amount.=Montant invalide.
Invalid rate.=Taux invalide.
Cannot sell an item with metadata.=Ne peut vendre un item avec des métadonnées.
Qty=Qté
Filter=Filtre
Search=Chercher
Reset=Reset

View File

@ -1 +1,4 @@
name=global_exchange
name = global_exchange
title = Global Exchange
description = Adds a server-wide commodities (item) exchange.
optional_depends = default