From 8926537bfc2a588e7bdcd3bd8e589f1a721228d0 Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Sun, 17 Sep 2023 10:14:56 -0400 Subject: [PATCH] Improved GUI --- atm.lua | 331 +++++++++++++++++++++++++++++++++++++++++++++--- bank-client.lua | 8 ++ bank.lua | 13 ++ 3 files changed, 335 insertions(+), 17 deletions(-) diff --git a/atm.lua b/atm.lua index 8ed0cae..1339814 100644 --- a/atm.lua +++ b/atm.lua @@ -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) - 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, 11, y, account.name, fg, bg) - g.drawText(term, 37, y, tostring(account.balance), fg, bg) + 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, 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 \ No newline at end of file diff --git a/bank-client.lua b/bank-client.lua index 8cefab7..2b9fc84 100644 --- a/bank-client.lua +++ b/bank-client.lua @@ -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 diff --git a/bank.lua b/bank.lua index 0d49b65..3e813eb 100644 --- a/bank.lua +++ b/bank.lua @@ -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),