10 Commits

7 changed files with 1509 additions and 1159 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.

293
atm.lua
View File

@ -1,152 +1,170 @@
-- A telling machine. Call this file with the exchange argument.
local exchange = ...
local exchange, formlib = ...
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_convert = {
["minercantile:copper_coin"]=1, ["minercantile:silver_coin"]=9, ["minercantile:gold_coin"]=81,
["maptools:copper_coin"]=1, ["maptools:silver_coin"]=9, ["maptools:gold_coin"]=81,
["bitchange:mineninth"]=729, ["bitchange:minecoin"]=6561, ["bitchange:minecoinblock"]=59049
}
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
return ret
end
local function info_fs(p_name)
local function deposit_fs(fs, p_name)
local balance = exchange:get_balance(p_name)
local spos = atm_pos[p_name].x..","..atm_pos[p_name].y..","..atm_pos[p_name].z
fs:size(8,9)
local fs
if not balance then
fs = label(0.5,0.5, "You don't have an account.")
fs:label(0.5,0.5, "You don't have an account.")
else
fs = label(0.5,0.5, "Balance: " .. balance)
fs:label(0.5,0.5, "Balance: " .. balance)
fs:label(1,1,"Put your coins to credit your account")
fs:list(3.5,2.5, 1,1, "nodemeta:"..spos, "main")
fs:list(0,4, 8,4, "current_player", "main")
--fs("list[nodemeta:"..spos..";main;3.5,2.5;1,1;]"..
-- "list[current_player;main;0,4.85;8,1;]"..
-- "list[current_player;main;0,6.08;8,3;8]" ..
fs("listring[nodemeta:"..spos..";main]"..
"listring[current_player;main]"
)
end
return "size[4,3]" .. fs .. logout(0.5,2)
fs:button(1,2, 2,1, "logout", "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, "Balance: " .. balance)
else
fs:label(0.5,0.5, "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", "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, "Balance: " .. balance)
fs:field(0.75,1.750, 3,1, "recipient", "Send to:", "")
fs:field(0.75,3.000, 3,1, "amount", "Amount", "")
fs:button(0,4.25, 2,1, "logout", "Log Out")
fs:button(2,4.25, 2,1, "send", "Send")
else
fs:button(0,4, 2,1, "logout", "Back")
fs:label(0.5,0.5, "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", "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 = "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, "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", "Log Out")
end
local function main_menu_fs(fs, p_name)
fs:size(6,2)
fs:button(0,0.125, 2,1, "deposit", "Cash Deposit")
fs:button(2,0.125, 2,1, "info", "Account Info")
fs:button(4,0.125, 2,1, "wire", "Wire Monies")
fs:button(0.50,1.125, 5.0,1, "transaction_log", "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.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)
elseif fields.deposit then
show_atm_form(deposit_fs, p_name)
end
return true
@ -180,18 +198,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
@ -230,10 +249,43 @@ 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", 1)
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,
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, "Cash deposit (+"..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(deposit_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_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 +306,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 +330,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,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

View File

@ -1,35 +1,63 @@
local exchange, formlib = ...
local exchange = ...
local search_cooldown = 2
local summary_interval = 600
local global_inv = nil
-- NALC split() function
local function split(str, sep)
if not str then return nil end
local result = {}
local regex = ("([^%s]+)"):format(sep)
for each in str:gmatch(regex) do
table.insert(result, each)
end
return result
end
local function is_integer(x)
return math.floor(x) == x
end
local function wear_string(wear)
if wear > 0 then
return "-" .. math.ceil(100 * wear / 65535) .. "%"
else
return "----"
end
end
local summary_fs = ""
local function mk_summary_fs()
local res = {
"size[8,8]",
"label[0,0;Updated Periodically]",
"tablecolumns[text;text;text;text;text;text]",
"table[0,1;8,6;summary_table;",
"Item,Description,Buy Vol,Buy Max,Sell Vol,Sell Min"
}
local fs = formlib.Builder()
local all_items = minetest.registered_items
for i, row in ipairs(exchange:market_summary()) do
local n = #res+1
res[n] = "," .. row.item_name
local def = all_items[row.item_name] or {}
res[n+1] = "," .. (def.description or "Unknown Item")
res[n+2] = "," .. (row.buy_volume or 0)
res[n+3] = "," .. (row.buy_max or "N/A")
res[n+4] = "," .. (row.sell_volume or 0)
res[n+5] = "," .. (row.sell_min or "N/A")
end
fs:tablecolumns("text", "text", "text", "text", "text", "text", "text")
fs:table(0,0, 11.75,9, "summary_table", function(add_row)
add_row("Item",
"Description",
"Wear",
"Buy Vol",
"Buy Max",
"Sell Vol",
"Sell Min")
res[#res+1] = "]"
res[#res+1] = "button[3,7;2,1;back;Back]"
local all_items = minetest.registered_items
for i, row in ipairs(exchange:market_summary()) do
local def = all_items[row.Item] or {}
add_row(row.Item,
split(def.description, "\n")[1] or "Unknown Item",
wear_string(row.Wear),
row.Buy_Volume or 0,
row.Buy_Max or "N/A",
row.Sell_Volume or 0,
row.Sell_Min or "N/A")
end
end)
summary_fs = table.concat(res)
summary_fs = tostring(fs)
end
minetest.after(0, mk_summary_fs)
@ -42,26 +70,34 @@ minetest.register_globalstep(function(dtime)
end
end)
local summary_form = "global_exchange:summary"
local function show_summary(p_name)
minetest.show_formspec(p_name, summary_form, summary_fs)
local wear_levels = {
[1] = { index = 1, text = "New (-0%)", wear = math.floor(0.00*65535) },
[2] = { index = 2, text = "Good (-10%)", wear = math.floor(0.10*65535) },
[3] = { index = 3, text = "Worn (-50%)", wear = math.floor(0.50*65535) },
[4] = { index = 4, text = "Junk (-100%)", wear = math.floor(1.00*65535) },
}
-- Allow lookup by text label as well as index
for _,v in ipairs(wear_levels) do
wear_levels[tostring(v.text)] = v
end
local main_state = {}
-- ^ A per-player state for the main form. It contains these values:
-- old_fields: Keeps track of the fields before this update, when changing
-- things slightly
-- search_results: The last search results the player obtained
-- last_search_time: The last time the player did a search. Used to implement
-- a cooldown on searches
-- sell: A boolean whether the player has sell selected
-- ^ A per-player state for the main form.
minetest.register_on_joinplayer(function(player)
exchange:new_account(player:get_player_name()) --just to make sure
main_state[player:get_player_name()] = {
old_fields = {},
search_results = {},
last_search_time = 0,
tab = 1,
buy_item = "",
buy_wear = wear_levels[1].text,
buy_price = "",
buy_amount = "1",
sell_price = "",
buy_page = 1,
selected_index = 0,
}
end)
@ -70,268 +106,268 @@ minetest.register_on_leaveplayer(function(player)
end)
local main_form = "global_exchange:exchange_main"
local tablecolumns =
"tablecolumns[text;text;text;text;text;text]"
local function table_from_results(results, x, y, w, h, selected)
local fs_tab
local function insert(str)
fs_tab[#fs_tab+1] = str
end
fs_tab = {
tablecolumns,
"table[" .. x .. "," .. y .. ";" .. w .. "," .. h .. ";",
"result_table;",
"Poster,Type,Item,Description,Amount,Rate"
}
local all_items = minetest.registered_items
for i, row in ipairs(results) do
insert(",")
insert(tostring(row.Poster))
insert(",")
insert(tostring(row.Type))
insert(",")
insert(tostring(row.Item))
insert(",")
local def = all_items[row.Item] or {}
insert(def.description or "Unknown Item")
insert(",")
insert(tostring(row.Amount))
insert(",")
insert(tostring(row.Rate))
end
if selected and selected ~= "" then
insert(";")
insert(selected)
end
insert("]")
return table.concat(fs_tab)
end
local function mk_main_fs(p_name, new_item, err_str, success)
local state = main_state[p_name]
if not state then return end -- Should have been initialized on player join
local old_fields = state.old_fields
local results = state.search_results
local item_def = new_item or old_fields.item or ""
local amount_def = old_fields.amount or ""
local rate_def = old_fields.rate or ""
local sell_def = state.sell or false
local selected_def = old_fields.selected or ""
local bal = exchange:get_balance(p_name)
local fs
if bal then
fs = "label[0,0;Balance: " .. bal .. "]"
else
fs = "label[0.2,0.5;Use an ATM to make your account.]"
end
fs = fs .. "button[4,0;2,1;summary;Market Summary]" ..
"button[6,0;2,1;your_orders;Your Orders]" ..
"field[0.2,1.5;3,1;item;Item: ;" .. item_def .. "]" ..
"field[3.2,1.5;3,1;amount;Amount: ;" .. amount_def .. "]" ..
"button[6,1;2,1.4;select_item;Select Item]" ..
"checkbox[5,3;sell;Sell;" .. tostring(sell_def) .. "]" ..
"field[0.2,2.5;2,1;rate;Rate: ;" .. rate_def .. "]" ..
"button[2,2;2,1.4;search;Search]" ..
"button[4,2;3,1.4;post_order;Post Order]"
if err_str then
fs = fs .. "label[0,3;Error: " .. err_str .. "]"
end
if success then
fs = fs .. "label[0,3;Success!]"
end
return "size[8,9]" .. fs .. table_from_results(results, 0, 4, 8, 5, selected_def)
end
local function show_main(p_name, new_item, err_str, success)
minetest.show_formspec(p_name, main_form, mk_main_fs(p_name, new_item, err_str, success))
end
-- Something similar to creative inventory
local pagemax = 1
local pagewidth = 12
local pageheight = 4
local pageitems = pagewidth * pageheight
local selectable_list = {}
-- Create detached inventory after loading all mods
-- Create inventory list after loading all mods
minetest.after(0, function()
local inv = minetest.create_detached_inventory("global_exchange", {
allow_move = function(inv, from_list, _, to_list, _,_, player)
local p_name = player:get_player_name()
if from_list == "main"
and to_list == "p_" .. p_name then
return 1
else
return 0
end
end,
allow_put = function()
return 0
end,
allow_take = function()
return 0
end,
on_move = function(inv, _, _, _, _, _, player)
local p_name = player:get_player_name()
local p_list = "p_" .. p_name
local item_name = inv:get_list(p_list)[1]:get_name()
inv:set_list(p_list, {})
inv:add_item("main", item_name)
show_main(p_name, item_name)
end,
})
local selectable_list,n = {},1
for name, def in pairs(minetest.registered_items) do
if (not def.groups.not_in_creative_inventory
or def.groups.not_in_creative_inventory == 0)
and def.description
and def.description ~= "" then
selectable_list[n] = name
n = n+1
if (def.groups.not_in_creative_inventory or 0) == 0 and
(split(def.description, "\n")[1] or "") ~= "" then
selectable_list[#selectable_list + 1] = name
end
end
table.sort(selectable_list)
inv:set_size("main", #selectable_list)
for _,itemstring in ipairs(selectable_list) do
inv:add_item("main", ItemStack(itemstring))
pagemax = math.max(math.ceil(#selectable_list / pageitems), 1)
end)
local main_form = "global_exchange:exchange_main"
local function table_from_results(fs, results, name, x, y, w, h, selected)
fs:tablecolumns("text", "text", "text", "text", "text", "text", "text")
fs:table(x,y, w,h, name, function(add_row)
add_row("Poster", "Type", "Item",
"Description",
"Wear", "Amount", "Rate")
local all_items = minetest.registered_items
for i, row in ipairs(results) do
local def = all_items[row.Item] or {}
add_row(row.Poster, row.Type, row.Item,
split(def.description, "\n")[1] or "Unknown Item",
wear_string(row.Wear), row.Amount, row.Rate)
end
end, math.max(0, tonumber(selected) or 0) + 1)
end
local function mk_main_market_fs(fs, p_name, state)
fs(summary_fs)
end
local function mk_main_order_book_fs(fs, p_name, x, y, w, h, item_name)
local order_book = exchange:order_book("", item_name)
fs:tablecolumns("text", "text", "text", "text")
fs:table(x,y, w,h, "order_book", function(add_row)
add_row("Type", "Rate", "Wear", "Amount")
for _,row in ipairs(order_book) do
add_row(row.Type, row.Rate, wear_string(row.Wear), row.Amount)
end
end, 1)
end
local function mk_main_buy_fs(fs, p_name, state)
mk_main_order_book_fs(fs, p_name, 0, 0, 8.75, 3.75, state.buy_item)
fs:item_image_button(9,0, 1,1, "buy_item", state.buy_item)
fs:field(10.25,0.40, 2,1, "buy_amount", "Quantity", state.buy_amount, false)
local wear = wear_levels[state.buy_wear] or wear_levels[1]
fs:dropdown(9,1, 3, "buy_wear", function(add_item)
for _,v in ipairs(wear_levels) do
add_item(v.text)
end
end, wear.index)
fs:field(9.35,2.40, 2.9,1, "buy_price", "Bid (ea.)", state.buy_price, false)
fs:button(9,3, 3,1, "buy", "Place Bid")
fs:container(0,4, function()
fs:button( 0,0.25, 1,1, "buy_left", "<<")
fs:button( 5,0.25, 2,1, "position", state.buy_page .. "/" .. pagemax)
fs:button(11,0.25, 1,1, "buy_right", ">>")
local firstitem = ((state.buy_page - 1) * pageitems)
for y=0,(pageheight-1) do
for x=0,(pagewidth-1) do
local index = firstitem + (pagewidth * y) + x + 1
if selectable_list[index] then
fs:item_image_button(x,1.25+y, 1,1, "select_" .. index,
selectable_list[index])
end
end
end
end)
end
local function mk_main_sell_fs(fs, p_name, state)
local sell_stack = global_inv:get_stack("p_" .. p_name, 1)
local sell_item = (not sell_stack:is_empty()
and sell_stack:get_name()) or ""
mk_main_order_book_fs(fs, p_name, 0, 0, 8.75, 3.75, sell_item)
fs:list(9,0, 1,1, "detached:global_exchange", "p_" .. p_name)
fs:field(9.35,2.40, 2.9,1, "sell_price", "Ask (ea.)", state.sell_price, false)
fs:button(9,3, 3,1, "sell", "Sell")
fs:box(1.9375,5.1875, 7.96875,4.03, "#00000020")
fs:list(2,5.25, 8,4, "current_player", "main")
end
local function mk_main_own_orders_fs(fs, p_name, state)
if not state.own_results then
state.own_results = exchange:search_player_orders(p_name) or {}
end
pagemax = math.ceil((#selectable_list - 1) / (8 * 4))
end)
state.selected_index = math.min(state.selected_index or 0, #state.own_results)
table_from_results(fs, state.own_results, "result_table", 0, 0, 11.75, 8.5, state.selected_index)
fs:button(4.5,8.5, 3,1, "cancel", "Cancel Order")
end
local main_tabs = {
[1] = { text = "Market", mk_fs = mk_main_market_fs },
[2] = { text = "Buy", mk_fs = mk_main_buy_fs },
[3] = { text = "Sell", mk_fs = mk_main_sell_fs },
[4] = { text = "My Orders", mk_fs = mk_main_own_orders_fs },
}
local function mk_main_fs(fs, p_name, err_str, success)
local state = main_state[p_name]
if not state then return end -- Should have been initialized on player join
fs:size(12,10)
fs:bgcolor("#606060", false)
fs:tabheader(0,0.65, "tab", function(add_tab)
for _,tab in ipairs(main_tabs) do
add_tab(tab.text)
end
end, state.tab or 1, false, true)
local bal = exchange:get_balance(p_name)
fs:label(0,0.37, "Balance: " .. bal)
if err_str then
fs:label(4,0.37, err_str)
elseif success then
fs:label(4,0.37, "Success!")
end
if main_tabs[state.tab] then
fs:container(0,1, main_tabs[state.tab].mk_fs, p_name, state)
end
end
local function show_main(p_name, err_str, success)
local fs = formlib.Builder()
mk_main_fs(fs, p_name, err_str, success)
minetest.show_formspec(p_name, main_form, tostring(fs))
end
minetest.register_on_joinplayer(function(player)
-- the inventory list name is "p_"..player_name
minetest.get_inventory({
type="detached",
name="global_exchange"
}):set_size("p_" .. player:get_player_name(), 1)
-- the inventory list name (for selling) is "p_"..player_name
global_inv:set_size("p_" .. player:get_player_name(), 1)
end)
local select_form = "global_exchange:select_form"
local function mk_select_formspec(p_name, start_i, pagenum)
return "size[9.3,8]" ..
"list[detached:global_exchange;main;0.3,0.5;8,4;" .. tostring(start_i) .. "]" ..
"button[0.3,4.5;1.6,1;select_prev;<<]"..
"button[6.7,4.5;1.6,1;select_next;>>]"..
"label[2.0,5.55;"..tostring(math.floor(pagenum)).."/"..tostring(pagemax).."]"..
"list[detached:global_exchange;p_" .. p_name .. ";0.3,7;1,1;]"
end
local player_pages = {}
local function show_select(p_name)
local pagenum = player_pages[p_name] or 1
local start_i = (pagenum - 1) * 8 * 4
local fs = mk_select_formspec(p_name, start_i, pagenum)
minetest.show_formspec(p_name, select_form, fs)
end
local own_form = "global_exchange:my_orders"
local own_state = {}
-- ^ Per=player state for the own orders form. Contains these fields:
-- selected_index: The selected index
-- own_results: Results for own orders.
local function mk_own_orders_fs(p_name, results, selected)
return "size[8,8]" ..
"label[0.5,0.2;Your Orders]" ..
"button[6,0;2,1;refresh;Refresh]" ..
table_from_results(results, 0, 2, 8, 4.5, selected or "") ..
"button[0,7;2,1;cancel;Cancel]" ..
"button[3,7;2,1;back;Back]"
end
local function show_own_orders(p_name, results, selected)
minetest.show_formspec(p_name, own_form, mk_own_orders_fs(p_name, results, selected))
end
-- Returns success, and also returns an error message if failed.
local function post_order(player, ex_name, order_type, item_name, amount_str, rate_str)
local function post_buy(player, ex_name, item_name, wear_str, amount_str, rate_str)
local p_name = player:get_player_name()
if item_name == "" then
if (item_name or "") == "" then
return false, "You must input an item"
end
if not minetest.registered_items[item_name] then
elseif not minetest.registered_items[item_name] then
return false, "That item does not exist."
end
local wear_level = wear_levels[wear_str]
if not wear_level then
return false, "Invalid wear."
end
local amount = tonumber(amount_str)
local rate = tonumber(rate_str)
local rate = tonumber(rate_str)
if not amount then
if not amount or not is_integer(amount) or amount < 1 then
return false, "Invalid amount."
end
if not rate then
elseif not rate or not is_integer(rate) or rate < 1 then
return false, "Invalid rate."
end
if amount > 1000 then
return false, "Max amount is 1000"
end
local p_inv = player:get_inventory()
local stack = ItemStack(item_name)
stack:set_count(amount)
if order_type == "buy" then
if not p_inv:room_for_item("main", stack) then
return false, "Not enough space in inventory."
end
local succ, res = exchange:buy(p_name, ex_name, item_name, amount, rate)
if not succ then
return false, res
end
stack:set_count(res)
p_inv:add_item("main", stack)
else
if not p_inv:contains_item("main", stack) then
return false, "Items not in inventory."
end
local succ, res = exchange:sell(p_name, ex_name, item_name, amount, rate)
if not succ then
return false, res
end
p_inv:remove_item("main", stack)
local succ, res = exchange:buy(p_name, ex_name, item_name, wear_level.wear, amount, rate)
if not succ then
return false, res
end
for _,row in ipairs(res) do
stack:set_count(row.amount)
stack:set_wear(row.wear)
local leftover = p_inv:add_item("main", stack)
-- Put anything that won't fit in the inventory in the player's inbox
if not leftover:is_empty() then
exchange:put_in_inbox(p_name, item_name, row.wear, leftover:get_count())
end
end
-- Refresh market summary "soonish"
elapsed = math.max(elapsed, summary_interval - 5)
return true
end
-- Returns success, and also returns an error message if failed.
-- The item to sell is determined by the player's list in global_inv.
local function post_sell(player, ex_name, rate_str)
local p_name = player:get_player_name()
local stack = global_inv:get_stack("p_" .. p_name, 1)
if not stack or stack:is_empty() then
return false, "You must input an item"
elseif not minetest.registered_items[stack:get_name()] then
return false, "That item does not exist."
end
if stack.get_meta then
local meta = stack:get_meta()
local def_meta = ItemStack(stack:get_name()):get_meta()
if not stack:get_meta():equals(def_meta) then
return false, "Cannot sell an item with metadata."
end
elseif (stack:get_metadata() or "") ~= "" then
return false, "Cannot sell an item with metadata."
end
local rate = tonumber(rate_str)
if not rate or not is_integer(rate) or rate < 1 then
return false, "Invalid rate."
end
local item_name = stack:get_name()
local wear = stack:get_wear()
local amount = stack:get_count()
local succ, res = exchange:sell(p_name, ex_name, item_name, wear, amount, rate)
if not succ then
return false, res
end
stack:clear()
global_inv:set_stack("p_" .. p_name, 1, stack)
-- Refresh market summary "soonish"
elapsed = math.max(elapsed, summary_interval - 5)
return true
end
@ -339,185 +375,113 @@ end
local function handle_main(player, fields)
local p_name = player:get_player_name()
local state = main_state[p_name]
local old_fields = state.old_fields
for k, v in pairs(fields) do
old_fields[k] = v
local copy_fields = {
"buy_wear",
"buy_amount",
"buy_price",
"sell_price"
}
for _,k in ipairs(copy_fields) do
if fields[k] then
state[k] = fields[k]
end
end
if fields.select_item then
show_select(p_name)
end
if fields.search then
local now = os.time()
local last_search = state.last_search_time
if now - last_search < search_cooldown then
show_main(p_name, nil, "Please wait before searching again.")
return
end
-- If the player is selling, she wants "buy" type offers.
local order_type
if state.sell then
order_type = "buy"
else
order_type = "sell"
end
local item_name = fields["item"]
local results = exchange:search_orders("", order_type, item_name)
state.search_results = results
state.last_search_time = now
if fields.tab then
state.tab = tonumber(fields.tab) or 1
show_main(p_name)
end
if fields.buy_left then
state.buy_page = (((state.buy_page or 1) + ((2*pagemax-1) - 1)) % pagemax) + 1
show_main(p_name)
end
if fields.buy_right then
state.buy_page = (((state.buy_page or 1) + ((2*pagemax-1) + 1)) % pagemax) + 1
show_main(p_name)
end
for name in pairs(fields) do
local index = tonumber(string.match(name, "select_([0-9]+)"))
if index and index >= 1 and index < #selectable_list then
state.buy_item = selectable_list[index]
show_main(p_name)
end
end
if fields.buy then
local succ, err =
post_buy(player, "", state.buy_item, fields.buy_wear,
fields.buy_amount, fields.buy_price)
if succ then
state.buy_amount = "1"
state.buy_price = ""
state.own_results = nil
show_main(p_name, nil, true)
else
show_main(p_name, err)
end
end
if fields.sell then
state.sell = fields.sell == "true"
end
if fields.post_order then
local now = os.time()
local last_search = state.last_search_time
if now - last_search < search_cooldown then
show_main(p_name, nil, "Please wait before posting.")
return
end
local order_type
if state.sell then
order_type = "sell"
else
order_type = "buy"
end
local succ, err =
post_order(player, "", order_type, fields.item, fields.amount, fields.rate)
local succ, err = post_sell(player, "", fields.sell_price)
if succ then
state.search_results = {}
show_main(p_name, nil, nil, true)
state.sell_price = ""
state.own_results = nil
show_main(p_name, nil, true)
else
show_main(p_name, nil, err)
show_main(p_name, err)
end
end
if fields.result_table then
local results = state.search_results
local event = minetest.explode_table_event(fields.result_table)
if event.type ~= "CHG" then
return
end
local index = event.row - 1
result = results[index]
if result then
old_fields.amount = tostring(result.Amount)
old_fields.rate = tostring(result.Rate)
end
show_main(p_name)
end
if fields.summary then
show_summary(p_name)
end
if fields.your_orders then
if not own_state[p_name] then
own_state[p_name] = {}
end
local o_state = own_state[p_name]
o_state.own_results = exchange:search_player_orders(p_name) or {}
show_own_orders(p_name, o_state.own_results)
end
end
local function handle_select(player, fields)
local p_name = player:get_player_name()
local pagenum = player_pages[p_name] or 1
if fields.select_prev then
player_pages[p_name] = math.max(1, pagenum - 1)
show_select(p_name)
elseif fields.select_next then
player_pages[p_name] = math.min(pagemax, pagenum + 1)
show_select(p_name)
end
end
local function handle_own_orders(player, fields)
local p_name = player:get_player_name()
local state = own_state[p_name] or {}
local results = state.own_results or {}
local idx = state.selected_index
local own_results = state.own_results or {}
if fields.refresh then
state.own_results = exchange:search_player_orders(p_name) or {}
show_own_orders(p_name, state.own_results)
end
if fields.cancel and idx then
local row = results[idx]
if not row then return true end
local p_inv = player:get_inventory()
local amount = row.Amount
local item = row.Item
local stack = ItemStack(item)
stack:set_count(amount)
if row.Type == "sell" then
if not p_inv:room_for_item("main", stack) then
show_own_orders(p_name, state.own_results, "Not enough room.")
return true
end
end
local succ, err =
exchange:cancel_order(p_name, row.Id, row.Type, row.Item, row.Amount, row.Rate)
if fields.cancel and own_results[idx] then
local succ, res = exchange:cancel_order(p_name, own_results[idx].Id)
if succ then
table.remove(results, idx)
if row.Type == "sell" then
p_inv:add_item("main", stack)
if res.Type == "sell" then
local p_inv = player:get_inventory()
local stack = ItemStack(res.Item)
stack:set_count(res.Amount)
stack:set_wear(res.Wear)
local leftover = p_inv:add_item("main", stack)
-- Put anything that won't fit in the inventory in the player's inbox
if not leftover:is_empty() then
exchange:put_in_inbox(p_name, res.Item, res.Wear, leftover:get_count())
end
end
else
-- Refresh the results, since there might have been a problem.
state.own_results = exchange:search_player_orders(p_name) or {}
-- Refresh market summary "soonish"
elapsed = math.max(elapsed, summary_interval - 5)
end
show_own_orders(p_name, state.own_results)
state.own_results = nil
show_main(p_name)
end
if fields.result_table then
local event = minetest.explode_table_event(fields.result_table)
if event.type == "CHG" then
state.selected_index = event.row - 1
show_own_orders(p_name, results, event.row)
show_main(p_name)
end
end
if fields.back then
show_main(p_name)
end
end
if fields.quit then
-- Return the player's unsold inventory, if any
local stack = global_inv:get_stack("p_" .. p_name, 1)
local p_inv = player:get_inventory()
local leftover = p_inv:add_item("main", stack)
-- Whatever doesn't fit in the player's inventory stays in the form.
-- Note that any items in the form when the server exits are lost.
global_inv:set_stack("p_" .. p_name, 1, leftover)
local function handle_summary(player, fields)
if fields.back then
show_main(player:get_player_name())
state.own_results = nil
end
end
@ -525,12 +489,6 @@ end
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname == main_form then
handle_main(player, fields)
elseif formname == select_form then
handle_select(player, fields)
elseif formname == own_form then
handle_own_orders(player, fields)
elseif formname == summary_form then
handle_summary(player, fields)
else
return
end
@ -538,6 +496,37 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
end)
global_inv = minetest.create_detached_inventory("global_exchange", {
allow_move = function(inv,from_list,from_index,to_list,to_index,count,player)
return 0
end,
allow_put = function(inv,to_list,to_index,stack,player)
local p_name = player:get_player_name()
if to_list == "p_" .. p_name then
return stack:get_count()
else
return 0
end
end,
allow_take = function(inv,from_list,from_index,stack,player)
local p_name = player:get_player_name()
if from_list == "p_" .. p_name then
return stack:get_count()
else
return 0
end
end,
on_put = function(inv,to_list,to_index,stack,player)
show_main(player:get_player_name())
end,
on_take = function(inv,from_list,from_index,stack,player)
show_main(player:get_player_name())
end,
})
minetest.register_node("global_exchange:exchange", {
description = "Exchange Terminal",
drawtype = "nodebox",
@ -552,18 +541,19 @@ minetest.register_node("global_exchange:exchange", {
paramtype = "light",
paramtype2 = "facedir",
groups = {cracky=2},
is_ground_content = false,
stack_max = 1,
light_source = 3,
node_box = {
type = "fixed",
fixed = {
{-8/16, -4/16, 3/16, 0.5, 0.5, 5/16},--screens
{-1/16, -7/16, 5/16, 1/16, 5/16, 7/16},--screen leg
{-3/16, -8/16, 4/16, 3/16, -7/16, 8/16},--leg platform
{-8/16, -4/16, 3/16, 8/16, 8/16, 5/16},--screens
{-1/16, -7/16, 5/16, 1/16, 5/16, 7/16},--screen leg
{-3/16, -8/16, 4/16, 3/16, -7/16, 8/16},--leg platform
{-7/16, -8/16, -8/16, 2/16, -6/16, -3/16},--keyboard
{3/16, -8/16, -3/16, 7/16, -7/16, 3/16},--phone low
{4/16, -7/16, -1/16, 6/16, -6/16, 3/16},--phone hi
{2/16, -7/16, 0, 8/16, -5/16, 2/16},--phone speaker
{ 3/16, -8/16, -3/16, 7/16, -7/16, 3/16},--phone low
{ 4/16, -7/16, -1/16, 6/16, -6/16, 3/16},--phone hi
{ 2/16, -7/16, 0/16, 8/16, -5/16, 2/16},--phone speaker
}
},
on_rightclick = function(_, _, clicker)
@ -581,8 +571,9 @@ minetest.register_node("global_exchange:exchange", {
minetest.register_craft( {
output = "global_exchange:exchange",
recipe = {
{ "default:steel_ingot", "default:steel_ingot", "default:steel_ingot" },
{ "default:mese_crystal", "default:steel_ingot", "default:diamond" },
{ "default:steel_ingot", "default:steel_ingot", "default:steel_ingot" },
{ "default:steel_ingot", "default:steel_ingot", "default:steel_ingot" },
{ "default:mese_crystal", "default:steel_ingot", "default:diamond" },
{ "default:steel_ingot", "default:steel_ingot", "default:steel_ingot" },
}
})
-- vim:set ts=4 sw=4 noet:

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,45 @@
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 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 = "Can use /setbalance",
give_to_singleplayer = false
})
minetest.register_chatcommand("setbalance", {
params = "[<name>] <balance>",
description = "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, "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.")