Improved GUI

This commit is contained in:
Andrew Lalis 2023-09-17 10:14:56 -04:00
parent caf10ad4fc
commit 8926537bfc
3 changed files with 335 additions and 17 deletions

315
atm.lua
View File

@ -10,17 +10,93 @@ like recording transactions.
local g = require("simple-graphics")
local bankClient = require("bank-client")
-- The name of the peripheral where this ATM can draw money from.
local CURRENCY_SOURCE = "minecraft:barrel_0"
-- The name of the peripheral where this ATM can deposit money to.
local CURRENCY_SINK = "minecraft:barrel_1"
-- The name of the peripheral where this ATM interacts with the user.
local CURRENCY_BIN = "minecraft:barrel_2"
local BANK_HOST = "central-bank"
local SECURITY_KEY = "4514-1691-1660-7358-1884-0506-0878-7098-1511-3359-3602-3581-6910-0791-1843-5936"
local modem = peripheral.find("modem") or error("No modem attached.")
bankClient.init(peripheral.getName(modem), BANK_HOST, SECURITY_KEY)
if not peripheral.isPresent(CURRENCY_SOURCE) then error("No CURRENCY_SOURCE peripheral named \""..CURRENCY_SOURCE.."\" was found.") end
if not peripheral.isPresent(CURRENCY_SINK) then error("No CURRENCY_SINK peripheral named \""..CURRENCY_SINK.."\" was found.") end
if not peripheral.isPresent(CURRENCY_BIN) then error("No CURRENCY_BIN peripheral named \""..CURRENCY_BIN.."\" was found.") end
local W, H = term.getSize()
local function isCurrency(itemStack)
return itemStack ~= nil and itemStack.name == "minecraft:sunflower" and itemStack.nbt == "1b95aea642a1b0e9624787ed7227cf35"
end
local function countCurrency(peripheralName)
local inv = peripheral.wrap(peripheralName)
if not inv then return 0 end
local total = 0
for slot, itemStack in pairs(inv.list()) do
if isCurrency(itemStack) then total = total + itemStack.count end
end
return total
end
local function getFreeSpace(peripheralName)
local inv = peripheral.wrap(peripheralName)
if not inv then return 0 end
local space = 0
for i = 1, inv.size() do
local itemStack = inv.getItemDetail(i)
if itemStack == nil then
space = space + 64
elseif isCurrency(itemStack) then
space = space + (64 - itemStack.count)
end
end
return space
end
local function transferCurrency(fromName, toName, amount)
local sourceInv = peripheral.wrap(fromName)
local transferred = 0
local attempts = 0
while transferred < amount do
local items = sourceInv.list()
for slot, itemStack in pairs(items) do
if isCurrency(itemStack) then
local amountToTransfer = math.min(amount - transferred, itemStack.count)
local actualTransferred = sourceInv.pushItems(toName, slot, amountToTransfer)
transferred = transferred + actualTransferred
end
end
attempts = attempts + 1
if attempts > 10 and transferred < amount then
return false, transferred
end
end
return true, amount
end
local function shortId(account)
return "*" .. string.sub(account.id, -5, -1)
end
local function isDigit(char)
if #char ~= 1 then return false end
local intValue = string.byte(char) - string.byte("0")
return intValue >= 0 and intValue <= 9
end
local function drawFrame()
g.clear(term, colors.white)
g.drawXLine(term, 1, W, 1, colors.black)
g.drawText(term, 2, 1, "ATM", colors.white, colors.black)
if bankClient.loggedIn() then
local txt = "Logged in as " .. bankClient.state.auth.username
local len = #txt
g.drawText(term, W-len, 1, "Logged in as", colors.lightGray, colors.black)
g.drawText(term, W-len+13, 1, bankClient.state.auth.username, colors.yellow, colors.black)
end
end
local function tryReadDiskCredentials(name)
@ -91,6 +167,8 @@ local function tryLoginViaInput()
end
elseif keyCode == keys.tab and selectedInput == "username" then
selectedInput = "password"
elseif keyCode == keys.enter and selectedInput == "password" then
return {username = username, password = password} -- Do login right away.
end
elseif event == "mouse_click" then
local button = p1
@ -153,6 +231,176 @@ local function checkCredentialsUI(credentials)
return true
end
local function currencyBinPreviewUpdater(x, y, fg, bg, delay)
delay = delay or 1
return function()
while true do
local amount = countCurrency(CURRENCY_BIN)
g.drawXLine(term, x, x + 10, y, bg)
g.drawText(term, x, y, tostring(amount), fg, bg)
os.sleep(delay)
end
end
end
local function showDepositUI(account)
drawFrame()
g.drawTextCenter(term, W/2, 3, "Deposit HandieMarks to your account "..shortId(account)..".", colors.black, colors.white)
g.drawTextCenter(term, W/2, 5, "Add currency to the bin, then click to continue.", colors.black, colors.white)
g.drawText(term, 20, 8, "Amount to Deposit", colors.black, colors.white)
local continueButtonCoords = g.drawButton(term, 20, 12, 11, 3, "Continue", colors.white, colors.green)
local cancelButtonCoords = g.drawButton(term, 20, 16, 11, 3, "Cancel", colors.white, colors.red)
local state = {cancel = false, doDeposit = false}
parallel.waitForAny(
currencyBinPreviewUpdater(20, 9, colors.orange, colors.gray, 0.5),
function ()
while true do
local event, button, x, y = os.pullEvent("mouse_click")
if button == 1 then
if g.isButtonPressed(x, y, continueButtonCoords) and countCurrency(CURRENCY_BIN) > 0 then
state.doDeposit = true
return
elseif g.isButtonPressed(x, y, cancelButtonCoords) then
state.cancel = true
return
end
end
end
end
)
if state.cancel then return false end
if state.doDeposit then
local function tryReturnFundsInError(amount)
local returnSuccess, returnAmount = transferCurrency(CURRENCY_SOURCE, CURRENCY_BIN, amount)
if not returnSuccess then
local missingAmount = amount - returnAmount
g.appendAndDrawConsole(term, console, "Couldn't return all funds. You are still owed "..tostring(missingAmount).." $HMK. Please contact an administrator for assistance.", cx, cy)
os.sleep(3)
else
g.appendAndDrawConsole(term, console, "Your funds have been returned to the bin. Please collect them.", cx, cy)
os.sleep(3)
end
end
-- Clear the buttons and show some status.
g.fillRect(term, 1, W, 12, H-11, colors.white)
local console = g.createConsole(W/2, H-11, colors.white, colors.black, "UP")
local cx = W/2 - W/4
local cy = 11
local amount = countCurrency(CURRENCY_BIN)
g.appendAndDrawConsole(term, console, "Making deposit with value of "..tostring(amount).." $HMK...", cx, cy)
os.sleep(1)
local success, actualAmount = transferCurrency(CURRENCY_BIN, CURRENCY_SINK, amount)
if not success then
g.appendAndDrawConsole(term, console, "Transfer failed! Actual transfer: "..tostring(actualAmount).." $HMK. Please contact an administrator to report the issue.", cx, cy)
os.sleep(1)
tryReturnFundsInError(actualAmount)
return false
end
g.appendAndDrawConsole(term, console, "Transfer complete.", cx, cy)
os.sleep(1)
local tx, errorMsg = bankClient.recordTransaction(account.id, amount, "ATM deposit")
if not tx then
g.appendAndDrawConsole(term, console, "Failed to post transaction: " .. errorMsg, cx, cy)
tryReturnFundsInError(amount)
os.sleep(3)
return false
end
g.appendAndDrawConsole(term, console, "Transaction posted to account.", cx, cy)
os.sleep(2)
return true
end
end
local function showWithdrawUI(account)
drawFrame()
g.drawTextCenter(term, W/2, 3, "Withdraw HandieMarks from your account "..shortId(account)..".", colors.black, colors.white)
g.drawTextCenter(term, W/2, 5, "Enter an amount to withdraw:", colors.black, colors.white)
g.drawXLine(term, 20, 30, 6, colors.gray)
g.drawTextCenter(term, W/2, 7, "(Current balance: "..tostring(account.balance).." $HMK)", colors.gray, colors.white)
local continueButtonCoords = g.drawButton(term, 20, 12, 11, 3, "Continue", colors.white, colors.green)
local cancelButtonCoords = g.drawButton(term, 20, 16, 11, 3, "Cancel", colors.white, colors.red)
local inputValue = ""
local function drawInputValue(val)
g.drawXLine(term, 20, 30, 6, colors.gray)
local amountColor = colors.orange
local intValue = tonumber(val)
if intValue ~= nil and intValue > account.balance then
amountColor = colors.red
end
g.drawText(term, 20, 6, val, colors.orange, colors.gray)
end
while true do
local event, p1, p2, p3 = os.pullEvent()
if event == "char" and isDigit(p1) and #inputValue < 10 then
inputValue = inputValue .. p1
drawInputValue(inputValue)
elseif event == "key" and p1 == keys.backspace and #inputValue > 0 then
inputValue = string.sub(inputValue, 1, #inputValue - 1)
drawInputValue(inputValue)
elseif event == "mouse_click" and p1 == 1 then
local x = p2
local y = p3
local amount = tonumber(inputValue)
if g.isButtonPressed(x, y, continueButtonCoords) and amount ~= nil and amount > 0 and amount <= account.balance then
local function tryReclaimFundsInError(amount)
local returnSuccess, returnAmount = transferCurrency(CURRENCY_BIN, CURRENCY_SINK, amount)
if not returnSuccess then
g.appendAndDrawConsole(term, console, "Failed to reclaim funds. Please contact an administrator.", cx, cy)
end
end
-- Do withdrawal
g.fillRect(term, 1, W, 12, H-11, colors.white)
local console = g.createConsole(W/2, H-11, colors.white, colors.black, "UP")
local cx = W/2 - W/4
local cy = 11
g.appendAndDrawConsole(term, console, "Making withdrawal of " .. tostring(amount) .. " $HMK from account " .. shortId(account) .. ".", cx, cy)
os.sleep(1)
local withdrawn = 0
while withdrawn < amount do
local freeSpace = getFreeSpace(CURRENCY_BIN)
if freeSpace < 1 then
g.appendAndDrawConsole(term, console, "No space available in the bin. Please take some currency out to continue.", cx, cy)
while getFreeSpace(CURRENCY_BIN) < 1 do
g.appendAndDrawConsole(term, console, "Waiting for free space...", cx, cy)
os.sleep(3)
end
end
local amountToTransfer = math.min(freeSpace, amount - withdrawn)
local success, actualTransfer = transferCurrency(CURRENCY_SOURCE, CURRENCY_BIN, amountToTransfer)
withdrawn = withdrawn + actualTransfer
if not success then
-- Failure! Send the money back, if we can.
g.appendAndDrawConsole(term, console, "Transfer failed! Please contact an administrator to report the issue.", cx, cy)
os.sleep(3)
tryReclaimFundsInError(withdrawn)
return false
else
g.appendAndDrawConsole(term, console, "Transferred " .. tostring(actualTransfer) .. " $HMK.", cx, cy)
os.sleep(1)
end
end
local tx, errorMsg = bankClient.recordTransaction(account.id, amount * -1, "ATM withdrawal")
if not tx then
g.appendAndDrawConsole(term, console, "Failed to post transaction: " .. errorMsg, cx, cy)
tryReclaimFundsInError(amount)
os.sleep(3)
return false
end
g.appendAndDrawConsole(term, console, "Transaction posted to account.", cx, cy)
os.sleep(2)
return true
elseif g.isButtonPressed(x, y, cancelButtonCoords) then
return false
end
end
end
end
local function showAccountUI(account)
while true do
drawFrame()
@ -165,18 +413,37 @@ local function showAccountUI(account)
g.drawText(term, 2, 7, "Name", colors.gray, colors.white)
g.drawText(term, 2, 8, account.name, colors.black, colors.white)
g.drawText(term, 2, 10, "Balance ($HMK)", colors.gray, colors.white)
g.drawText(term, 2, 11, tostring(account.balance), colors.yellow, colors.white)
g.drawText(term, 2, 11, tostring(account.balance), colors.orange, colors.white)
local buttons = {}
buttons.deposit = g.drawButton(term, 35, 4, 17, 3, "Deposit", colors.white, colors.green)
if account.balance > 0 then
buttons.withdraw = g.drawButton(term, 35, 8, 17, 3, "Withdraw", colors.white, colors.purple)
buttons.transfer = g.drawButton(term, 35, 12, 17, 3, "Transfer", colors.white, colors.orange)
else
buttons.close = g.drawButton(term, 35, 16, 17, 3, "Close Account", colors.white, colors.red)
end
local event, button, x, y = os.pullEvent("mouse_click")
if button == 1 then
if y == 2 and x >= W-3 then
return -- exit back to the accounts UI
elseif g.isButtonPressed(x, y, buttons.deposit) then
local success = showDepositUI(account)
if success then return end -- If successful, go back to the accounts page.
elseif buttons.withdraw and g.isButtonPressed(x, y, buttons.withdraw) then
local success = showWithdrawUI(account)
if success then return end
elseif buttons.transfer and g.isButtonPressed(x, y, buttons.transfer) then
-- Do Transfer
return
elseif buttons.close and g.isButtonPressed(x, y, buttons.close) then
end
end
end
end
local function showAccountsUI()
local accounts, errorMsg = bankClient.getAccounts()
while true do
drawFrame()
g.drawXLine(term, 1, 19, 2, colors.gray)
@ -184,30 +451,60 @@ local function showAccountsUI()
g.drawXLine(term, 10, 35, 2, colors.lightGray)
g.drawText(term, 11, 2, "Name", colors.white, colors.lightGray)
g.drawXLine(term, 36, W, 2, colors.gray)
g.drawText(term, 37, 2, "Balance ($HMK)", colors.white, colors.gray)
g.drawText(term, 37, 2, "Balance", colors.white, colors.gray)
g.drawText(term, W-6, 2, "Log Out", colors.white, colors.red)
local accounts, errorMsg = bankClient.getAccounts()
if accounts then
for i, account in pairs(accounts) do
local bg = colors.blue
if i % 2 == 0 then bg = colors.lightBlue end
local fg = colors.white
local y = i + 2
g.drawXLine(term, 1, W, y, bg)
g.drawText(term, 2, y, "*" .. string.sub(account.id, -5, -1), fg, bg)
g.drawText(term, 2, y, shortId(account), fg, bg)
g.drawText(term, 11, y, account.name, fg, bg)
g.drawText(term, 37, y, tostring(account.balance), fg, bg)
end
else
g.drawTextCenter(term, W/2, 4, "Error: " .. errorMsg, colors.red, colors.white)
end
local event, button, x, y = os.pullEvent("mouse_click")
if button == 1 and y > 2 and (y - 2) <= #accounts then
local account = accounts[y-2]
showAccountUI(account)
if button == 1 then
if accounts and y > 2 and (y - 2) <= #accounts then
showAccountUI(accounts[y-2])
elseif y == 2 and x >= W-6 then
bankClient.logOut()
return
end
end
end
end
local function logoutAfterInactivity()
local function now() return os.epoch("utc") end
local DELAY = 30000
local lastActivity = now()
while now() < lastActivity + DELAY do
parallel.waitForAny(
function () os.sleep(1) end,
function ()
local event = os.pullEvent()
if event == "mouse_click" or event == "key" or event == "key_up" or event == "char" then
lastActivity = now()
end
end
)
end
bankClient.logOut()
drawFrame()
g.drawText(term, 2, 3, "Logged out due to inactivity.", colors.gray, colors.white)
os.sleep(2)
end
while true do
local credentials = showLoginUI()
local loginSuccess = checkCredentialsUI(credentials)
if loginSuccess then
showAccountsUI()
parallel.waitForAny(showAccountsUI, logoutAfterInactivity)
end
return
end

View File

@ -96,6 +96,14 @@ function client.getAccounts()
return response.data
end
function client.getAccount(accountId)
local response = requestAuth("GET_ACCOUNT", {accountId = accountId})
if not response.success then
return nil, response.error
end
return response.data
end
function client.createAccount(accountName)
local response = requestAuth("CREATE_ACCOUNT", {name = accountName})
if not response.success then

View File

@ -309,6 +309,18 @@ local function handleGetUserAccounts(msg)
return {success = true, data = getAccounts(msg.auth.username)}
end
local function handleGetUserAccount(msg)
if not msg.data or not msg.data.accountId then
return {success = false, error = "Invalid request. Requires data.accountId."}
end
local accounts = getAccounts(msg.auth.username)
local account = findAccountById(accounts, msg.data.accountId)
if not account then
return {success = false, error = "Account doesn't exist."}
end
return {success = true, data = account}
end
local function handleCreateUserAccount(msg)
if not msg.data or not msg.data.name then
return {success = false, error = "Invalid request. Requires data.name."}
@ -354,6 +366,7 @@ local BANK_REQUESTS = {
["DELETE_USER"] = authProtect(handleDeleteUser),
["RENAME_USER"] = authProtect(handleRenameUser),
["GET_ACCOUNTS"] = authProtect(handleGetUserAccounts),
["GET_ACCOUNT"] = authProtect(handleGetUserAccount),
["CREATE_ACCOUNT"] = authProtect(handleCreateUserAccount),
["DELETE_ACCOUNT"] = authProtect(handleDeleteUserAccount),
["RENAME_ACCOUNT"] = authProtect(handleRenameUserAccount),