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 Using the Exchange
================== ==================
Main Screen Overview
----------- -----------
The first screen you see is where you can search and post new buy/sell orders. At the top of the exchange form there are four tabs: Market Summary, Buy, Sell,
Here is an overview of each element: and Your Orders. Pressing each tab will take you to the indicated screen as
- Market Summary - Pressing this will take you to the market summary screen. described below.
- 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.
Market Summary Market Summary
-------------- --------------
This summarizes the various items available on the exchange. From left to right, 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 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 the tool wear if applicable, the amount requested by buyers, the maximum rate
offered by sellers, and the minimum rate offered by sellers. It is updated offered by buyers, the amount offered by sellers, and the minimum rate offered
periodically. 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 Your Orders
----------- -----------
This screen lets you see and cancel your orders. To cancel an order, click the This screen lets you see and cancel your orders. To cancel an order, click the
order and press the "Cancel" button. 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
Select Item not fit in the inventory they are placed in your Inbox instead to be claimed
----------- later.
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
Buying/Selling Buying/Selling
============== ==============
Once you have opened the exchange, you have a few options. If you don't already 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 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 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, When selling, the Ask field is the minimum price you will accept for each
leave it unchecked. Next, you need to select the item you want to deal in. There item. When buying, the Bid field determines the maximum amount you are willing
are two ways: typing the item name (e.g. default:cobble) in manually to the item to pay. If there are matching offers (i.e. there are one or more offers with a
field, or using the "Select Item" menu. If you haven't already decided on a price, price at least as good and a compatible tool wear level), then that part of
or you want to make sure your order is filled quickly, you can conduct a search. your offer will immediately be filled. For example, if you post a buy order
To do this, click the "Search" button. This will give you a list of results. If for 10 cobblestone at 5 credits each, and there is a sell offer for 5
you checked the "Sell" box, then these will be buy orders, and will show the cobblestone at 3 credits each, it will give you 5 cobble immediately at a
maximum price per item each buyer is willing to accept. Otherwise, these will be total cost of 15 credits (the order-book price), and leave an order on the
sell orders, displaying the minimum price each seller will accept. If you click exchange for 5 more cobblestone at 5 credits each.
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.
Once your offer is on the exchange, you can view or cancel it from the "Your Once your offer is on the exchange, you can view or cancel it from the "Your
Orders" menu. Orders" menu.

325
atm.lua
View File

@ -1,152 +1,197 @@
-- A telling machine. Call this file with the exchange argument. -- 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_form = "global_exchange:atm_form"
local atm_pos = {}
local main_menu =[[ local unique = (function(unique_num)
size[6,2] return function()
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 function logout(x,y)
return "button[" .. x .. "," .. y .. ";2,1;logout;Log Out]"
end
local function label(x,y,text)
return "label[" .. x .. "," .. y .. ";"
.. minetest.formspec_escape(text) .. "]"
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 unique_num = unique_num + 1
return unique_num
return ret
end 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 info_fs(p_name) local coins_convert = {
local balance = exchange:get_balance(p_name) [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 fs local function withdraw_fs(fs, p_name, amount)
if not balance then local spos = atm_pos[p_name].x..","..atm_pos[p_name].y..","..atm_pos[p_name].z
fs = label(0.5,0.5, "You don't have an account.") 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 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 else
fs = label(0.5,0.5, "Balance: " .. balance) msg = err
end
elseif w_amount then
msg = S("Invalid number ! Must be an Integer > 0")
end end
return "size[4,3]" .. fs .. logout(0.5,2)
end
local function wire_fs(p_name)
local balance = exchange:get_balance(p_name) local balance = exchange:get_balance(p_name)
local fs = "size[4,5]" .. logout(0,4) fs:size(8,10)
if not balance then if not balance then
return 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(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
fs:button(0,8.75, 2,1, "logout", S("Log Out"))
end end
-- To prevent duplicates local function info_fs(fs, p_name)
return fs .. field(-100, -100, 0,0, "trans_id", "", unique()) .. local balance = exchange:get_balance(p_name)
label(0.5,0.5, "Balance: " .. balance) ..
field(0.5,1.5, 2,1, "recipient", "Send to:", "") .. fs:size(4,3)
field(0.5,2.5, 2,1, "amount", "Amount", "") ..
"button[2,4;2,1;send;Send]" 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
fs:button(1,2, 2,1, "logout", S("Log Out"))
end end
local function send_fs(p_name, receiver, amt_str) local function wire_fs(fs, p_name)
local fs = "size[7,3]" 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 amt = tonumber(amt_str)
local msg = nil
if not amt or amt <= 0 then if not amt or amt <= 0 then
return fs .. label(0.5,0.5, "Invalid transfer amount.") .. msg = S("Invalid transfer amount.")
"button[0.5,2;2,1;wire;Back]" else
end
local succ, err = exchange:transfer_credits(p_name, receiver, amt) local succ, err = exchange:transfer_credits(p_name, receiver, amt)
if not succ then if not succ then
return fs .. label(0.5,0.5, "Error: " .. err) .. msg = "Error: " .. err
"button[0.5,2;2,1;wire;Back]" else
msg = "Successfully sent " .. amt ..
" credits to " .. receiver .. "."
end end
return fs.. label(0.5,0.5, "Successfully sent " .. end
amt .. " credits to " .. receiver) ..
"button[0.5,2;2,1;wire;Back]" fs:label(0.5,0.5, msg)
end end
local function log_fs(p_name) local function log_fs(fs, p_name)
local res = { fs:size(14,8)
"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",
}
for i, entry in ipairs(exchange:player_log(p_name)) do fs:label(0,0, S("Transaction Log"))
i = i*4
res[i] = "," fs:element("tablecolumns", "text", "text")
res[i+1] = tostring(entry.Time)
res[i+2] = "," fs("table[0,0.75;13.75,6.75;log_table;Time,Message")
res[i+3] = entry.Message for _, entry in ipairs(exchange:player_log(p_name)) do
fs(","):escape_list(entry.Time, entry.Message)
end 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 end
local trans_ids = {} local trans_ids = {}
minetest.register_on_player_receive_fields(function(player, formname, fields) minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= atm_form then return end if formname ~= atm_form then return end
if fields.quit then return true end if fields.quit then return true end
local p_name = player:get_player_name() 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 return true
end end
trans_ids[p_name] = this_id trans_ids[p_name] = this_id
if fields.logout then if fields.logout then
minetest.show_formspec(p_name, atm_form, main_menu) show_atm_form(main_menu_fs, p_name)
end elseif fields.info then
show_atm_form(info_fs, p_name)
if fields.info then elseif fields.wire then
minetest.show_formspec(p_name, atm_form, info_fs(p_name)) show_atm_form(wire_fs, p_name)
end elseif fields.withdraw then
show_atm_form(withdraw_fs, p_name, fields and fields.w_amount)
if fields.wire then elseif fields.send then
minetest.show_formspec(p_name, atm_form, wire_fs(p_name)) show_atm_form(send_fs, p_name, fields.recipient, fields.amount)
end elseif fields.transaction_log then
show_atm_form(log_fs, p_name)
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))
end end
return true return true
@ -180,18 +225,19 @@ minetest.register_node("global_exchange:atm_bottom", {
selection_box = { selection_box = {
type = "fixed", type = "fixed",
fixed = { fixed = {
{-0.5, -0.5, -0.5, 0.5, 0.5, 0.5}, {-0.500, -0.500, -0.5000, 0.500, 0.500, 0.50},
{-0.5, 0.5, -0.5, -0.375, 1.125, -0.25}, {-0.500, 0.500, -0.5000, -0.375, 1.125, -0.25},
{0.375, 0.5, -0.5, 0.5, 1.125, -0.25}, { 0.375, 0.500, -0.5000, 0.500, 1.125, -0.25},
{-0.5, 0.5, -0.25, 0.5, 1.5, 0.5}, {-0.500, 0.500, -0.2500, 0.500, 1.500, 0.50},
{-0.5, 1.125, -0.4375, -0.375, 1.25, -0.25}, {-0.500, 1.125, -0.4375, -0.375, 1.250, -0.25},
{0.375, 1.125, -0.4375, 0.5, 1.25, -0.25}, { 0.375, 1.125, -0.4375, 0.500, 1.250, -0.25},
{-0.5, 1.25, -0.375, -0.375, 1.375, -0.25}, {-0.500, 1.250, -0.3750, -0.375, 1.375, -0.25},
{0.375, 1.25, -0.375, 0.5, 1.375, -0.25}, { 0.375, 1.250, -0.3750, 0.500, 1.375, -0.25},
{-0.5, 1.375, -0.3125, -0.375, 1.5, -0.25}, {-0.500, 1.375, -0.3125, -0.375, 1.500, -0.25},
{0.375, 1.375, -0.3125, 0.5, 1.5, -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) on_place = function(itemstack, placer, pointed_thing)
local under = pointed_thing.under local under = pointed_thing.under
local pos local pos
@ -223,6 +269,11 @@ minetest.register_node("global_exchange:atm_bottom", {
return itemstack return itemstack
end end
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) on_destruct = function(pos)
local pos2 = {x = pos.x, y = pos.y + 1, z = pos.z} local pos2 = {x = pos.x, y = pos.y + 1, z = pos.z}
local n2 = minetest.get_node(pos2) local n2 = minetest.get_node(pos2)
@ -230,10 +281,49 @@ minetest.register_node("global_exchange:atm_bottom", {
minetest.remove_node(pos2) minetest.remove_node(pos2)
end end
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) 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.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, end,
}) })
@ -254,15 +344,15 @@ minetest.register_node("global_exchange:atm_top", {
node_box = { node_box = {
type = "fixed", type = "fixed",
fixed = { fixed = {
{-0.5, -0.5, -0.5, -0.375, 0.125, -0.25}, {-0.500, -0.500, -0.5000, -0.375, 0.125, -0.25},
{0.375, -0.5, -0.5, 0.5, 0.125, -0.25}, { 0.375, -0.500, -0.5000, 0.500, 0.125, -0.25},
{-0.5, -0.5, -0.25, 0.5, 0.5, 0.5}, {-0.500, -0.500, -0.2500, 0.500, 0.500, 0.50},
{-0.5, 0.125, -0.4375, -0.375, 0.25, -0.25}, {-0.500, 0.125, -0.4375, -0.375, 0.250, -0.25},
{0.375, 0.125, -0.4375, 0.5, 0.25, -0.25}, { 0.375, 0.125, -0.4375, 0.500, 0.250, -0.25},
{-0.5, 0.25, -0.375, -0.375, 0.375, -0.25}, {-0.500, 0.250, -0.3750, -0.375, 0.375, -0.25},
{0.375, 0.25, -0.375, 0.5, 0.375, -0.25}, { 0.375, 0.250, -0.3750, 0.500, 0.375, -0.25},
{-0.5, 0.375, -0.3125, -0.375, 0.5, -0.25}, {-0.500, 0.375, -0.3125, -0.375, 0.500, -0.25},
{0.375, 0.375, -0.3125, 0.5, 0.5, -0.25}, { 0.375, 0.375, -0.3125, 0.500, 0.500, -0.25},
} }
}, },
selection_box = { selection_box = {
@ -285,3 +375,4 @@ minetest.register_craft( {
}) })
minetest.register_alias("global_exchange:atm", "global_exchange:atm_bottom") 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, formlib = ...
local exchange = ...
local mailbox_form = "global_exchange:digital_mailbox" local mailbox_form = "global_exchange:digital_mailbox"
local mailbox_contents = {} local mailbox_contents = {}
local selected_index = {} 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 function get_mail(p_name)
local mail_maybe = mailbox_contents[p_name] local mail_maybe = mailbox_contents[p_name]
if mail_maybe then
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 return mail_maybe
else
mailbox_contents[p_name] = {}
return mailbox_contents[p_name]
end
end end
local function mk_inbox_list(results, x, y, w, h) local function wear_string(wear)
local res = { return "-" .. math.ceil(100 * wear / 65535) .. "%"
"textlist[", end
tostring(x),
",",
tostring(y),
";",
tostring(w),
",",
tostring(h),
";result_list;"
}
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 for i, row in ipairs(results) do
res[i*2+8] = row.Amount .. " " .. row.Item local wear_suffix = nil
res[i*2+9] = "," if row.Wear > 0 then
wear_suffix = " (" .. wear_string(row.Wear) .. ")"
end end
res[#res+1] = "]" add_row(row.Amount, " ", row.Item, wear_suffix)
end
return table.concat(res) end)
end end
local function mk_mail_fs(p_name, results, err_str) local function mk_mail_fs(fs, p_name, results, err_str)
fs = "size[6,8]" .. fs:size(8,8)
"label[0,0;Inbox]" fs:label(0,0, "Inbox")
if err_str then if err_str then
fs = fs .. "label[3,0;Error: " .. err_str .. "]" fs:label(3,0, "Error: " .. err_str)
end end
return fs .. mk_inbox_list(results, 0, 1, 6, 6) .. mk_inbox_list(fs, results, 0, 1, 7.75, 6.25)
"button[0,7;2,1;claim;Claim]"
fs:button(3,7.35, 2,1, "claim", "Claim")
end end
local function show_mail(p_name, results, err_str) local function show_mail(p_name, err_str)
minetest.show_formspec(p_name, mailbox_form, mk_mail_fs(p_name, results, 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 end
@ -63,35 +64,6 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
if fields.quit then return true end if fields.quit then return true end
local p_name = player:get_player_name() 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 if fields.result_list then
local event = minetest.explode_textlist_event(fields.result_list) 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
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 return true
end) end)
minetest.register_node("global_exchange:mailbox", { minetest.register_node("global_exchange:mailbox", {
description = "Digital Mailbox", description = "Digital Mailbox",
tiles = {"global_exchange_box.png", tiles = {
"global_exchange_box.png",
"global_exchange_box.png", "global_exchange_box.png",
"global_exchange_box.png^global_exchange_mailbox_side.png", "global_exchange_box.png^global_exchange_mailbox_side.png",
}, },
is_ground_content = false,
stack_max = 1,
groups = {cracky=2}, groups = {cracky=2},
on_rightclick = function(pos, node, clicker) on_rightclick = function(pos, node, clicker)
local p_name = clicker:get_player_name() local p_name = clicker:get_player_name()
local _,res = exchange:view_inbox(p_name) mailbox_contents[p_name] = nil
mailbox_contents[p_name] = res show_mail(p_name)
minetest.show_formspec(p_name, mailbox_form, mk_mail_fs(p_name, res))
end, end,
}) })
@ -129,3 +127,4 @@ minetest.register_craft( {
{ "default:stone", "default:stone", "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() local insecure_env = minetest.request_insecure_environment()
assert(insecure_env, assert(insecure_env, "global_exchange needs to be trusted to run under mod security.")
"global_exchange needs to be trusted to run under mod security.")
local modpath = minetest.get_modpath(minetest.get_current_modname()) .. "/" local modpath = minetest.get_modpath(minetest.get_current_modname()) .. "/"
local income = tonumber(minetest.setting_get("citizens_income")) or 10 local S = minetest.get_translator("global_exchange")
local income_interval = 1200
local income_msg = "You receive your citizen's income (+" .. income .. ")"
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() minetest.register_on_shutdown(function()
exchange:close() exchange:close()
end) end)
local function handle_setbalance_command(caller, name, newbalance)
local function check_giving() return exchange:set_balance(name, newbalance)
local now = os.time()
if now < next_payout then
return
end end
next_payout = now + income_interval minetest.register_privilege("balance", {
description = S("Can use /setbalance"),
give_to_singleplayer = false
})
for _, player in ipairs(minetest.get_connected_players()) do minetest.register_chatcommand("setbalance", {
local p_name = player:get_player_name() params = S("[<name>] <balance>"),
description = S("set a player's trading balance"),
local succ = exchange:give_credits(p_name, income, privs = {balance=true},
"Citizen's Income (+" .. income .. ")") func = function(caller, param)
local name, balancestr = string.match(param, "([^ ]+) ([0-9]+)")
if succ then if not name or not balancestr then
minetest.chat_send_player(p_name, income_msg) name = caller
balancestr = string.match(param, "([0-9]+)")
if not balancestr then
return false, S("Invalid parameters (see /help setbalance)")
end end
end end
return handle_setbalance_command(caller, name, tonumber(balancestr))
end,
})
minetest.after(5, check_giving) assert(loadfile(modpath .. "atm.lua"))(exchange, formlib)
end assert(loadfile(modpath .. "exchange_machine.lua"))(exchange, formlib)
assert(loadfile(modpath .. "digital_mailbox.lua"))(exchange, formlib)
minetest.after(5, check_giving) minetest.log("action", "[global_exchange] loaded.")
assert(loadfile(modpath .. "atm.lua"))(exchange)
assert(loadfile(modpath .. "exchange_machine.lua"))(exchange)
assert(loadfile(modpath .. "digital_mailbox.lua"))(exchange)

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