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

331
atm.lua
View File

@ -10,17 +10,93 @@ like recording transactions.
local g = require("simple-graphics") local g = require("simple-graphics")
local bankClient = require("bank-client") 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 BANK_HOST = "central-bank"
local SECURITY_KEY = "4514-1691-1660-7358-1884-0506-0878-7098-1511-3359-3602-3581-6910-0791-1843-5936" 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.") local modem = peripheral.find("modem") or error("No modem attached.")
bankClient.init(peripheral.getName(modem), BANK_HOST, SECURITY_KEY) 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 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() local function drawFrame()
g.clear(term, colors.white) g.clear(term, colors.white)
g.drawXLine(term, 1, W, 1, colors.black) g.drawXLine(term, 1, W, 1, colors.black)
g.drawText(term, 2, 1, "ATM", colors.white, 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 end
local function tryReadDiskCredentials(name) local function tryReadDiskCredentials(name)
@ -91,6 +167,8 @@ local function tryLoginViaInput()
end end
elseif keyCode == keys.tab and selectedInput == "username" then elseif keyCode == keys.tab and selectedInput == "username" then
selectedInput = "password" selectedInput = "password"
elseif keyCode == keys.enter and selectedInput == "password" then
return {username = username, password = password} -- Do login right away.
end end
elseif event == "mouse_click" then elseif event == "mouse_click" then
local button = p1 local button = p1
@ -153,6 +231,176 @@ local function checkCredentialsUI(credentials)
return true return true
end 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) local function showAccountUI(account)
while true do while true do
drawFrame() drawFrame()
@ -165,18 +413,37 @@ local function showAccountUI(account)
g.drawText(term, 2, 7, "Name", colors.gray, colors.white) 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, 8, account.name, colors.black, colors.white)
g.drawText(term, 2, 10, "Balance ($HMK)", colors.gray, 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") local event, button, x, y = os.pullEvent("mouse_click")
if button == 1 then if button == 1 then
if y == 2 and x >= W-3 then if y == 2 and x >= W-3 then
return -- exit back to the accounts UI 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
end end
end end
local function showAccountsUI() local function showAccountsUI()
local accounts, errorMsg = bankClient.getAccounts()
while true do while true do
drawFrame() drawFrame()
g.drawXLine(term, 1, 19, 2, colors.gray) g.drawXLine(term, 1, 19, 2, colors.gray)
@ -184,30 +451,60 @@ local function showAccountsUI()
g.drawXLine(term, 10, 35, 2, colors.lightGray) g.drawXLine(term, 10, 35, 2, colors.lightGray)
g.drawText(term, 11, 2, "Name", colors.white, colors.lightGray) g.drawText(term, 11, 2, "Name", colors.white, colors.lightGray)
g.drawXLine(term, 36, W, 2, colors.gray) 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)
for i, account in pairs(accounts) do g.drawText(term, W-6, 2, "Log Out", colors.white, colors.red)
local bg = colors.blue local accounts, errorMsg = bankClient.getAccounts()
if i % 2 == 0 then bg = colors.lightBlue end if accounts then
local fg = colors.white for i, account in pairs(accounts) do
local y = i + 2 local bg = colors.blue
g.drawXLine(term, 1, W, y, bg) if i % 2 == 0 then bg = colors.lightBlue end
g.drawText(term, 2, y, "*" .. string.sub(account.id, -5, -1), fg, bg) local fg = colors.white
g.drawText(term, 11, y, account.name, fg, bg) local y = i + 2
g.drawText(term, 37, y, tostring(account.balance), fg, bg) g.drawXLine(term, 1, W, y, 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 end
local event, button, x, y = os.pullEvent("mouse_click") local event, button, x, y = os.pullEvent("mouse_click")
if button == 1 and y > 2 and (y - 2) <= #accounts then if button == 1 then
local account = accounts[y-2] if accounts and y > 2 and (y - 2) <= #accounts then
showAccountUI(account) showAccountUI(accounts[y-2])
elseif y == 2 and x >= W-6 then
bankClient.logOut()
return
end
end end
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 while true do
local credentials = showLoginUI() local credentials = showLoginUI()
local loginSuccess = checkCredentialsUI(credentials) local loginSuccess = checkCredentialsUI(credentials)
if loginSuccess then if loginSuccess then
showAccountsUI() parallel.waitForAny(showAccountsUI, logoutAfterInactivity)
end end
return
end end

View File

@ -96,6 +96,14 @@ function client.getAccounts()
return response.data return response.data
end 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) function client.createAccount(accountName)
local response = requestAuth("CREATE_ACCOUNT", {name = accountName}) local response = requestAuth("CREATE_ACCOUNT", {name = accountName})
if not response.success then if not response.success then

View File

@ -309,6 +309,18 @@ local function handleGetUserAccounts(msg)
return {success = true, data = getAccounts(msg.auth.username)} return {success = true, data = getAccounts(msg.auth.username)}
end 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) local function handleCreateUserAccount(msg)
if not msg.data or not msg.data.name then if not msg.data or not msg.data.name then
return {success = false, error = "Invalid request. Requires data.name."} return {success = false, error = "Invalid request. Requires data.name."}
@ -354,6 +366,7 @@ local BANK_REQUESTS = {
["DELETE_USER"] = authProtect(handleDeleteUser), ["DELETE_USER"] = authProtect(handleDeleteUser),
["RENAME_USER"] = authProtect(handleRenameUser), ["RENAME_USER"] = authProtect(handleRenameUser),
["GET_ACCOUNTS"] = authProtect(handleGetUserAccounts), ["GET_ACCOUNTS"] = authProtect(handleGetUserAccounts),
["GET_ACCOUNT"] = authProtect(handleGetUserAccount),
["CREATE_ACCOUNT"] = authProtect(handleCreateUserAccount), ["CREATE_ACCOUNT"] = authProtect(handleCreateUserAccount),
["DELETE_ACCOUNT"] = authProtect(handleDeleteUserAccount), ["DELETE_ACCOUNT"] = authProtect(handleDeleteUserAccount),
["RENAME_ACCOUNT"] = authProtect(handleRenameUserAccount), ["RENAME_ACCOUNT"] = authProtect(handleRenameUserAccount),