11 Commits

7 changed files with 1544 additions and 1157 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.

326
atm.lua
View File

@ -1,152 +1,196 @@
-- 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 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] unique_num = unique_num + 1
button[4,0;2,1;wire;Wire Monies] return unique_num
button[1,1;4,1;transaction_log;Transaction Log] end
]] end)(0)
local coins = {
[1] = "bitchange:minecoinblock", [2] = "bitchange:minecoin", [3] = "bitchange:mineninth",
[4] = "maptools:gold_coin", [5] = "maptools:silver_coin", [6] = "maptools:copper_coin"
}
local function logout(x,y) local coins_convert = {
return "button[" .. x .. "," .. y .. ";2,1;logout;Log Out]" [coins[6]]=1, [coins[5]]=9, [coins[4]]=81, [coins[3]]=729, [coins[2]]=6561, [coins[1]]=59049,
end ["minercantile:copper_coin"]=1, ["minercantile:silver_coin"]=9, ["minercantile:gold_coin"]=81
}
local function withdraw_fs(fs, p_name, amount)
local spos = atm_pos[p_name].x..","..atm_pos[p_name].y..","..atm_pos[p_name].z
local inv = minetest.get_inventory({ type = "node", pos={x=atm_pos[p_name].x, y=atm_pos[p_name].y, z=atm_pos[p_name].z} })
local function label(x,y,text) local msg = ""
return "label[" .. x .. "," .. y .. ";" local w_amount = tonumber(amount)
.. minetest.formspec_escape(text) .. "]" if w_amount and math.floor(w_amount) == w_amount and w_amount > 0 then
end local succ, err = exchange:give_credits(p_name, 0 - w_amount, "Cash withdrawal: (-"..w_amount..")")
if succ then
local index = 1
repeat
local m = math.floor(w_amount / coins_convert[coins[index]])
if m > 0 then
inv:add_item( "main", ItemStack({ name = coins[index], count = m }) )
end
w_amount = w_amount - (coins_convert[coins[index]] * m)
index = index + 1
until ( w_amount == 0)
else
msg = err
end
elseif w_amount then
msg = "Invalid number ! Must be an Integer > 0"
end
local function field(x,y, w,h, name, label, default)
return "field[" .. x .. "," .. y .. ";" .. w .. "," .. h .. ";"
.. name .. ";" .. minetest.formspec_escape(label) .. ";"
.. minetest.formspec_escape(default) .. "]"
end
local unique_num = 1
local function unique()
local ret = unique_num
unique_num = unique_num + 1
return ret
end
local function info_fs(p_name)
local balance = exchange:get_balance(p_name) local balance = exchange:get_balance(p_name)
local fs fs:size(8,10)
if not balance then 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 else
fs = label(0.5,0.5, "Balance: " .. balance) fs:label(3,8.9, "Balance: " .. balance)
fs:field(0.75, 1.25, 3.25, 1, "w_amount", "Desired amount:")
fs:button(4, 1, 3.25, 1, "withdraw", "Get !")
fs:label(1, 2.25, "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 end
fs:button(0,8.75, 2,1, "logout", "Log Out")
return "size[4,3]" .. fs .. logout(0.5,2)
end end
local function info_fs(fs, p_name)
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(4,3)
if not balance then if balance then
return fs .. label(0.5,0.5, "You don't have an account.") fs:label(0.5,0.5, "Balance: " .. balance)
else
fs:label(0.5,0.5, "You don't have an account.")
end end
-- To prevent duplicates fs:button(1,2, 2,1, "logout", "Log Out")
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]"
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, "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 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 = "Invalid transfer amount."
"button[0.5,2;2,1;wire;Back]" 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 end
local succ, err = exchange:transfer_credits(p_name, receiver, amt) fs:label(0.5,0.5, msg)
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]"
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, "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", "Log Out")
end
local function main_menu_fs(fs, p_name)
fs:size(8,2)
fs:button(0,0.125, 4,1, "withdraw", "Cash deposit and withdrawal")
fs:button(4,0.125, 2,1, "info", "Account Info")
fs:button(6,0.125, 2,1, "wire", "Wire Monies")
fs:button(0.50, 1.125, 7, 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 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 +224,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 +268,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 +280,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, "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(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 +343,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 = {
@ -278,10 +367,11 @@ minetest.register_node("global_exchange:atm_top", {
minetest.register_craft( { minetest.register_craft( {
output = "global_exchange:atm", output = "global_exchange:atm",
recipe = { recipe = {
{ "default:stone", "default:stone", "default:stone" }, { "default:stone", "default:stone", "default:stone" },
{ "default:stone", "default:gold_ingot", "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") 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, 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
return mail_maybe if not mail_maybe then
else local _,res = exchange:view_inbox(p_name)
mailbox_contents[p_name] = {} mail_maybe = res or {}
return mailbox_contents[p_name] mailbox_contents[p_name] = mail_maybe
selected_index[p_name] = math.min(selected_index[p_name] or 0, #mail_maybe)
end end
return mail_maybe
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[",
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)
end end
local function mk_mail_fs(p_name, results, err_str) local function mk_inbox_list(fs, results, x, y, w, h)
fs = "size[6,8]" .. fs:textlist(x,y, w,h, "result_list", function(add_row)
"label[0,0;Inbox]" 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 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,
}) })
@ -125,7 +123,8 @@ minetest.register_craft( {
output = "global_exchange:mailbox", output = "global_exchange:mailbox",
recipe = { recipe = {
{ "default:stone", "default:gold_ingot", "default:stone" }, { "default:stone", "default:gold_ingot", "default:stone" },
{ "default:stone", "default:chest", "default:stone" }, { "default:stone", "default:chest", "default:stone" },
{ "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

View File

@ -1,35 +1,63 @@
local exchange, formlib = ...
local exchange = ...
local search_cooldown = 2 local search_cooldown = 2
local summary_interval = 600 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 summary_fs = ""
local function mk_summary_fs() local function mk_summary_fs()
local res = { local fs = formlib.Builder()
"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 all_items = minetest.registered_items fs:tablecolumns("text", "text", "text", "text", "text", "text", "text")
for i, row in ipairs(exchange:market_summary()) do fs:table(0,0, 11.75,9, "summary_table", function(add_row)
local n = #res+1 add_row("Item",
res[n] = "," .. row.item_name "Description",
local def = all_items[row.item_name] or {} "Wear",
res[n+1] = "," .. (def.description or "Unknown Item") "Buy Vol",
res[n+2] = "," .. (row.buy_volume or 0) "Buy Max",
res[n+3] = "," .. (row.buy_max or "N/A") "Sell Vol",
res[n+4] = "," .. (row.sell_volume or 0) "Sell Min")
res[n+5] = "," .. (row.sell_min or "N/A")
end
res[#res+1] = "]" local all_items = minetest.registered_items
res[#res+1] = "button[3,7;2,1;back;Back]" 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 end
minetest.after(0, mk_summary_fs) minetest.after(0, mk_summary_fs)
@ -42,26 +70,34 @@ minetest.register_globalstep(function(dtime)
end end
end) end)
local summary_form = "global_exchange:summary"
local function show_summary(p_name) local wear_levels = {
minetest.show_formspec(p_name, summary_form, summary_fs) [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 end
local main_state = {} local main_state = {}
-- ^ A per-player state for the main form. It contains these values: -- ^ A per-player state for the main form.
-- 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
minetest.register_on_joinplayer(function(player) minetest.register_on_joinplayer(function(player)
exchange:new_account(player:get_player_name()) --just to make sure exchange:new_account(player:get_player_name()) --just to make sure
main_state[player:get_player_name()] = { main_state[player:get_player_name()] = {
old_fields = {}, tab = 1,
search_results = {}, buy_item = "",
last_search_time = 0, buy_wear = wear_levels[1].text,
buy_price = "",
buy_amount = "1",
sell_price = "",
buy_page = 1,
selected_index = 0,
} }
end) end)
@ -70,268 +106,268 @@ minetest.register_on_leaveplayer(function(player)
end) 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 -- Something similar to creative inventory
local pagemax = 1 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() 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 for name, def in pairs(minetest.registered_items) do
if (not def.groups.not_in_creative_inventory if (def.groups.not_in_creative_inventory or 0) == 0 and
or def.groups.not_in_creative_inventory == 0) (split(def.description, "\n")[1] or "") ~= "" then
and def.description selectable_list[#selectable_list + 1] = name
and def.description ~= "" then
selectable_list[n] = name
n = n+1
end end
end end
table.sort(selectable_list) table.sort(selectable_list)
inv:set_size("main", #selectable_list)
for _,itemstring in ipairs(selectable_list) do pagemax = math.max(math.ceil(#selectable_list / pageitems), 1)
inv:add_item("main", ItemStack(itemstring)) 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 end
pagemax = math.ceil((#selectable_list - 1) / (8 * 4)) state.selected_index = math.min(state.selected_index or 0, #state.own_results)
end)
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) minetest.register_on_joinplayer(function(player)
-- the inventory list name is "p_"..player_name -- the inventory list name (for selling) is "p_"..player_name
minetest.get_inventory({ global_inv:set_size("p_" .. player:get_player_name(), 1)
type="detached",
name="global_exchange"
}):set_size("p_" .. player:get_player_name(), 1)
end) 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. -- 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() local p_name = player:get_player_name()
if item_name == "" then if (item_name or "") == "" then
return false, "You must input an item" return false, "You must input an item"
end elseif not minetest.registered_items[item_name] then
if not minetest.registered_items[item_name] then
return false, "That item does not exist." return false, "That item does not exist."
end end
local wear_level = wear_levels[wear_str]
if not wear_level then
return false, "Invalid wear."
end
local amount = tonumber(amount_str) 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." return false, "Invalid amount."
end elseif not rate or not is_integer(rate) or rate < 1 then
if not rate then
return false, "Invalid rate." return false, "Invalid rate."
end end
if amount > 1000 then
return false, "Max amount is 1000"
end
local p_inv = player:get_inventory() local p_inv = player:get_inventory()
local stack = ItemStack(item_name) local stack = ItemStack(item_name)
stack:set_count(amount)
if order_type == "buy" then local succ, res = exchange:buy(p_name, ex_name, item_name, wear_level.wear, amount, rate)
if not p_inv:room_for_item("main", stack) then if not succ then
return false, "Not enough space in inventory." return false, res
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)
end 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 return true
end end
@ -339,185 +375,113 @@ end
local function handle_main(player, fields) local function handle_main(player, fields)
local p_name = player:get_player_name() local p_name = player:get_player_name()
local state = main_state[p_name] local state = main_state[p_name]
local old_fields = state.old_fields
for k, v in pairs(fields) do local copy_fields = {
old_fields[k] = v "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 end
if fields.select_item then if fields.tab then
show_select(p_name) state.tab = tonumber(fields.tab) or 1
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
show_main(p_name) show_main(p_name)
end 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 if fields.sell then
state.sell = fields.sell == "true" local succ, err = post_sell(player, "", fields.sell_price)
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)
if succ then if succ then
state.search_results = {} state.sell_price = ""
show_main(p_name, nil, nil, true) state.own_results = nil
show_main(p_name, nil, true)
else else
show_main(p_name, nil, err) show_main(p_name, err)
end end
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 idx = state.selected_index
local own_results = state.own_results or {}
if fields.refresh then if fields.cancel and own_results[idx] then
state.own_results = exchange:search_player_orders(p_name) or {} local succ, res = exchange:cancel_order(p_name, own_results[idx].Id)
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 succ then if succ then
table.remove(results, idx) if res.Type == "sell" then
if row.Type == "sell" then local p_inv = player:get_inventory()
p_inv:add_item("main", stack) 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 end
else
-- Refresh the results, since there might have been a problem. -- Refresh market summary "soonish"
state.own_results = exchange:search_player_orders(p_name) or {} elapsed = math.max(elapsed, summary_interval - 5)
end end
show_own_orders(p_name, state.own_results) state.own_results = nil
show_main(p_name)
end end
if fields.result_table then if fields.result_table then
local event = minetest.explode_table_event(fields.result_table) local event = minetest.explode_table_event(fields.result_table)
if event.type == "CHG" then if event.type == "CHG" then
state.selected_index = event.row - 1 state.selected_index = event.row - 1
show_own_orders(p_name, results, event.row) show_main(p_name)
end end
end end
if fields.back then if fields.quit then
show_main(p_name) -- Return the player's unsold inventory, if any
end local stack = global_inv:get_stack("p_" .. p_name, 1)
end 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) state.own_results = nil
if fields.back then
show_main(player:get_player_name())
end end
end end
@ -525,12 +489,6 @@ end
minetest.register_on_player_receive_fields(function(player, formname, fields) minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname == main_form then if formname == main_form then
handle_main(player, fields) 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 else
return return
end end
@ -538,6 +496,37 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
end) 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", { minetest.register_node("global_exchange:exchange", {
description = "Exchange Terminal", description = "Exchange Terminal",
drawtype = "nodebox", drawtype = "nodebox",
@ -552,18 +541,19 @@ minetest.register_node("global_exchange:exchange", {
paramtype = "light", paramtype = "light",
paramtype2 = "facedir", paramtype2 = "facedir",
groups = {cracky=2}, groups = {cracky=2},
is_ground_content = false,
stack_max = 1, stack_max = 1,
light_source = 3, light_source = 3,
node_box = { node_box = {
type = "fixed", type = "fixed",
fixed = { fixed = {
{-8/16, -4/16, 3/16, 0.5, 0.5, 5/16},--screens {-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 {-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 {-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 {-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 { 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 { 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 { 2/16, -7/16, 0/16, 8/16, -5/16, 2/16},--phone speaker
} }
}, },
on_rightclick = function(_, _, clicker) on_rightclick = function(_, _, clicker)
@ -581,8 +571,9 @@ minetest.register_node("global_exchange:exchange", {
minetest.register_craft( { minetest.register_craft( {
output = "global_exchange:exchange", output = "global_exchange:exchange",
recipe = { recipe = {
{ "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: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" },
} }
}) })
-- 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() 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 exchange = assert(loadfile(modpath .. "exchange.lua"))(insecure_env).
local income_interval = 1200 open_exchange(minetest.get_worldpath() .. "/global_exchange.db")
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 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
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)
end 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) minetest.log("action", "[global_exchange] loaded.")
assert(loadfile(modpath .. "exchange_machine.lua"))(exchange)
assert(loadfile(modpath .. "digital_mailbox.lua"))(exchange)