diff --git a/builder/.package b/builder/.package index be49c2c..e1140b2 100644 --- a/builder/.package +++ b/builder/.package @@ -4,6 +4,6 @@ }, title = 'Schematic Builder', repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/builder', - description = [[ Build structures from schematic files using a turtle or command computer. ]], + description = [[Build structures from schematic files using a turtle or command computer. ]], licence = 'MIT', } diff --git a/milo/.package b/milo/.package index 6946b44..d3f8b6e 100644 --- a/milo/.package +++ b/milo/.package @@ -5,7 +5,9 @@ title = 'Milo: Advanced inventory management', repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/milo', description = [[Provides AE style crafting in computercraft. - Includes Importing, exporting, autocrafting, replenish and limits, and much more. - Includes over 200 standard Minecraft recipes.]], + +Includes: importing, exporting, autocrafting, replenish, limits and much more. + +Includes over 200 standard Minecraft recipes.]], licence = 'MIT', } diff --git a/milo/apis/milo.lua b/milo/apis/milo.lua index d9470f2..7fba1eb 100644 --- a/milo/apis/milo.lua +++ b/milo/apis/milo.lua @@ -47,29 +47,19 @@ end function Milo:getState(key) if not self.state then - self.state = { } - Config.load('milo.state', self.state) + self.state = Config.load('milo.state') end return self.state[key] end function Milo:setState(key, value) if not self.state then - self.state = { } - Config.load('milo.state', self.state) + self.state = Config.load('milo.state') end self.state[key] = value Config.update('milo.state', self.state) end -function Milo:uniqueKey(item) - return table.concat({ item.name, item.damage, item.nbtHash }, ':') -end - -function Milo:splitKey(key) - return itemDB:splitKey(key) -end - function Milo:resetCraftingStatus() self.context.storage.activity = { } @@ -85,18 +75,8 @@ function Milo:registerTask(task) table.insert(self.context.tasks, task) end -function Milo:getItem(items, inItem, ignoreDamage, ignoreNbtHash) - if not ignoreDamage and not ignoreNbtHash then - return items[inItem.key or self:uniqueKey(inItem)] - end - - for _,item in pairs(items) do - if item.name == inItem.name and - (ignoreDamage or item.damage == inItem.damage) and - (ignoreNbtHash or item.nbtHash == inItem.nbtHash) then - return item - end - end +function Milo:getItem(inItem) + return self:listItems()[inItem.key or itemDB:makeKey(inItem)] end -- returns a list of items that matches along with a total count @@ -106,7 +86,7 @@ function Milo:getMatches(item, flags) local items = self:listItems() if not flags.ignoreDamage and not flags.ignoreNbtHash then - local key = item.key or Milo:uniqueKey(item) + local key = item.key or itemDB:makeKey(item) local v = items[key] if v then t[key] = Util.shallowCopy(v) @@ -146,7 +126,7 @@ function Milo:getTurtleInventory() end function Milo:requestCrafting(item) - local key = Milo:uniqueKey(item) + local key = itemDB:makeKey(item) if not self.context.craftingQueue[key] then item.crafted = 0 @@ -157,7 +137,7 @@ function Milo:requestCrafting(item) end end --- queue up an action that reliees on the crafting grid +-- queue up an action that relies on the crafting grid function Milo:queueRequest(request, callback) if Util.empty(self.context.queue) then os.queueEvent('milo_queue') @@ -178,7 +158,7 @@ function Milo:craftAndEject(item, count) end function Milo:makeRequest(item, count, callback) - local current = Milo:getItem(Milo:listItems(), item) or { count = 0 } + local current = self:getItem(item) or { count = 0 } if count <= 0 then return { @@ -187,18 +167,18 @@ function Milo:makeRequest(item, count, callback) count = 0, current = current.count, item = item, - key = item.key or Milo:uniqueKey(item), + key = item.key or itemDB:makeKey(item), } end local toCraft = count - math.min(current.count, count) if toCraft > 0 then - local recipe = Craft.findRecipe(self:uniqueKey(item)) + local recipe = Craft.findRecipe(itemDB:makeKey(item)) if not recipe then toCraft = 0 else -- if you ask for 1 stick, getCraftableAmount will return 4 (obviously) - toCraft = math.min(toCraft, Craft.getCraftableAmount(recipe, toCraft, Milo:listItems(), { })) + toCraft = math.min(toCraft, Craft.getCraftableAmount(recipe, toCraft, self:listItems(), { })) end end @@ -208,11 +188,11 @@ function Milo:makeRequest(item, count, callback) count = math.min(count, current.count), current = current.count, item = item, - key = item.key or Milo:uniqueKey(item), + key = item.key or itemDB:makeKey(item), } if request.count > 0 then - Milo:queueRequest(request, callback) + self:queueRequest(request, callback) end if request.craft > 0 then @@ -244,7 +224,7 @@ function Milo:updateRecipe(result, recipe) end function Milo:saveMachineRecipe(recipe, result, machine) - local key = Milo:uniqueKey(result) + local key = itemDB:makeKey(result) -- save the recipe self.context.userRecipes[key] = recipe @@ -261,32 +241,29 @@ function Milo:mergeResources(t) t = Util.shallowCopy(t) for k,v in pairs(self.context.resources) do - local key = itemDB:splitKey(k) - local item = self:getItem(t, key) + local item = t[k] if item then item = Util.shallowCopy(item) else - item = key + item = itemDB:splitKey(k) item.count = 0 item.key = k end Util.merge(item, v) - item.resource = v - t[item.key] = item + t[k] = item end for k in pairs(Craft.recipes) do - local v = itemDB:splitKey(k) - local item = self:getItem(t, v) + local item = t[k] if not item then - item = v + item = itemDB:splitKey(k) item.count = 0 item.key = k else item = Util.shallowCopy(item) end - t[item.key] = item item.has_recipe = true + t[k] = item end for key in pairs(Craft.machineLookup) do @@ -294,7 +271,7 @@ function Milo:mergeResources(t) if item then item = Util.shallowCopy(item) item.is_craftable = true - t[item.key] = item + t[key] = item end end diff --git a/milo/apis/storage.lua b/milo/apis/storage.lua index 9f469ff..7713b51 100644 --- a/milo/apis/storage.lua +++ b/milo/apis/storage.lua @@ -116,6 +116,10 @@ function Storage:initStorage() end v.adapter.online = true v.adapter.dirty = true + + if v.adapter.isOn and not v.adapter.isOn() then -- turtle + v.adapter.turnOn() + end elseif device[k] then v.adapter = device[k] v.adapter.online = true diff --git a/milo/core/listing.lua b/milo/core/listing.lua index 7a36ab0..8b1e326 100644 --- a/milo/core/listing.lua +++ b/milo/core/listing.lua @@ -169,7 +169,7 @@ function page:eject(amount) if amount == 'stack' then amount = item.maxCount or 64 elseif amount == 'all' then - item = Milo:getItem(Milo:listItems(), item) + item = Milo:getItem(item) if item then amount = item.count end diff --git a/milo/core/machines.lua b/milo/core/machines.lua index 0cf90ec..5589c30 100644 --- a/milo/core/machines.lua +++ b/milo/core/machines.lua @@ -340,7 +340,7 @@ function nodeWizard.filter:eventHandler(event) elseif event.type == 'scan_turtle' then local inventory = Milo:getTurtleInventory() for _,item in pairs(inventory) do - self.entry.filter[Milo:uniqueKey(item)] = true + self.entry.filter[itemDB:makeKey(item)] = true end self:resetGrid() self.grid:update() @@ -359,7 +359,7 @@ function nodeWizard.filter:eventHandler(event) self.form:save() self.entry.filter = { } for _,v in pairs(self.grid.values) do - self.entry.filter[Milo:uniqueKey(v)] = true + self.entry.filter[itemDB:makeKey(v)] = true end self:hide() self.callback() diff --git a/milo/plugins/exportTask.lua b/milo/plugins/exportTask.lua index 1094c96..722110d 100644 --- a/milo/plugins/exportTask.lua +++ b/milo/plugins/exportTask.lua @@ -31,7 +31,7 @@ function ExportTask:cycle(context) if slot then -- something is in the slot, find what we can export for key in pairs(entry.filter) do - local filterItem = Milo:splitKey(key) + local filterItem = itemDB:splitKey(key) if (slot.name == filterItem.name and (entry.ignoreDamage or slot.damage == filterItem.damage) and (entry.ignoreNbtHash or slot.nbtHash == filterItem.nbtHash)) then @@ -50,7 +50,7 @@ function ExportTask:cycle(context) -- slot is empty - export first matching item we have in storage for key in pairs(entry.filter) do - local items = Milo:getMatches(Milo:splitKey(key), entry) + local items = Milo:getMatches(itemDB:splitKey(key), entry) local _, item = next(items) if item then local count = math.min(item.count, itemDB:getMaxCount(item)) diff --git a/milo/plugins/importTask.lua b/milo/plugins/importTask.lua index 0bd6991..92c171b 100644 --- a/milo/plugins/importTask.lua +++ b/milo/plugins/importTask.lua @@ -17,12 +17,12 @@ function ImportTask:cycle(context) local function itemMatchesFilter(item) if not entry.ignoreDamage and not entry.ignoreNbtHash then - local key = Milo:uniqueKey(item) + local key = itemDB:makeKey(item) return entry.filter[key] end for key in pairs(entry.filter) do - local v = Milo:splitKey(key) + local v = itemDB:splitKey(key) if item.name == v.name and (entry.ignoreDamage or item.damage == v.damage) and (entry.ignoreNbtHash or item.nbtHash == v.nbtHash) then diff --git a/milo/plugins/item/manageTab.lua b/milo/plugins/item/manageTab.lua index 98407eb..8c1b8a0 100644 --- a/milo/plugins/item/manageTab.lua +++ b/milo/plugins/item/manageTab.lua @@ -39,11 +39,12 @@ local manageTab = UI.Window { } function manageTab:setItem(item) - self.origItem = item - self.item = Util.shallowCopy(item) - self.res = item.resource or { } + self.item = item + self.res = Util.shallowCopy(context.resources[item.key] or { }) self.res.displayName = self.item.displayName self.form:setValues(self.res) + + -- TODO: ignore damage should not be active if there is not a maxDamage value end function manageTab:eventHandler(event) @@ -51,19 +52,17 @@ function manageTab:eventHandler(event) UI:setPreviousPage() elseif event.type == 'form_complete' then - local item = self.item - if self.form:save() then - if self.res.displayName ~= self.origItem.displayName then - self.origItem.displayName = self.res.displayName - itemDB:add(self.origItem) + if self.res.displayName ~= self.item.displayName then + self.item.displayName = self.res.displayName + itemDB:add(self.item) itemDB:flush() - - -- TODO: ugh - if context.storage.cache[self.origItem.key] then - context.storage.cache[self.origItem.key].displayName = self.res.displayName + if context.storage.cache[self.item.key] then + context.storage.cache[self.item.key].displayName = self.res.displayName end + --context.storage:setDirty() end + self.res.displayName = nil Util.prune(self.res, function(v) if type(v) == 'boolean' then @@ -75,20 +74,14 @@ function manageTab:eventHandler(event) end) local newKey = { - name = item.name, - damage = self.res.ignoreDamage and 0 or item.damage, - nbtHash = not self.res.ignoreNbtHash and item.nbtHash or nil, + name = self.item.name, + damage = self.res.ignoreDamage and 0 or self.item.damage, + nbtHash = not self.res.ignoreNbtHash and self.item.nbtHash or nil, } - for k,v in pairs(context.resources) do - if v == self.res then - context.resources[k] = nil - break - end - end - + context.resources[self.item.key] = nil if not Util.empty(self.res) then - context.resources[Milo:uniqueKey(newKey)] = self.res + context.resources[itemDB:makeKey(newKey)] = self.res end Milo:saveResources() diff --git a/milo/plugins/limitTask.lua b/milo/plugins/limitTask.lua index 1de414e..a205810 100644 --- a/milo/plugins/limitTask.lua +++ b/milo/plugins/limitTask.lua @@ -1,4 +1,5 @@ -local Milo = require('milo') +local itemDB = require('itemDB') +local Milo = require('milo') local LimitTask = { name = 'limiter', @@ -11,7 +12,7 @@ function LimitTask:cycle(context) if trashcan then for key,res in pairs(context.resources) do if res.limit then - local items, count = Milo:getMatches(Milo:splitKey(key), res) + local items, count = Milo:getMatches(itemDB:splitKey(key), res) if count > res.limit then local amount = count - res.limit for _, item in pairs(items) do diff --git a/milo/plugins/machineLearn.lua b/milo/plugins/machineLearn.lua index b94914f..87063aa 100644 --- a/milo/plugins/machineLearn.lua +++ b/milo/plugins/machineLearn.lua @@ -111,7 +111,7 @@ function pages.confirmation:validate() } for k,v in pairs(inventory) do - recipe.ingredients[k] = Milo:uniqueKey(v) + recipe.ingredients[k] = itemDB:makeKey(v) end Milo:saveMachineRecipe(recipe, result, machine.name) diff --git a/milo/plugins/potionImportTask.lua b/milo/plugins/potionImportTask.lua index 115695b..0f90dc0 100644 --- a/milo/plugins/potionImportTask.lua +++ b/milo/plugins/potionImportTask.lua @@ -25,7 +25,7 @@ function PotionImportTask:cycle(context) if blazePowder then context.storage:export(bs, 5, 1, blazePowder) else - local item = itemDB:get(BLAZE_POWDER) or Milo:splitKey(BLAZE_POWDER) + local item = itemDB:get(BLAZE_POWDER) or itemDB:splitKey(BLAZE_POWDER) item.requested = 1 Milo:requestCrafting(item) end @@ -35,7 +35,7 @@ function PotionImportTask:cycle(context) -- brewing has completd if self.brewQueue[bs.name] and list[1] then - local key = Milo:uniqueKey(list[1]) + local key = itemDB:makeKey(list[1]) if not Craft.findRecipe(key) then Milo:saveMachineRecipe(self.brewQueue[bs.name], list[1], bs.name) end @@ -68,7 +68,7 @@ function PotionImportTask:cycle(context) if valid() then for i = 1, 4 do - recipe.ingredients[i] = Milo:uniqueKey(list[i]) + recipe.ingredients[i] = itemDB:makeKey(list[i]) end self.brewQueue[bs.name] = recipe diff --git a/milo/plugins/remote.lua b/milo/plugins/remote.lua index e0398e2..d449480 100644 --- a/milo/plugins/remote.lua +++ b/milo/plugins/remote.lua @@ -106,7 +106,7 @@ local function client(socket) local slot = node.adapter.getItemMeta(slotNo) if slot then if context.storage:import(node, slotNo, slot.count, slot) > 0 then - local item = Milo:getItem(Milo:listItems(), slot) + local item = Milo:getItem(slot) if item then socket:write({ type = 'received', @@ -127,7 +127,7 @@ local function client(socket) if count == 'stack' then count = itemDB:getMaxCount(data.item) elseif count == 'all' then - local item = Milo:getItem(Milo:listItems(), data.item) + local item = Milo:getItem(data.item) count = item and item.count or 0 end diff --git a/milo/plugins/replenishTask.lua b/milo/plugins/replenishTask.lua index ec8b5a2..0c36c6c 100644 --- a/milo/plugins/replenishTask.lua +++ b/milo/plugins/replenishTask.lua @@ -1,3 +1,4 @@ +local itemDB = require('itemDB') local Milo = require('milo') local ReplenishTask = { @@ -8,7 +9,7 @@ local ReplenishTask = { function ReplenishTask:cycle(context) for k,res in pairs(context.resources) do if res.low then - local item = Milo:splitKey(k) + local item = itemDB:splitKey(k) item.key = k local _, count = Milo:getMatches(item, res) @@ -27,7 +28,7 @@ function ReplenishTask:cycle(context) replenish = true, }) else - local request = context.craftingQueue[Milo:uniqueKey(item)] + local request = context.craftingQueue[itemDB:makeKey(item)] if request and request.replenish then --request.count = request.crafted end diff --git a/milo/plugins/storeView.lua b/milo/plugins/storeView.lua index e49e62f..4e4959f 100644 --- a/milo/plugins/storeView.lua +++ b/milo/plugins/storeView.lua @@ -8,7 +8,7 @@ local Util = require('util') local colors = _G.colors local os = _G.os -local config = Config.load('store') +local config = Config.load('shop') --[[ Display ]]-- local function createPage(node) @@ -123,18 +123,14 @@ local pages = { } -- called when an item to sell has been changed Event.on('store_refresh', function() - config = Config.load('store') + config = Config.load('shop') end) -Event.on('store_provide', function(_, item, quantity) - local count = 0 - for k, v in pairs(config) do - if v.name == item then - count = Milo:eject(itemDB:splitKey(k), quantity) - break - end - end - os.queueEvent('store_provided', item, count) +Event.on('store_provide', function(_, item, quantity, uid) + Milo:queueRequest({ }, function() + local count = Milo:eject(item, quantity) + os.queueEvent('store_provided', uid, count) + end) end) --[[ Task ]]-- diff --git a/milo/plugins/turtleLearn.lua b/milo/plugins/turtleLearn.lua index 1071297..01f23ec 100644 --- a/milo/plugins/turtleLearn.lua +++ b/milo/plugins/turtleLearn.lua @@ -55,7 +55,7 @@ local function learnRecipe() ]]-- maxCount = 1 - newRecipe.craftingTools[Milo:uniqueKey(tool)] = true + newRecipe.craftingTools[itemDB:makeKey(tool)] = true v1.craftingTool = true break end @@ -80,7 +80,7 @@ local function learnRecipe() newRecipe.count = recipe.count - local key = Milo:uniqueKey(recipe) + local key = itemDB:makeKey(recipe) if recipe.maxCount ~= 64 then newRecipe.maxCount = recipe.maxCount end @@ -88,7 +88,7 @@ local function learnRecipe() if ingredient.maxDamage > 0 then -- ingredient.damage = '*' -- I don't think this is right end - ingredients[k] = Milo:uniqueKey(ingredient) + ingredients[k] = itemDB:makeKey(ingredient) end Milo:updateRecipe(key, newRecipe) diff --git a/swshop/.package b/swshop/.package new file mode 100644 index 0000000..49caaa4 --- /dev/null +++ b/swshop/.package @@ -0,0 +1,6 @@ +{ + title = 'Switchcraft basic shop', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/swshop', + description = 'Modification of the k store by Lemmmy', + licence = 'MIT', +} diff --git a/swshop/json b/swshop/json new file mode 100644 index 0000000..3bef482 --- /dev/null +++ b/swshop/json @@ -0,0 +1,209 @@ +------------------------------------------------------------------ utils +local controls = {["\n"]="\\n", ["\r"]="\\r", ["\t"]="\\t", ["\b"]="\\b", ["\f"]="\\f", ["\""]="\\\"", ["\\"]="\\\\"} + +local function isArray(t) + local max = 0 + for k,v in pairs(t) do + if type(k) ~= "number" then + return false + elseif k > max then + max = k + end + end + return max == #t +end + +local whites = {['\n']=true; ['\r']=true; ['\t']=true; [' ']=true; [',']=true; [':']=true} +function removeWhite(str) + while whites[str:sub(1, 1)] do + str = str:sub(2) + end + return str +end + +------------------------------------------------------------------ encoding + +local function encodeCommon(val, pretty, tabLevel, tTracking) + local str = "" + + -- Tabbing util + local function tab(s) + str = str .. ("\t"):rep(tabLevel) .. s + end + + local function arrEncoding(val, bracket, closeBracket, iterator, loopFunc) + str = str .. bracket + if pretty then + str = str .. "\n" + tabLevel = tabLevel + 1 + end + for k,v in iterator(val) do + tab("") + loopFunc(k,v) + str = str .. "," + if pretty then str = str .. "\n" end + end + if pretty then + tabLevel = tabLevel - 1 + end + if str:sub(-2) == ",\n" then + str = str:sub(1, -3) .. "\n" + elseif str:sub(-1) == "," then + str = str:sub(1, -2) + end + tab(closeBracket) + end + + -- Table encoding + if type(val) == "table" then + assert(not tTracking[val], "Cannot encode a table holding itself recursively") + tTracking[val] = true + if isArray(val) then + arrEncoding(val, "[", "]", ipairs, function(k,v) + str = str .. encodeCommon(v, pretty, tabLevel, tTracking) + end) + else + arrEncoding(val, "{", "}", pairs, function(k,v) + assert(type(k) == "string", "JSON object keys must be strings", 2) + str = str .. encodeCommon(k, pretty, tabLevel, tTracking) + str = str .. (pretty and ": " or ":") .. encodeCommon(v, pretty, tabLevel, tTracking) + end) + end + -- String encoding + elseif type(val) == "string" then + str = '"' .. val:gsub("[%c\"\\]", controls) .. '"' + -- Number encoding + elseif type(val) == "number" or type(val) == "boolean" then + str = tostring(val) + else + error("JSON only supports arrays, objects, numbers, booleans, and strings", 2) + end + return str +end + +function encode(val) + return encodeCommon(val, false, 0, {}) +end + +function encodePretty(val) + return encodeCommon(val, true, 0, {}) +end + +------------------------------------------------------------------ decoding + +local decodeControls = {} +for k,v in pairs(controls) do + decodeControls[v] = k +end + +function parseBoolean(str) + if str:sub(1, 4) == "true" then + return true, removeWhite(str:sub(5)) + else + return false, removeWhite(str:sub(6)) + end +end + +function parseNull(str) + return nil, removeWhite(str:sub(5)) +end + +local numChars = {['e']=true; ['E']=true; ['+']=true; ['-']=true; ['.']=true} +function parseNumber(str) + local i = 1 + while numChars[str:sub(i, i)] or tonumber(str:sub(i, i)) do + i = i + 1 + end + local val = tonumber(str:sub(1, i - 1)) + str = removeWhite(str:sub(i)) + return val, str +end + +function parseString(str) + str = str:sub(2) + local s = "" + while str:sub(1,1) ~= "\"" do + local next = str:sub(1,1) + str = str:sub(2) + assert(next ~= "\n", "Unclosed string") + + if next == "\\" then + local escape = str:sub(1,1) + str = str:sub(2) + + next = assert(decodeControls[next..escape], "Invalid escape character") + end + + s = s .. next + end + return s, removeWhite(str:sub(2)) +end + +function parseArray(str) + str = removeWhite(str:sub(2)) + + local val = {} + local i = 1 + while str:sub(1, 1) ~= "]" do + local v = nil + v, str = parseValue(str) + val[i] = v + i = i + 1 + str = removeWhite(str) + end + str = removeWhite(str:sub(2)) + return val, str +end + +function parseObject(str) + str = removeWhite(str:sub(2)) + + local val = {} + while str:sub(1, 1) ~= "}" do + local k, v = nil, nil + k, v, str = parseMember(str) + val[k] = v + str = removeWhite(str) + end + str = removeWhite(str:sub(2)) + return val, str +end + +function parseMember(str) + local k = nil + k, str = parseValue(str) + local val = nil + val, str = parseValue(str) + return k, val, str +end + +function parseValue(str) + local fchar = str:sub(1, 1) + if fchar == "{" then + return parseObject(str) + elseif fchar == "[" then + return parseArray(str) + elseif tonumber(fchar) ~= nil or numChars[fchar] then + return parseNumber(str) + elseif str:sub(1, 4) == "true" or str:sub(1, 5) == "false" then + return parseBoolean(str) + elseif fchar == "\"" then + return parseString(str) + elseif str:sub(1, 4) == "null" then + return parseNull(str) + end + return nil +end + +function decode(str) + str = removeWhite(str) + t = parseValue(str) + return t +end + +function decodeFromFile(path) + local file = assert(fs.open(path, "r")) + local decoded = decode(file.readAll()) + file.close() + return decoded +end \ No newline at end of file diff --git a/swshop/jua.lua b/swshop/jua.lua new file mode 100644 index 0000000..993c965 --- /dev/null +++ b/swshop/jua.lua @@ -0,0 +1,122 @@ +local juaVersion = "0.0" + +juaRunning = false +eventRegistry = {} +timedRegistry = {} + +local function registerEvent(event, callback) + if eventRegistry[event] == nil then + eventRegistry[event] = {} + end + + table.insert(eventRegistry[event], callback) +end + +local function registerTimed(time, repeating, callback) + if repeating then + callback(true) + end + + table.insert(timedRegistry, { + time = time, + repeating = repeating, + callback = callback, + timer = os.startTimer(time) + }) +end + +local function discoverEvents(event) + local evs = {} + for k,v in pairs(eventRegistry) do + if k == event or string.match(k, event) or event == "*" then + for i,v2 in ipairs(v) do + table.insert(evs, v2) + end + end + end + + return evs +end + +function on(event, callback) + registerEvent(event, callback) +end + +function setInterval(callback, time) + registerTimed(time, true, callback) +end + +function setTimeout(callback, time) + registerTimed(time, false, callback) +end + +function tick() + local eargs = {os.pullEventRaw()} + local event = eargs[1] + + if eventRegistry[event] == nil then + eventRegistry[event] = {} + else + local evs = discoverEvents(event) + for i, v in ipairs(evs) do + v(unpack(eargs)) + end + end + + if event == "timer" then + local timer = eargs[2] + + for i = #timedRegistry, 1, -1 do + local v = timedRegistry[i] + if v.timer == timer then + v.callback(not v.repeating or nil) + + if v.repeating then + v.timer = os.startTimer(v.time) + else + table.remove(timedRegistry, i) + end + end + end + end +end + +function run() + os.queueEvent("init") + juaRunning = true + while juaRunning do + tick() + end +end + +function go(func) + on("init", func) + run() +end + +function stop() + juaRunning = false +end + +function await(func, ...) + local args = {...} + local out + local finished + func(function(...) + out = {...} + finished = true + end, unpack(args)) + while not finished do tick() end + return unpack(out) +end + +return { + on = on, + setInterval = setInterval, + setTimeout = setTimeout, + tick = tick, + run = run, + go = go, + stop = stop, + await = await +} \ No newline at end of file diff --git a/swshop/k.lua b/swshop/k.lua new file mode 100644 index 0000000..a52d0fd --- /dev/null +++ b/swshop/k.lua @@ -0,0 +1,413 @@ +local w +local r +local jua +local json +local await + +local endpoint = "krist.ceriat.net" +local wsEndpoint = "ws://"..endpoint +local httpEndpoint = "http://"..endpoint + +local function asserttype(var, name, vartype, optional) + if not (type(var) == vartype or optional and type(var) == "nil") then + error(name..": expected "..vartype.." got "..type(var), 3) + end +end + +function init(juai, jsoni, wi, ri) + asserttype(juai, "jua", "table") + asserttype(jsoni, "json", "table") + asserttype(wi, "w", "table", true) + asserttype(ri, "r", "table") + + jua = juai + await = juai.await + json = jsoni + w = wi + r = ri +end + +local function prints(...) + local objs = {...} + for i, obj in ipairs(objs) do + print(textutils.serialize(obj)) + end +end + +local function url(call) + return httpEndpoint..call +end + +local function api_request(cb, api, data) + local success, url, handle = await(r.request, url(api) .. (api:find("%%?") and "?cc" or "&cc"), {["Content-Type"]="application/json"}, data and json.encode(data)) + if success then + cb(success, json.decode(handle.readAll())) + handle.close() + else + cb(success) + end +end + +local function authorize_websocket(cb, privatekey) + asserttype(cb, "callback", "function") + asserttype(privatekey, "privatekey", "string", true) + + api_request(function(success, data) + cb(success and data and data.ok, data.url and data.url:gsub("wss:", "ws:") or data) + end, "/ws/start", { + privatekey = privatekey + }) +end + +function address(cb, address) + asserttype(cb, "callback", "function") + asserttype(address, "address", "string") + + api_request(function(success, data) + if data.address then + data.address.address = address + end + cb(success and data and data.ok, data.address or data) + end, "/addresses/"..address) +end + +function addressTransactions(cb, address, limit, offset) + asserttype(cb, "callback", "function") + asserttype(address, "address", "string") + asserttype(limit, "limit", "number", true) + asserttype(offset, "offset", "number", true) + + api_request(function(success, data) + cb(success and data and data.ok, data.transactions or data) + end, "/addresses/"..address.."/transactions?limit="..(limit or 50).."&offset="..(offset or 0)) +end + +function addressNames(cb, address) + asserttype(cb, "callback", "function") + asserttype(address, "address", "string") + + api_request(function(success, data) + cb(success and data and data.ok, data.names or data) + end, "/addresses/"..address.."/names") +end + +function addresses(cb, limit, offset) + asserttype(cb, "callback", "function") + asserttype(limit, "limit", "number", true) + asserttype(offset, "offset", "number", true) + + api_request(function(success, data) + cb(success and data and data.ok, data.addresses or data) + end, "/addresses?limit="..(limit or 50).."&offset="..(offset or 0)) +end + +function rich(cb, limit, offset) + asserttype(cb, "callback", "function") + asserttype(limit, "limit", "number", true) + asserttype(offset, "offset", "number", true) + + api_request(function(success, data) + cb(success and data and data.ok, data.addresses or data) + end, "/addresses/rich?limit="..(limit or 50).."&offset="..(offset or 0)) +end + +function transactions(cb, limit, offset) + asserttype(cb, "callback", "function") + asserttype(limit, "limit", "number", true) + asserttype(offset, "offset", "number", true) + + api_request(function(success, data) + cb(success and data and data.ok, data.transactions or data) + end, "/transactions?limit="..(limit or 50).."&offset="..(offset or 0)) +end + +function latestTransactions(cb, limit, offset) + asserttype(cb, "callback", "function") + asserttype(limit, "limit", "number", true) + asserttype(offset, "offset", "number", true) + + api_request(function(success, data) + cb(success and data and data.ok, data.transactions or data) + end, "/transactions/latest?limit="..(limit or 50).."&offset="..(offset or 0)) +end + +function transaction(cb, txid) + asserttype(cb, "callback", "function") + asserttype(txid, "txid", "number") + + api_request(function(success, data) + cb(success and data and data.ok, data.transaction or data) + end, "/transactions/"..txid) +end + +function makeTransaction(cb, privatekey, to, amount, metadata) + asserttype(cb, "callback", "function") + asserttype(privatekey, "privatekey", "string") + asserttype(to, "to", "string") + asserttype(amount, "amount", "number") + asserttype(metadata, "metadata", "string", true) + + api_request(function(success, data) + cb(success and data and data.ok, data.transaction or data) + end, "/transactions", { + privatekey = privatekey, + to = to, + amount = amount, + metadata = metadata + }) +end + +local wsEventNameLookup = { + blocks = "block", + ownBlocks = "block", + transactions = "transaction", + ownTransactions = "transaction", + names = "name", + ownNames = "name", + ownWebhooks = "webhook", + motd = "motd" +} + +local wsEvents = {} + +local wsReqID = 0 +local wsReqRegistry = {} +local wsEvtRegistry = {} +local wsHandleRegistry = {} + +local function newWsID() + local id = wsReqID + wsReqID = wsReqID + 1 + return id +end + +local function registerEvent(id, event, callback) + if wsEvtRegistry[id] == nil then + wsEvtRegistry[id] = {} + end + + if wsEvtRegistry[id][event] == nil then + wsEvtRegistry[id][event] = {} + end + + table.insert(wsEvtRegistry[id][event], callback) +end + +local function registerRequest(id, reqid, callback) + if wsReqRegistry[id] == nil then + wsReqRegistry[id] = {} + end + + wsReqRegistry[id][reqid] = callback +end + +local function discoverEvents(id, event) + local evs = {} + for k,v in pairs(wsEvtRegistry[id]) do + if k == event or string.match(k, event) or event == "*" then + for i,v2 in ipairs(v) do + table.insert(evs, v2) + end + end + end + + return evs +end + +wsEvents.success = function(id, handle) + -- fire success event + wsHandleRegistry[id] = handle + if wsEvtRegistry[id] then + local evs = discoverEvents(id, "success") + for i, v in ipairs(evs) do + v(id, handle) + end + end +end + +wsEvents.failure = function(id) + -- fire failure event + if wsEvtRegistry[id] then + local evs = discoverEvents(id, "failure") + for i, v in ipairs(evs) do + v(id) + end + end +end + +wsEvents.message = function(id, data) + local data = json.decode(data) + --print("msg:"..tostring(data.ok)..":"..tostring(data.type)..":"..tostring(data.id)) + --prints(data) + -- handle events and responses + if wsReqRegistry[id] and wsReqRegistry[id][tonumber(data.id)] then + wsReqRegistry[id][tonumber(data.id)](data) + elseif wsEvtRegistry[id] then + local evs = discoverEvents(id, data.type) + for i, v in ipairs(evs) do + v(data) + end + + if data.event then + local evs = discoverEvents(id, data.event) + for i, v in ipairs(evs) do + v(data) + end + end + + local evs2 = discoverEvents(id, "message") + for i, v in ipairs(evs2) do + v(id, data) + end + end +end + +wsEvents.closed = function(id) + -- fire closed event + if wsEvtRegistry[id] then + local evs = discoverEvents(id, "closed") + for i, v in ipairs(evs) do + v(id) + end + end +end + +local function wsRequest(cb, id, type, data) + local reqID = newWsID() + registerRequest(id, reqID, function(data) + cb(data) + end) + data.id = tostring(reqID) + data.type = type + wsHandleRegistry[id].send(json.encode(data)) +end + +local function barebonesMixinHandle(id, handle) + handle.on = function(event, cb) + registerEvent(id, event, cb) + end + + return handle +end + +local function mixinHandle(id, handle) + handle.subscribe = function(cb, event, eventcb) + local data = await(wsRequest, id, "subscribe", { + event = event + }) + registerEvent(id, wsEventNameLookup[event], eventcb) + cb(data.ok, data) + end + + return barebonesMixinHandle(id, handle) +end + +function connect(cb, privatekey, preconnect) + asserttype(cb, "callback", "function") + asserttype(privatekey, "privatekey", "string", true) + asserttype(preconnect, "preconnect", "function", true) + local url + if privatekey then + local success, auth = await(authorize_websocket, privatekey) + url = success and auth or wsEndpoint + end + local id = w.open(wsEvents, url) + if preconnect then + preconnect(id, barebonesMixinHandle(id, {})) + end + registerEvent(id, "success", function(id, handle) + cb(true, mixinHandle(id, handle)) + end) + registerEvent(id, "failure", function(id) + cb(false) + end) +end + +local domainMatch = "^([%l%d-_]*)@?([%l%d-]+).kst$" +local commonMetaMatch = "^(.+)=(.+)$" + +function parseMeta(meta) + asserttype(meta, "meta", "string") + local tbl = {meta={}} + + for m in meta:gmatch("[^;]+") do + if m:match(domainMatch) then + -- print("Matched domain") + + local p1, p2 = m:match("([%l%d-_]*)@"), m:match("@?([%l%d-]+).kst") + tbl.name = p1 + tbl.domain = p2 + + elseif m:match(commonMetaMatch) then + -- print("Matched common meta") + + local p1, p2 = m:match(commonMetaMatch) + + tbl.meta[p1] = p2 + + else + -- print("Unmatched standard meta") + + table.insert(tbl.meta, m) + end + -- print(m) + end + -- print(textutils.serialize(tbl)) + return tbl +end + +local g = string.gsub +sha256 = loadstring(g(g(g(g(g(g(g(g('Sa=XbandSb=XbxWSc=XlshiftSd=unpackSe=2^32SYf(g,h)Si=g/2^hSj=i%1Ui-j+j*eVSYk(l,m)Sn=l/2^mUn-n%1VSo={0x6a09e667Tbb67ae85T3c6ef372Ta54ff53aT510e527fT9b05688cT1f83d9abT5be0cd19}Sp={0x428a2f98T71374491Tb5c0fbcfTe9b5dba5T3956c25bT59f111f1T923f82a4Tab1c5ed5Td807aa98T12835b01T243185beT550c7dc3T72be5d74T80deb1feT9bdc06a7Tc19bf174Te49b69c1Tefbe4786T0fc19dc6T240ca1ccT2de92c6fT4a7484aaT5cb0a9dcT76f988daT983e5152Ta831c66dTb00327c8Tbf597fc7Tc6e00bf3Td5a79147T06ca6351T14292967T27b70a85T2e1b2138T4d2c6dfcT53380d13T650a7354T766a0abbT81c2c92eT92722c85Ta2bfe8a1Ta81a664bTc24b8b70Tc76c51a3Td192e819Td6990624Tf40e3585T106aa070T19a4c116T1e376c08T2748774cT34b0bcb5T391c0cb3T4ed8aa4aT5b9cca4fT682e6ff3T748f82eeT78a5636fT84c87814T8cc70208T90befffaTa4506cebTbef9a3f7Tc67178f2}SYq(r,q)if e-1-r[1] 122 and 101 or byte > 57 and byte + 39 or byte) +end + +function makev2address(key) + local protein = {} + local stick = sha256(sha256(key)) + local n = 0 + local link = 0 + local v2 = "k" + repeat + if n < 9 then protein[n] = string.sub(stick,0,2) + stick = sha256(sha256(stick)) end + n = n + 1 + until n == 9 + n = 0 + repeat + link = tonumber(string.sub(stick,1+(2*n),2+(2*n)),16) % 9 + if string.len(protein[link]) ~= 0 then + v2 = v2 .. makeaddressbyte(tonumber(protein[link],16)) + protein[link] = '' + n = n + 1 + else + stick = sha256(stick) + end + until n == 9 + return v2 +end + +function toKristWalletFormat(passphrase) + return sha256("KRISTWALLET"..passphrase).."-000" +end + +return { + init = init, + address = address, + addressTransactions = addressTransactions, + addressNames = addressNames, + addresses = addresses, + rich = rich, + transactions = transactions, + latestTransactions = latestTransactions, + transaction = transaction, + makeTransaction = makeTransaction, + connect = connect, + parseMeta = parseMeta, + sha256 = sha256, + makeaddressbyte = makeaddressbyte, + makev2address = makev2address, + toKristWalletFormat = toKristWalletFormat +} \ No newline at end of file diff --git a/swshop/r.lua b/swshop/r.lua new file mode 100644 index 0000000..b13062e --- /dev/null +++ b/swshop/r.lua @@ -0,0 +1,63 @@ +local jua = nil +local idPatt = "#R%d+" + +callbackRegistry = {} + +local function gfind(str, patt) + local t = {} + for found in str:gmatch(patt) do + table.insert(t, found) + end + + if #t > 0 then + return t + else + return nil + end +end + +local function findID(url) + local found = gfind(url, idPatt) + return tonumber(found[#found]:sub(found[#found]:find("%d+"))) +end + +local function newID() + return #callbackRegistry + 1 +end + +local function trimID(url) + local found = gfind(url, idPatt) + local s, e = url:find(found[#found]) + return url:sub(1, s-1) +end + +function request(callback, url, headers, postData) + local id = newID() + local newUrl = url .. "#R" .. id + http.request(newUrl, postData, headers) + callbackRegistry[id] = callback +end + +function init(jua) + jua = jua + jua.on("http_success", function(event, url, handle) + local id = findID(url) + if callbackRegistry[id] then + callbackRegistry[id](true, trimID(url), handle) + table.remove(callbackRegistry, id) + end + end) + + jua.on("http_failure", function(event, url, handle) + local id = findID(url) + if callbackRegistry[id] then + callbackRegistry[id](false, trimID(url), handle) + table.remove(callbackRegistry, id) + end + end) +end + +return { + request = request, + init = init +} \ No newline at end of file diff --git a/swshop/shop.lua b/swshop/shop.lua new file mode 100644 index 0000000..8a54519 --- /dev/null +++ b/swshop/shop.lua @@ -0,0 +1,119 @@ +local programDir = fs.getDir(shell.getRunningProgram()) +os.loadAPI(programDir .. '/'.. 'json') + +local w = require("w") +local r = require("r") +local k = require("k") +local jua = require("jua") +local await = jua.await + +local fs = _G.fs +local json = _G.json +local os = _G.os +local textutils = _G.textutils + +r.init(jua) +w.init(jua) +k.init(jua, json, w, r) + +local domain +local password +local privatekey +local address + +jua.on("terminate", function() + jua.stop() + _G.printError("Terminated") +end) + +local function getItemDetails(item) + local f = fs.open('usr/config/shop', "r") + if f then + local t = f.readAll() + f.close() + t = textutils.unserialize(t) + for k, v in pairs(t) do + if v.name == item then + return k, v.price + end + end + end +end + +local function handleTransaction(transaction) + local from = transaction.from + local to = transaction.to + local value = transaction.value + if to ~= address or not transaction.metadata then return end + + local metadata = k.parseMeta(transaction.metadata) + if not metadata.domain or metadata.domain ~= domain then return end + + local recipient = metadata.meta and (metadata.meta["return"] or from) or from + print("Handling transaction from ", recipient) + print('purchase: ' .. tostring(metadata.name)) + print('value: ' .. value) + + local function refundTransaction(amount, reason) + print("Refunding to ", recipient) + await(k.makeTransaction, privatekey, recipient, amount, reason) + end + + local itemId, price = getItemDetails(metadata.name) + if not itemId or not price then + print('invalid item') + --return refundTransaction(value, "error=Item specified is not valid") + return -- there could be multiple stores... + end + + if value < price then + print('value too low') + return refundTransaction(value, "error=Please pay the price listed on-screen.") + end + + local count = math.floor(value / price) + local uid = math.random() + os.queueEvent('store_provide', itemId, count, uid) + local timerId = os.startTimer(5) + while true do + local e, p1, p2 = os.pullEvent() + if e == 'timer' and p1 == timerId then + print('timed out waiting for provide') + refundTransaction(value, "error=Timed out attempting to provide items") + break + + elseif e == 'store_provided' and p1 == uid then + local extra = value - (price * p2) + if extra > 0 then + print('extra: ' .. extra) + refundTransaction(extra, "message=Here's your change!") + end + break + end + end +end + +jua.on('open_store', function(_, _domain, _password) + domain = _domain + password = _password + +print('opening store for: ' .. domain) + + privatekey = k.toKristWalletFormat(password) + address = k.makev2address(privatekey) + + local success, ws = await(k.connect, privatekey) + assert(success, "Failed to get websocket URL") + + print("Connected to websocket.") + + success = await(ws.subscribe, "ownTransactions", function(data) + local transaction = data.transaction + handleTransaction(transaction) + end) + assert(success, "Failed to subscribe to event") +end) + +jua.go(function() + print("Ready") +end) diff --git a/swshop/w.lua b/swshop/w.lua new file mode 100644 index 0000000..7158792 --- /dev/null +++ b/swshop/w.lua @@ -0,0 +1,134 @@ +local jua = nil +local idPatt = "#R%d+" + +if not ((socket and socket.websocket) or http.websocketAsync) then + error("You do not have CC:Tweaked/CCTweaks installed or you are not on the latest version.") +end + +local newws = socket and socket.websocket or http.websocketAsync +local async +if socket and socket.websocket then + async = false +else + async = true +end + +callbackRegistry = {} +wsRegistry = {} + +local function gfind(str, patt) + local t = {} + for found in str:gmatch(patt) do + table.insert(t, found) + end + + if #t > 0 then + return t + else + return nil + end +end + +local function findID(url) + local found = gfind(url, idPatt) + return tonumber(found[#found]:sub(found[#found]:find("%d+"))) +end + +local function newID() + return #callbackRegistry + 1 +end + +local function trimID(url) + local found = gfind(url, idPatt) + local s, e = url:find(found[#found]) + return url:sub(1, s-1) +end + +function open(callback, url, headers) + local id + if async then + id = newID() + end + local newUrl + if async then + newUrl = url .. "#R" .. id + newws(newUrl, headers) + else + if headers then + error("Websocket headers not supported under CCTweaks") + end + local ws = newws(url) + ws.send = ws.write + id = ws.id() + wsRegistry[id] = ws + end + callbackRegistry[id] = callback + return id +end + +function init(jua) + jua = jua + if async then + jua.on("websocket_success", function(event, url, handle) + local id = findID(url) + if id and callbackRegistry[id] and callbackRegistry[id].success then + callbackRegistry[id].success(id, handle) + end + end) + + jua.on("websocket_failure", function(event, url) + local id = findID(url) + if id and callbackRegistry[id] and callbackRegistry[id].failure then + callbackRegistry[id].failure(id) + end + table.remove(callbackRegistry, id) + end) + + jua.on("websocket_message", function(event, url, data) + local id = findID(url) + if id and callbackRegistry[id] and callbackRegistry[id].message then + callbackRegistry[id].message(id, data) + end + end) + + jua.on("websocket_closed", function(event, url) + local id = findID(url) + if id and callbackRegistry[id] and callbackRegistry[id].closed then + callbackRegistry[id].closed(id) + end + table.remove(callbackRegistry, id) + end) + else + jua.on("socket_connect", function(event, id) + if id and callbackRegistry[id] and callbackRegistry[id].success then + callbackRegistry[id].success(id, wsRegistry[id]) + end + end) + + jua.on("socket_error", function(event, id, msg) + if id and callbackRegistry[id] and callbackRegistry[id].failure then + callbackRegistry[id].failure(id, msg) + end + table.remove(callbackRegistry, id) + end) + + jua.on("socket_message", function(event, id) + if id and callbackRegistry[id] and callbackRegistry[id].message then + local data = wsRegistry[id].read() + callbackRegistry[id].message(id, data) + end + end) + + jua.on("socket_closed", function(event, id) + if id and callbackRegistry[id] and callbackRegistry[id].closed then + callbackRegistry[id].closed(id) + end + table.remove(callbackRegistry, id) + end) + end +end + +return { + open = open, + init = init +} \ No newline at end of file