Newer
Older
Radon / core / ShopState.lua
@Allymonies Allymonies on 27 Dec 2022 8 KB Working shop functionality
local Krypton = require("Krypton")
local ScanInventory = require("core.inventory.ScanInventory")
local Pricing = require("core.Pricing")

---@class ShopState
---@field running boolean
local ShopState = {}
local ShopState_mt = { __index = ShopState }

function ShopState.new(config, products)
    local self = setmetatable({}, ShopState_mt)

    self.running = false
    self.config = config
    self.products = products
    self.selectedCurrency = config.currencies[1]
    self.selectedCategory = 1
    self.numCategories = 1
    self.productsChanged = false
    self.lastTouched = os.epoch("utc")

    return self
end

local function waitForAnimation(uid)
    coroutine.yield("animationFinished", uid)
end

local function parseMeta(transactionMeta)
    local meta = {}
    for metaEntry in transactionMeta:gmatch("([^;]+)") do
        if metaEntry:find("=") then
            local key, value = metaEntry:match("([^=]+)=([^=]+)")
            meta[key] = value
        else
            meta[metaEntry] = true
        end
    end
    return meta
end

local function validateReturnAddress(address)
    -- Primitive validation, will accept all valid addresses at the expense of some false positives
    if address:find("@") then
        local metaname, name = address:match("([^@]+)@([^@]+).%w+")
        if not metaname or not name then
            return false
        end
        if #name > 64 or #metaname > 32 then
            return false
        end
    else
        if #address > 64 then
            return false
        end
    end
    return true
end

local function refund(currency, address, meta, value, message, error)
    message = message or "Here is your refund!"
    local returnTo = address
    if meta and meta["return"] then
        if validateReturnAddress(meta["return"]) then
            returnTo = meta["return"]
        end
    end
    if not error then
        currency.krypton.ws:makeTransaction(returnTo, value, "message=" .. message)
    else
        currency.krypton.ws:makeTransaction(returnTo, value, "error=" .. message)
    end
end

-- Anytime the shop state is resumed, animation should be finished instantly. (call animation finish hooks)
---@param state ShopState
local function runShop(state)
    -- Shop is starting
    state.running = true
    state.currencies = {}
    local kryptonListeners = {}
    for _, currency in ipairs(state.config.currencies) do
        local node = currency.node
        if not node and currency.id == "krist" then
            node = "https://krist.dev/"
        elseif not node and currency.id == "tenebra" then
            node = "https://tenebra.lil.gay/"
        end
        currency.krypton = Krypton.new({
            privateKey = currency.pkey,
            node = node,
            id = currency.id,
        })
        table.insert(state.currencies, currency)
        local kryptonWs = currency.krypton:connect()
        kryptonWs:subscribe("ownTransactions")
        kryptonWs:getSelf()
        table.insert(kryptonListeners, function() kryptonWs:listen() end)
    end
    parallel.waitForAny(function()
        while true do
            local event, transactionEvent = os.pullEvent("transaction")
            local transactionCurrency = nil
            for _, currency in ipairs(state.currencies) do
                if currency.krypton.id == transactionEvent.source then
                    transactionCurrency = currency
                    break
                end
            end
            if transactionCurrency then
                local transaction = transactionEvent.transaction
                local sentName = transaction.sent_name
                local sentMetaname = transaction.sent_metaname
                local nameSuffix = transactionCurrency.krypton.currency.name_suffix
                if sentName and transactionCurrency.name:find(".") then
                    sentName = sentName .. "." .. nameSuffix
                end
                if sentName and sentName:lower() == transactionCurrency.name:lower() then
                    local meta = parseMeta(transaction.metadata)
                    if sentMetaname then
                        local purchasedProduct = nil
                        for _, product in ipairs(state.products) do
                            if product.address:lower() == sentMetaname:lower() then
                                purchasedProduct = product
                                break
                            end
                        end
                        if purchasedProduct then
                            local productPrice = Pricing.getProductPrice(purchasedProduct, transactionCurrency)
                            local amountPurchased = math.floor(transaction.value / productPrice)
                            if amountPurchased > 0 then
                                if purchasedProduct.quantity and purchasedProduct.quantity > 0 then
                                    local productSources, available = ScanInventory.findProductItems(state.products, purchasedProduct, amountPurchased)
                                    local refundAmount = transaction.value - (available * productPrice)
                                    print("Purchased " .. available .. " of " .. purchasedProduct.name .. " for " .. transaction.from .. " for " .. transaction.value .. " " .. transactionCurrency.name .. " (refund " .. refundAmount .. ")")
                                    if available > 0 then
                                        for _, productSource in ipairs(productSources) do
                                            peripheral.call(productSource.inventory, "pushItems", state.config.peripherals.outputChest, productSource.slot, productSource.amount, 1)
                                            peripheral.call(state.config.peripherals.outputChest, "drop", 1, productSource.amount, "up")
                                        end
                                        if refundAmount > 0 then
                                            refund(transactionCurrency, transaction.from, meta, refundAmount, "Here is the funds remaining after your purchase!")
                                        end
                                    else
                                        refund(transactionCurrency, transaction.from, meta, transaction.value, "Sorry, that item is out of stock!")
                                    end
                                else
                                    refund(transactionCurrency, transaction.from, meta, transaction.value, "Sorry, that item is out of stock!")
                                end
                            else
                                refund(transactionCurrency, transaction.from, meta, transaction.value, "You must purchase at least one of this product!", true)
                            end
                        else
                            refund(transactionCurrency, transaction.from, meta, transaction.value, "Must supply a valid product to purchase!", true)
                        end
                    else
                        refund(transactionCurrency, transaction.from, meta, transaction.value, "Must supply a product to purchase!", true)
                    end
                end
            end
        end
    end, function()
        while state.running do
            ScanInventory.updateProductInventory(state.products)
            if state.config.settings.hideUnavailableProducts then
                state.productsChanged = true
            end
            sleep(state.config.settings.pollFrequency)
        end
    end, function()
        while state.running do
            if state.config.settings.categoryCycleFrequency > 0 and os.epoch("utc") > state.lastTouched + (state.config.settings.activityTimeout * 1000) then
                state.selectedCategory = state.selectedCategory + 1
                if state.selectedCategory > state.numCategories then
                    state.selectedCategory = 1
                end
                state.productsChanged = true
            end
            sleep(math.min(1, state.config.settings.categoryCycleFrequency))
        end
    end, unpack(kryptonListeners))
end

return {
    ShopState = ShopState,
    runShop = runShop,
}