diff --git a/config.lua b/config.lua index 2c1c11b..e03ad86 100644 --- a/config.lua +++ b/config.lua @@ -85,7 +85,7 @@ peripherals = { monitor = nil, exchangeChest = nil, - outputChest = nil, + outputChest = "minecraft:chest_3", }, exchange = { enabled = true, diff --git a/core/ConfigValidator.lua b/core/ConfigValidator.lua index 1538349..e793c1a 100644 --- a/core/ConfigValidator.lua +++ b/core/ConfigValidator.lua @@ -75,8 +75,8 @@ }, peripherals = { monitor = "string?", - exchangeChest = "string?", - outputChest = "string?", + exchangeChest = "networked_chest?", + outputChest = "networked_chest", }, exchange = { enabled = "boolean", @@ -167,6 +167,28 @@ error("Config value " .. subpath .. " must be a color") end end + if v == "networked_chest" then + if type(config[k]) ~= "string" then + error("Config value " .. subpath .. " must be a networked chest") + end + if config[k] == "left" or config[k] == "right" or config[k] == "front" or config[k] == "back" or config[k] == "top" or config[k] == "bottom" then + error("Config value " .. subpath .. " must not be a relative position") + end + local chestMethods = peripheral.getMethods(config[k]) + if not config[k] then + error("Config value " .. subpath .. " must refer to a valid peripheral") + end + local hasDropMethod = false + for i = 1, #chestMethods do + if chestMethods[i] == "drop" then + hasDropMethod = true + break + end + end + if not hasDropMethod then + error("Config value " .. subpath .. " must refer to a peripheral with an inventory") + end + end if v == "boolean" and type(config[k]) ~= "boolean" then error("Config value " .. subpath .. " must be a boolean") end diff --git a/core/Pricing.lua b/core/Pricing.lua new file mode 100644 index 0000000..924c473 --- /dev/null +++ b/core/Pricing.lua @@ -0,0 +1,17 @@ +local function getProductPrice(product, currency) + local price = product.price / currency.value + if product.priceOverrides then + for i = 1, #product.priceOverrides do + local override = product.priceOverrides[i] + if override.currency == currency.id then + price = override.price + break + end + end + end + return price +end + +return { + getProductPrice = getProductPrice +} \ No newline at end of file diff --git a/core/ShopState.lua b/core/ShopState.lua index 4333624..080f1b2 100644 --- a/core/ShopState.lua +++ b/core/ShopState.lua @@ -1,5 +1,6 @@ local Krypton = require("Krypton") local ScanInventory = require("core.inventory.ScanInventory") +local Pricing = require("core.Pricing") ---@class ShopState ---@field running boolean @@ -25,6 +26,52 @@ 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) @@ -40,19 +87,77 @@ node = "https://tenebra.lil.gay/" end currency.krypton = Krypton.new({ - privateKey = currency.privateKey, + privateKey = currency.pkey, node = node, id = currency.id, }) table.insert(state.currencies, currency) local kryptonWs = currency.krypton:connect() - kryptonWs:subscribe("transactions") + kryptonWs:subscribe("ownTransactions") + kryptonWs:getSelf() table.insert(kryptonListeners, function() kryptonWs:listen() end) end - parallel.waitForAny(unpack(kryptonListeners), function() + parallel.waitForAny(function() while true do - local event, transaction = os.pullEvent("transaction") - --print("Received transaction on " .. transaction.source) + 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 @@ -73,7 +178,7 @@ end sleep(math.min(1, state.config.settings.categoryCycleFrequency)) end - end) + end, unpack(kryptonListeners)) end return { diff --git a/core/inventory/ScanInventory.lua b/core/inventory/ScanInventory.lua index 2d2abbc..365f326 100644 --- a/core/inventory/ScanInventory.lua +++ b/core/inventory/ScanInventory.lua @@ -123,8 +123,8 @@ for i = 1, #items do local item = items[i] local matchingProducts = findMatchingProducts(products, item) - for i = 1, #matchingProducts do - local product = matchingProducts[i] + for j = 1, #matchingProducts do + local product = matchingProducts[j] product.newQty = product.newQty + item.count end end @@ -139,7 +139,55 @@ return itemCache end +local function findProductItemsFrom(product, quantity, items, cached) + local sources = {} + local remaining = quantity + for i = 1, #items do + local item = items[i] + local inventory = item.inventory + local slot = item.slot + if cached or item.name == product.modid then + item = peripheral.call(inventory, "getItemMeta", slot) + if item then + if item.name ~= product.modid or (product.predicates and not partialObjectMatches(product.predicates, item)) then + item = nil + else + item.inventory = inventory + item.slot = slot + end + end + if item and item.count > 0 then + local amount = math.min(item.count, remaining) + table.insert(sources, { + inventory = item.inventory, + slot = item.slot, + amount = amount + }) + remaining = remaining - amount + end + end + if remaining <= 0 then + break + end + end + return sources, quantity - remaining +end + +local function findProductItems(products, product, quantity) + local sources = nil + local amount = 0 + local items = getItemCache() + sources, amount = findProductItemsFrom(product, quantity, items, true) + if amount == 0 then + updateProductInventory(products) + items = getItemCache() + sources, amount = findProductItemsFrom(product, quantity, items) + end + return sources, amount +end + return { updateProductInventory = updateProductInventory, - getItemCache = getItemCache + getItemCache = getItemCache, + findProductItems = findProductItems } \ No newline at end of file diff --git a/radon.lua b/radon.lua index 199c27b..2c6929a 100644 --- a/radon.lua +++ b/radon.lua @@ -17,6 +17,7 @@ local Rect = require("components.Rect") local RenderCanvas = require("components.RenderCanvas") local Core = require("core.ShopState") +local Pricing = require("core.Pricing") local ShopRunner = require("core.ShopRunner") local ConfigValidator = require("core.ConfigValidator") @@ -43,20 +44,6 @@ return displayedProducts end -local function getProductPrice(product, currency) - local price = product.price / currency.value - if product.priceOverrides then - for i = 1, #product.priceOverrides do - local override = product.priceOverrides[i] - if override.currency == currency.id then - price = override.price - break - end - end - end - return price -end - local function getCurrencySymbol(currency, productTextSize) local currencySymbol if currency.krypton and currency.krypton.currency then @@ -149,7 +136,7 @@ for i = 1, #shopProducts do local product = shopProducts[i] product.quantity = product.quantity or 0 - local productPrice = getProductPrice(product, props.shopState.selectedCurrency) + local productPrice = Pricing.getProductPrice(product, props.shopState.selectedCurrency) if productTextSize == "large" then maxAddrWidth = math.max(maxAddrWidth, bigFont:getWidth(product.address .. "@")+2) maxQtyWidth = math.max(maxQtyWidth, bigFont:getWidth(tostring(product.quantity))+4) @@ -169,7 +156,7 @@ -- Display products in format: --
product.quantity = product.quantity or 0 - local productPrice = getProductPrice(product, props.shopState.selectedCurrency) + local productPrice = Pricing.getProductPrice(product, props.shopState.selectedCurrency) local qtyColor = theme.colors.normalQtyColor if product.quantity == 0 then qtyColor = theme.colors.outOfStockQtyColor