mirror of
https://github.com/AR2000AR/openComputers_codes.git
synced 2025-09-08 06:31:14 +02:00
471 lines
19 KiB
Lua
471 lines
19 KiB
Lua
local gui = require("libGUI")
|
|
local filesystem = require("filesystem")
|
|
local io = require("io")
|
|
local serialization = require("serialization")
|
|
local text = require("text")
|
|
local event = require("event")
|
|
local term = require("term")
|
|
local sleep = require("os").sleep
|
|
local component = require("component")
|
|
local transposer = component.transposer
|
|
local computer = require("computer")
|
|
local libCoin = {} --imported later if required by config
|
|
local bank = {} --imported later if required by config
|
|
local libCB = {} --imported later if required by config
|
|
|
|
--make sure we have the required hardware
|
|
assert(transposer, "No transposer found")
|
|
|
|
--constants
|
|
local CONFIG_PATH = "/etc/vending/"
|
|
local CONFIG_FILE = CONFIG_PATH .. "config.cfg"
|
|
local PRODUCT_LIST_FILE = CONFIG_PATH .. "products.csv"
|
|
local OUT_PATH = "/var/vending/"
|
|
local SALE_STATS = OUT_PATH .. "sales.csv"
|
|
|
|
local COLUMN_SOLD_ITEM = 1
|
|
local COLUMN_SOLD_QTE = 2
|
|
local COLUMN_COST_ITEM = 3
|
|
local COLUMN_COST_QTE = 4
|
|
local COLUMN_COST_COIN = 5
|
|
|
|
--global vars
|
|
local config = {}
|
|
local products = {}
|
|
local availableProduct = {}
|
|
|
|
--functions definition
|
|
local function parseCSVLine(line, sep)
|
|
local endReached = false
|
|
local res = {}
|
|
while (not endReached) do
|
|
local i = line:find(sep)
|
|
if (not i) then
|
|
endReached = true
|
|
i = #line + 1
|
|
end
|
|
table.insert(res, line:sub(1, i - 1))
|
|
line = line:sub(i + 1)
|
|
end
|
|
return (res)
|
|
end
|
|
|
|
local function reloadLibs()
|
|
if (config.acceptCoin) then libCoin = require("libCoin") else libCoin = nil end
|
|
if (config.acceptCB) then
|
|
bank = require("bank_api");
|
|
libCB = require("libCB")
|
|
else
|
|
bank = nil
|
|
libCB = nil
|
|
end
|
|
end
|
|
|
|
local function saveConfig()
|
|
--save the config table to a file
|
|
local cFile = io.open(CONFIG_FILE, "w")
|
|
assert(cFile, string.format("Could not open : %s", CONFIG_FILE))
|
|
cFile:write(serialization.serialize(config))
|
|
cFile:close()
|
|
end
|
|
|
|
local function itemFromString(itemString)
|
|
local name, damage = itemString:match("([^:]+:[^:]+):?([0-9]*)")
|
|
if (damage == "") then damage = "0" end
|
|
damage = tonumber(damage)
|
|
return {
|
|
name = name,
|
|
damage = damage,
|
|
label = name:match(":([^:]+)")
|
|
}
|
|
end
|
|
|
|
local function itemEquals(itemA, itemB)
|
|
if (type(itemA) == "string") then itemA = itemFromString(itemA) end
|
|
if (type(itemB) == "string") then itemB = itemFromString(itemB) end
|
|
return itemA.name == itemB.name and itemA.damage == itemB.damage
|
|
end
|
|
|
|
local function payInCoin(amount)
|
|
local paid = false
|
|
--check allowed payment method
|
|
if (config.acceptCoin) then
|
|
paid = libCoin.moveCoin(amount, config.chestFront, config.chestBack)
|
|
end
|
|
if (paid) then return paid end
|
|
if (config.acceptCB) then
|
|
local readerComponent = nil
|
|
if (not config.forceDriveEvent and component.isAvailable("drive")) then
|
|
readerComponent = component.drive
|
|
else
|
|
print("Insert CB")
|
|
readerComponent = libCB.waitForCB(config.cbTimeout)
|
|
end
|
|
local encryptedCardData = libCB.loadCB(readerComponent, config.cbTimeout)
|
|
local try = 0
|
|
local cb = false
|
|
if (encryptedCardData) then
|
|
repeat
|
|
io.write("PIN :")
|
|
local pin = term.read(nil, false, nil, "*")
|
|
if (not pin) then print(""); return false end
|
|
pin = pin:gsub("\n", "") --remove newline cause by term.read
|
|
cb = libCB.getCB(encryptedCardData, pin)
|
|
try = try + 1
|
|
print("") --clean new line
|
|
until (cb or try >= 3)
|
|
local res = bank.makeTransaction(config.accountUUID, cb, amount)
|
|
paid = res == 0
|
|
if (not paid) then
|
|
print(({
|
|
[1] = "NO ACCOUNT",
|
|
[2] = "ERROR ACCOUNT",
|
|
[3] = "ERROR CB",
|
|
[4] = "ERROR AMOUNT",
|
|
[5] = "ERROR_RECEIVING_ACCOUNT",
|
|
[-1] = "TIMEOUT",
|
|
[-2] = "WRONG MESSAGE"
|
|
})[res])
|
|
end
|
|
else
|
|
print("NO CB")
|
|
end
|
|
end
|
|
return paid
|
|
end
|
|
|
|
local function checkItemAvailability(item, amount, side)
|
|
if (not side) then side = config.chestBack end
|
|
assert(item, "No item provided")
|
|
assert(amount and type(amount) == "number", "No or invalid amount")
|
|
assert(not side or type(side) == "number", "Invalid side")
|
|
local foundAmmount = 0
|
|
for chestItem in transposer.getAllStacks(side) do
|
|
if (chestItem.name and itemEquals(item, chestItem)) then
|
|
foundAmmount = foundAmmount + chestItem.size
|
|
end
|
|
end
|
|
return foundAmmount >= amount
|
|
end
|
|
|
|
local function getFreeSpace(side, item)
|
|
checkArg(1, side, "number")
|
|
checkArg(2, item, "nil", "table")
|
|
local stackSize = 64
|
|
local emptyStack = 0
|
|
local freeSpace = 0
|
|
if (item and item.maxSize) then stackSize = item.maxSize end
|
|
for chestItem in transposer.getAllStacks(side) do
|
|
if (not chestItem.name) then
|
|
emptyStack = emptyStack + 1
|
|
elseif (item and itemEquals(chestItem, item)) then
|
|
stackSize = chestItem.maxSize
|
|
freeSpace = freeSpace + chestItem.maxSize - chestItem.size
|
|
end
|
|
end
|
|
return freeSpace + emptyStack * stackSize
|
|
end
|
|
|
|
local function moveItem(source, sink, item, amount)
|
|
assert(type(source) == "number", "Invalid source side")
|
|
assert(type(sink) == "number", "Invalid sink side")
|
|
assert(item, "No item provided")
|
|
amount = amount or 1
|
|
assert(type(amount) == "number", "number expected")
|
|
local slot = 0
|
|
local request = amount
|
|
for chestItem in transposer.getAllStacks(source) do
|
|
slot = slot + 1
|
|
if (chestItem.name and itemEquals(item, chestItem)) then
|
|
amount = amount - transposer.transferItem(source, sink, amount, slot)
|
|
end
|
|
if (amount <= 0) then break end
|
|
end
|
|
return request - amount
|
|
end
|
|
|
|
local function payInItem(item, amount)
|
|
if (checkItemAvailability(item, amount, config.chestFront)) then
|
|
return moveItem(config.chestFront, config.chestBack, item, amount) == amount
|
|
end
|
|
end
|
|
|
|
local function logSale(item, itemQte, unitCost, qte)
|
|
if (not config.logSales) then return end
|
|
checkArg(1, item, "table")
|
|
checkArg(2, itemQte, "number")
|
|
checkArg(3, unitCost, "number", "string")
|
|
checkArg(4, qte, "number")
|
|
assert(item.name, "Invalid item provided")
|
|
local sFile = io.open(SALE_STATS, "a")
|
|
assert(sFile, "Something when terribally wrong with " .. SALE_STATS)
|
|
sFile:write(string.format("%s,%i,%s,%i\n", item.name, itemQte, unitCost, qte))
|
|
sFile:close()
|
|
end
|
|
|
|
local function getSalesCoinTotal()
|
|
local sFile = io.open(SALE_STATS, "r")
|
|
if (not sFile) then return 0 end
|
|
local total = 0
|
|
for line in sFile:lines() do
|
|
local data = parseCSVLine(line, ",")
|
|
if (tonumber(data[3])) then
|
|
total = total + tonumber(data[3]) * tonumber(data[4])
|
|
end
|
|
end
|
|
sFile:close()
|
|
return total
|
|
end
|
|
|
|
--load configurations
|
|
if (not filesystem.isDirectory(CONFIG_PATH)) then
|
|
filesystem.makeDirectory(CONFIG_PATH)
|
|
end
|
|
config = {
|
|
acceptCoin = true,
|
|
acceptCB = false,
|
|
chestFront = 3,
|
|
chestBack = 2,
|
|
accountUUID = "",
|
|
exitString = "exit",
|
|
adminPlayer = "",
|
|
logSales = true,
|
|
forceDriveEvent = true,
|
|
cbTimeout = 30
|
|
}
|
|
if (filesystem.exists(CONFIG_FILE) and not filesystem.isDirectory(CONFIG_FILE)) then
|
|
local cFile = io.open(CONFIG_FILE, "r")
|
|
assert(cFile, "Something went wrong when reading the config file")
|
|
local tconf = serialization.unserialize(cFile:read("*a")) or {}
|
|
cFile:close()
|
|
for key, val in pairs(tconf) do
|
|
config[key] = val
|
|
end
|
|
cFile:close()
|
|
end
|
|
saveConfig()
|
|
reloadLibs()
|
|
--load product list
|
|
if (filesystem.exists(PRODUCT_LIST_FILE) and not filesystem.isDirectory(PRODUCT_LIST_FILE)) then
|
|
local pFile = io.open(PRODUCT_LIST_FILE, "r")
|
|
assert(pFile, "Something went wrong when reading the product list file")
|
|
local header = false --was the header read ?
|
|
local lineNb = 0
|
|
for line in pFile:lines() do
|
|
lineNb = lineNb + 1
|
|
line = string.gsub(line, " ", "")
|
|
if (header and line ~= "" and line ~= "\n") then
|
|
local productInfo = parseCSVLine(line, ",")
|
|
if (productInfo[1] ~= "") then productInfo[1] = itemFromString(productInfo[1]) else productInfo[1] = false end
|
|
if (productInfo[3] ~= "") then productInfo[3] = itemFromString(productInfo[3]) else productInfo[3] = false end
|
|
productInfo[2] = tonumber(productInfo[2]) or 1
|
|
productInfo[4] = tonumber(productInfo[4]) or false
|
|
productInfo[5] = tonumber(productInfo[5]) or false
|
|
if (productInfo[5] == 0) then productInfo[5] = false end
|
|
assert(productInfo[2], string.format("%s:%i:invalid product data %q\nNo sold item qte", PRODUCT_LIST_FILE, lineNb, line))
|
|
assert(productInfo[4] or productInfo[5], string.format("%s:%i:invalid product data %q\nNo price qte", PRODUCT_LIST_FILE, lineNb, line))
|
|
assert(#productInfo == 5, string.format("%s:%i:invalid product data %q\nNot enough columns", PRODUCT_LIST_FILE, lineNb, line))
|
|
table.insert(products, productInfo)
|
|
table.insert(availableProduct, false)
|
|
end
|
|
header = true --use to skip the header on the first line
|
|
end
|
|
pFile:close()
|
|
end
|
|
--load items label
|
|
for item in transposer.getAllStacks(config.chestBack) do
|
|
if (item.name) then
|
|
for i, product in ipairs(products) do
|
|
if (product[1] and itemEquals(product[1], item)) then
|
|
product[1].label = item.label
|
|
end
|
|
if (product[3] and itemEquals(product[3], item)) then
|
|
product[3].label = item.label
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if (not filesystem.isDirectory(OUT_PATH)) then
|
|
filesystem.makeDirectory(OUT_PATH)
|
|
end
|
|
|
|
local run = true
|
|
local event_interrupted = event.listen("interrupted", function() end)
|
|
|
|
--main
|
|
while (run) do
|
|
local op = nil
|
|
--user interface loop (draw/input)
|
|
repeat -- sales menu
|
|
--draw the products list
|
|
term.clear()
|
|
for i, product in ipairs(products) do
|
|
local cost = product[COLUMN_COST_COIN]
|
|
if (not product[COLUMN_COST_COIN] or product[COLUMN_COST_COIN] == 0) then
|
|
cost = string.format("%qx%s", product[COLUMN_COST_ITEM].label, product[COLUMN_COST_QTE])
|
|
end
|
|
if (checkItemAvailability(product[COLUMN_SOLD_ITEM], product[COLUMN_SOLD_QTE])) then --filter out unavailable products
|
|
print(string.format("%i : %qx%s (%s)", i, product[COLUMN_SOLD_ITEM].label, product[COLUMN_SOLD_QTE], cost))
|
|
end
|
|
end
|
|
--read user input
|
|
io.write(string.format("1-%i[*qte] >", #products))
|
|
op = io.read("l")
|
|
until (op)
|
|
|
|
if (op == "admin") then --admin
|
|
--admin Menu
|
|
local auth = false
|
|
if (config.adminPlayer ~= "") then
|
|
term.clear()
|
|
print("Press any key")
|
|
local _, _, _, _, player = event.pull(10, "key_down")
|
|
if (player and player == config.adminPlayer) then auth = true end
|
|
else auth = true end
|
|
while (auth) do
|
|
--print the interface
|
|
term.clear()
|
|
if (config.logSales) then print(string.format("Coin(s) earned : %i", getSalesCoinTotal())) end
|
|
if (config.acceptCoin) then print(string.format("Coins in back chest : (%i) %i %i %i %i", libCoin.getValue(libCoin.getCoin(config.chestBack)), libCoin.getCoin(config.chestBack))) end
|
|
print("1 : Payment settings")
|
|
print("2 : Unload to front")
|
|
print("3 : Load from front")
|
|
if (config.acceptCoin) then
|
|
print("4 : Unload coins to front")
|
|
print("5 : Load coins from front")
|
|
end
|
|
print("6 : Clear sale data")
|
|
print("7 : Return")
|
|
print(string.format("%q : Exit the program", config.exitString))
|
|
--read user input
|
|
io.write("[1-7] >")
|
|
op = io.read("l")
|
|
if (op == config.exitString) then --exit
|
|
run = false
|
|
auth = false
|
|
elseif (op == nil) then
|
|
auth = false --exit admin mode
|
|
end
|
|
op = tonumber(op)
|
|
|
|
--switch
|
|
if (op == 1) then --Payment settings
|
|
local subMenu = true
|
|
repeat
|
|
--interface
|
|
term.clear()
|
|
print(string.format("1 : Accept coin (%s)", config.acceptCoin))
|
|
print(string.format("2 : Accept card (%s)", config.acceptCB))
|
|
if (config.acceptCB) then
|
|
print(string.format("3 : Register new owner account (%.9s****-****-****-************)", config.accountUUID))
|
|
print(string.format("4 : Set card read timeout (%d)", config.cbTimeout))
|
|
end
|
|
print("5 : back")
|
|
io.write("[1-5] >")
|
|
op = io.read("l")
|
|
if (op == nil) then subMenu = false end --exit admin mode
|
|
op = tonumber(op)
|
|
|
|
--switch
|
|
if (op == 1) then --toggle acceptCoin
|
|
config.acceptCoin = not config.acceptCoin
|
|
reloadLibs()
|
|
saveConfig()
|
|
elseif (op == 2) then --toggle acceptCB
|
|
config.acceptCB = not config.acceptCB
|
|
reloadLibs()
|
|
saveConfig()
|
|
elseif (config.acceptCB and op == 3) then --register owner bank account
|
|
term.clear()
|
|
print("Insert or swipe card")
|
|
local readerComponent = libCB.waitForCB(config.cbTimeout) --read card
|
|
if (readerComponent) then --card swipped
|
|
local encryptedCardData = libCB.loadCB(readerComponent) --load card data to decrypt later
|
|
local cb = nil
|
|
local try = 0
|
|
repeat --ask for pin (3 erros max)
|
|
io.write("PIN :")
|
|
local pin = term.read(nil, false, nil, "*")
|
|
if (not pin) then print(""); return false end
|
|
pin = pin:gsub("\n", "") --remove newline cause by term.read
|
|
cb = libCB.getCB(encryptedCardData, pin)
|
|
try = try + 1
|
|
print("") --clean new line
|
|
until (cb or try >= 3)
|
|
if (cb) then --cb = false if pin is wrong
|
|
config.accountUUID = cb.uuid
|
|
saveConfig()
|
|
end
|
|
end
|
|
elseif (config.acceptCB and op == 4) then --card read timout
|
|
io.write("timout (s) >")
|
|
op = io.read("l")
|
|
op = tonumber(op)
|
|
if (op) then
|
|
config.cbTimeout = op
|
|
saveConfig()
|
|
end
|
|
elseif (op == 5) then subMenu = false end --exit the submenu
|
|
until (not subMenu)
|
|
elseif (op == 2) then --Back chest unloading
|
|
print("Unloading")
|
|
while (transposer.transferItem(config.chestBack, config.chestFront) ~= 0) do end
|
|
elseif (op == 3) then --Back chest loading
|
|
print("Loading")
|
|
while (transposer.transferItem(config.chestFront, config.chestBack) ~= 0) do end
|
|
elseif (config.acceptCoin and op == 4) then --Unload coins
|
|
libCoin.moveCoin(libCoin.getValue(libCoin.getCoin(config.chestBack)), config.chestBack, config.chestFront)
|
|
elseif (config.acceptCoin and op == 5) then --Load coins
|
|
libCoin.moveCoin(libCoin.getValue(libCoin.getCoin(config.chestFront)), config.chestFront, config.chestBack)
|
|
elseif (config.acceptCoin and op == 6) then --Reset sales info
|
|
if (filesystem.exists(SALE_STATS)) then filesystem.remove(SALE_STATS) end
|
|
elseif (op == 7) then --exit admin mode
|
|
auth = false
|
|
end
|
|
end
|
|
else --sale
|
|
local qte = 1
|
|
op, qte = op:match("([0-9]+)[x\\*]?([0-9]*)")
|
|
op = tonumber(op)
|
|
qte = tonumber(qte) or 1
|
|
if (op and op >= 1 and op <= #products) then
|
|
if (checkItemAvailability(products[op][COLUMN_SOLD_ITEM], products[op][COLUMN_SOLD_QTE] * qte)) then
|
|
--check if there is enough space to give the bought item
|
|
if (getFreeSpace(config.chestFront, products[op][COLUMN_SOLD_ITEM]) >= products[op][COLUMN_SOLD_QTE] * qte) then
|
|
if (products[op][COLUMN_COST_COIN]) then
|
|
if (payInCoin(products[op][COLUMN_COST_COIN] * qte)) then
|
|
moveItem(config.chestBack, config.chestFront, products[op][COLUMN_SOLD_ITEM], products[op][COLUMN_SOLD_QTE] * qte)
|
|
logSale(products[op][COLUMN_SOLD_ITEM], products[op][COLUMN_SOLD_QTE], products[op][COLUMN_COST_COIN], qte)
|
|
print(string.format("Sold %i %s for %i coin(s)", products[op][COLUMN_SOLD_QTE] * qte, products[op][COLUMN_SOLD_ITEM].label, products[op][COLUMN_COST_COIN] * qte))
|
|
computer.beep()
|
|
else
|
|
print("No payment")
|
|
end
|
|
else
|
|
if (getFreeSpace(config.chestBack, products[op][COLUMN_COST_ITEM]) >= products[op][COLUMN_COST_QTE] * qte) then
|
|
if (payInItem(products[op][COLUMN_COST_ITEM], products[op][COLUMN_COST_QTE] * qte)) then
|
|
moveItem(config.chestBack, config.chestFront, products[op][COLUMN_SOLD_ITEM], products[op][COLUMN_SOLD_QTE] * qte)
|
|
logSale(products[op][COLUMN_SOLD_ITEM], products[op][COLUMN_SOLD_QTE], string.format("%s*%i", products[op][COLUMN_COST_ITEM].name, products[op][COLUMN_COST_QTE]), qte)
|
|
print(string.format("Sold %i %s for %i %s", products[op][COLUMN_SOLD_QTE] * qte, products[op][COLUMN_SOLD_ITEM].label, products[op][COLUMN_COST_QTE] * qte, products[op][COLUMN_COST_ITEM].label))
|
|
computer.beep()
|
|
else
|
|
print("Not enough to pay")
|
|
end
|
|
else
|
|
print("Could not accept payment")
|
|
end
|
|
end
|
|
else
|
|
print("Not enough free space in the chest")
|
|
end
|
|
else
|
|
print("UNAVAILABLE")
|
|
end
|
|
print("Press the Return key to continue")
|
|
term.read(nil, true, nil, nil)
|
|
end
|
|
end
|
|
end
|
|
|
|
event.cancel(event_interrupted)
|