local version = "1.3.33" local configHelpers = require "util.configHelpers" local schemas = require "core.schemas" local ScanInventory = require("core.inventory.ScanInventory") local oldPullEvent = os.pullEvent os.pullEvent = os.pullEventRaw local oldPrint = print local logs = {} local maxLogs = 100 function print(...) local args = {...} local str = "" for i = 1, #args do str = str .. tostring(args[i]) if i ~= #args then str = str .. " " end end table.insert(logs, 1, {time = os.time("utc"), text = str}) if #logs > maxLogs then table.remove(logs, #logs) end end --- Imports local _ = require("util.score") local sound = require("util.sound") local eventHook = require("util.eventHook") local Display = require("modules.display") local Solyd = require("modules.solyd") local hooks = require("modules.hooks") local useCanvas = hooks.useCanvas local Button = require("components.Button") local SmolButton = require("components.SmolButton") local BasicButton = require("components.BasicButton") local BigText = require("components.BigText") local Modal = require("components.Modal") local Logs = require("components.Logs") local ConfigEditor = require("components.ConfigEditor") local bigFont = require("fonts.bigfont") local SmolText = require("components.SmolText") local smolFont = require("fonts.smolfont") local BasicText = require("components.BasicText") 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") local defaultLayout = require("DefaultLayout") local loadRIF = require("modules.rif") local configDefaults = require("configDefaults") local config = require("config") local products = require("products") local eventHooks = {} if fs.exists(fs.combine(fs.getDir(shell.getRunningProgram()), "eventHooks.lua")) then eventHooks = require("eventHooks") end --- End Imports configHelpers.loadDefaults(config, configDefaults) local configErrors = ConfigValidator.validateConfig(config) local productsErrors = ConfigValidator.validateProducts(products) if (configErrors and #configErrors > 0) or (productsErrors and #productsErrors > 0) then config.ready = false else config.ready = true end local peripherals = {} configHelpers.getPeripherals(config, peripherals) -- if config.shopSync and config.shopSync.enabled and not config.shopSync.force then -- error("ShopSync is not yet finalized, please update Radon to use this feature, or set config.shopSync.force to true to use current ShopSync spec") -- end local display = Display.new({theme=config.theme, monitor=config.peripherals.monitor}) local terminal = Display.new({theme=config.terminalTheme, monitor=term}) local layout = nil local layoutFile = nil local layoutRenderer = nil local configState = { config = config, products = products, eventHooks = eventHooks, } local Main = Solyd.wrapComponent("Main", function(props) local canvas = useCanvas(display) local flatCanvas = {} if props.configState.config.ready and props.shopState.kryptonReady then local theme = props.configState.config.theme if theme.formatting.layout ~= "custom" or not theme.formatting.layoutFile then local addBg = false if theme.formatting.layout ~= layout then layout = theme.formatting.layout if theme.palette then require("util.setPalette")(display.mon, theme.palette) end if theme.colors and theme.colors.bgColor then display.ccCanvas.clear = theme.colors.bgColor addBg = true end display.ccCanvas:outputFlush(display.mon) end flatCanvas = defaultLayout(canvas, display, props, theme, version) if addBg then table.insert(flatCanvas, Rect { display = display, x = 1, y = 1, width = display.bgCanvas.width, height = display.bgCanvas.height, color = theme.colors.bgColor, }) end else local addBg = false if theme.formatting.layoutFile ~= layoutFile or theme.formatting.layout ~= layout then layout = "custom" layoutFile = theme.formatting.layoutFile local f = fs.open(layoutFile, "r") if not f then error("Could not open layout file: " .. layoutFile) end layoutRendererString = f.readAll() f.close() local loadedString, err = load(layoutRendererString, layoutFile, "c", setmetatable({ require = require }, {__index = _ENV})) if not loadedString then error("Could not load layout file: " .. err) end layoutRenderer = loadedString() if theme.layouts[layoutFile] and theme.layouts[layoutFile].palette then require("util.setPalette")(display.mon, theme.layouts[layoutFile].palette) end if theme.layouts[layoutFile] and theme.layouts[layoutFile].colors and theme.layouts[layoutFile].colors.bgColor then display.ccCanvas.clear = theme.layouts[layoutFile].colors.bgColor addBg = true end display.ccCanvas:outputFlush(display.mon) end flatCanvas = layoutRenderer(canvas, display, props, theme, version) if addBg then table.insert(flatCanvas, 1, Rect { display = display, x = 1, y = 1, width = display.bgCanvas.width, height = display.bgCanvas.height, color = theme.layouts[layoutFile].colors.bgColor, }) end end elseif not props.configState.config.ready then flatCanvas = { BasicText({ display = display, text = "Waiting on config...", x = 1, y = 1, color = colors.white, bgColor = colors.black, }) } else flatCanvas = { BasicText({ display = display, text = "Connecting...", x = 1, y = 1, color = colors.white, bgColor = colors.black, }) } end return _.flat({ _.flat(flatCanvas) }), { canvas = {canvas, 1, 1}, config = props.configState.config or {}, shopState = props.shopState or {}, products = props.shopState.products, peripherals = props.peripherals or {}, } end) local terminalState = { prevCatagory = "logs", activeCatagory = "logs", configErrors = configErrors, productsErrors = productsErrors, scroll = 0, maxScroll = 0, } --local mbsMode = settings.get("mbs.shell.enabled") local Terminal = Solyd.wrapComponent("Terminal", function(props) local canvas = useCanvas(terminal) local theme = props.configState.config.terminalTheme local flatCanvas = {} local versionString = "Radon " .. version local terminalCatagories = { "logs", "config", "products" } local bodyHeight = math.floor(terminal.bgCanvas.height / 3) - 1 local bodyWidth = math.floor(terminal.bgCanvas.width / 2) table.insert(flatCanvas, Rect { key = "header", display = terminal, x = 1, y = 1, width = terminal.bgCanvas.width, height = 3, color = theme.colors.titleBgColor, }) table.insert(flatCanvas, BasicText { key = "title", display = terminal, align = "left", text = versionString, x = 1, y = 1, color = theme.colors.titleTextColor, bg = theme.colors.titleBgColor, }) local catagoriesX = 1 + #versionString for i = 1, #terminalCatagories do local bgColor = theme.colors.catagoryBgColor if props.terminalState.activeCatagory == terminalCatagories[i] then bgColor = theme.colors.activeCatagoryBgColor end table.insert(flatCanvas, BasicButton { key = "catagory-" .. terminalCatagories[i], display = terminal, align = "center", text = " " .. terminalCatagories[i] .. " ", x = 2 + catagoriesX, y = 1, color = theme.colors.catagoryTextColor, bg = bgColor, onClick = function() props.terminalState.activeCatagory = terminalCatagories[i] end }) catagoriesX = catagoriesX + 3 + #terminalCatagories[i] end if (props.terminalState.configErrors and #props.terminalState.configErrors > 0) or terminalState.activeCatagory == "config" then if terminalState.prevCatagory ~= "config" then terminalState.prevCatagory = "config" terminalState.configPath = "" terminalState.scroll = 0 end table.insert(flatCanvas, ConfigEditor { key = "config-editor", display = terminal, x = 1, y = 2, width = bodyWidth, height = bodyHeight, config = props.configState.config, schema = schemas.configSchema, errors = props.terminalState.configErrors, errorPrefix = "config", terminalState = props.terminalState, theme = theme.colors.configEditor, onSave = function(newConfig) props.shopState.oldConfig = props.configState.config props.shopState.config = newConfig props.configState.config = newConfig props.terminalState.configErrors = ConfigValidator.validateConfig(newConfig) if (not props.terminalState.configErrors or #props.terminalState.configErrors == 0) and (not props.terminalState.productsErrors or #props.terminalState.productsErrors == 0) then newConfig.ready = true end configHelpers.getPeripherals(newConfig, peripherals) -- TODO: Detect if we actually need to update currencies props.shopState.changedCurrencies = true local f = fs.open("config.lua", "w") f.write("return " .. textutils.serialize(newConfig)) f.close() print("Configs updated!") if props.configState.eventHooks and props.configState.eventHooks.configSaved then props.configState.eventHooks.configSaved(newConfig) end end }) elseif (props.terminalState.productsErrors and #props.terminalState.productsErrors > 0) or terminalState.activeCatagory == "products" then if terminalState.prevCatagory ~= "products" then terminalState.prevCatagory = "products" terminalState.configPath = "" terminalState.scroll = 0 end table.insert(flatCanvas, ConfigEditor { key = "products-editor", display = terminal, x = 1, y = 2, width = bodyWidth, height = bodyHeight, config = props.shopState.products, schema = schemas.productsSchema, errors = props.terminalState.productsErrors, errorPrefix = "products", terminalState = props.terminalState, theme = theme.colors.productsEditor, onSave = function(newConfig) props.shopState.products = newConfig props.configState.products = newConfig props.terminalState.productsErrors = ConfigValidator.validateProducts(products) if (not props.terminalState.configErrors or #props.terminalState.configErrors == 0) and (not props.terminalState.productsErrors or #props.terminalState.productsErrors == 0) then props.configState.config.ready = true end ScanInventory.clearNbtCache() local f = fs.open("products.lua", "w") f.write("return " .. textutils.serialize(newConfig)) f.close() print("Products updated!") if props.configState.eventHooks and props.configState.eventHooks.productsSaved then props.configState.eventHooks.productsSaved(newConfig) end end }) elseif terminalState.activeCatagory == "logs" then table.insert(flatCanvas, Logs { key = "logs", display = terminal, x = 1, y = 2, width = bodyWidth, height = bodyHeight, logs = props.logs, color = theme.colors.logTextColor, bg = theme.colors.bgColor, }) end table.insert(flatCanvas, Modal { key="modal" }) return _.flat({ _.flat(flatCanvas) }), { canvas = {canvas, 1, 1}, config = props.configState.config or {}, shopState = props.shopState or {}, products = props.shopState.products, terminalState = props.terminalState, peripherals = props.peripherals or {}, modal = props.modal } end) local t = 0 local tree = nil local terminalTree = nil local lastClock = os.epoch("utc") local lastCanvases = { { stack = {}, hash = {} }, { stack = {}, hash = {} }, } local function diffCanvasStack(diffDisplay, newStack, lastCanvas) -- Find any canvases that were removed local removed = {} local kept, newCanvasHash = {}, {} for i = 1, #lastCanvas.stack do removed[lastCanvas.stack[i][1]] = lastCanvas.stack[i] end for i = 1, #newStack do if removed[newStack[i][1]] then kept[#kept+1] = newStack[i] removed[newStack[i][1]] = nil newStack[i][1].allDirty = false else -- New newStack[i][1].allDirty = true end newCanvasHash[newStack[i][1]] = newStack[i] end -- Mark rectangle of removed canvases on bgCanvas (TODO: using bgCanvas is a hack) for _, canvas in pairs(removed) do if canvas[1].brand == "TextCanvas" then diffDisplay.bgCanvas:dirtyRect(canvas[2], canvas[3], canvas[1].width*2, canvas[1].height*3) else diffDisplay.bgCanvas:dirtyRect(canvas[2], canvas[3], canvas[1].width, canvas[1].height) end end -- For each kept canvas, mark the bounds if the new bounds are different for i = 1, #kept do local newCanvas = kept[i] local oldCanvas = lastCanvas.hash[newCanvas[1]] if oldCanvas then if oldCanvas[2] ~= newCanvas[2] or oldCanvas[3] ~= newCanvas[3] then -- TODO: Optimize this? if oldCanvas[1].brand == "TextCanvas" then diffDisplay.bgCanvas:dirtyRect(oldCanvas[2], oldCanvas[3], oldCanvas[1].width*2, oldCanvas[1].height*3) diffDisplay.bgCanvas:dirtyRect(newCanvas[2], newCanvas[3], newCanvas[1].width*2, newCanvas[1].height*3) else diffDisplay.bgCanvas:dirtyRect(oldCanvas[2], oldCanvas[3], oldCanvas[1].width, oldCanvas[1].height) diffDisplay.bgCanvas:dirtyRect(newCanvas[2], newCanvas[3], newCanvas[1].width, newCanvas[1].height) end end end end lastCanvas.stack = newStack lastCanvas.hash = newCanvasHash end local shopState = Core.ShopState.new(config, products, peripherals, version, logs, eventHooks) --local Profiler = require("profile") local deltaTimer = os.startTimer(0) local success, err = pcall(function() ShopRunner.launchShop(shopState, function() --Profiler:activate() print("Radon " .. version .. " started") if eventHooks and eventHooks.start then eventHook.execute(eventHooks.start, version, config, products, shopState) end while true do -- add t = t if we need animations tree = Solyd.render(tree, Main { configState = configState, shopState = shopState, peripherals = peripherals}) local context = Solyd.getTopologicalContext(tree, { "canvas", "aabb" }) diffCanvasStack(display, context.canvas, lastCanvases[1]) local t1 = os.epoch("utc") local cstack = { {display.bgCanvas, 1, 1}, unpack(context.canvas) } -- cstack[#cstack+1] = {display.textCanvas, 1, 1} display.ccCanvas:composite(unpack(cstack)) display.ccCanvas:outputDirty(display.mon) local t2 = os.epoch("utc") --print("Render time: " .. (t2-t1) .. "ms") terminalTree = Solyd.render(terminalTree, Terminal { configState = configState, products = products, shopState = shopState, peripherals = peripherals, logs = logs, terminalState = terminalState, modal = {}}) local terminalContext = Solyd.getTopologicalContext(terminalTree, { "canvas", "aabb", "input" }) diffCanvasStack(terminal, terminalContext.canvas, lastCanvases[2]) t1 = os.epoch("utc") local cstackT = { {terminal.bgCanvas, 1, 1}, unpack(terminalContext.canvas) } -- cstack[#cstack+1] = {display.textCanvas, 1, 1} terminal.ccCanvas:composite(unpack(cstackT)) terminal.ccCanvas:outputDirty(terminal.mon) t2 = os.epoch("utc") -- print("Render time: " .. (t2-t1) .. "ms") local activeNode = hooks.findActiveInput(terminalContext.input) if activeNode then if activeNode.inputState.cursorX and activeNode.inputState.cursorY then terminal.mon.setCursorPos(activeNode.inputState.cursorX, activeNode.inputState.cursorY) else terminal.mon.setCursorPos(activeNode.x, activeNode.y) end terminal.mon.setTextColor(colors.black) terminal.mon.setCursorBlink(true) else terminal.mon.setCursorBlink(false) end local receivedEvent = false local terminate = false while not receivedEvent do local e = { os.pullEvent() } receivedEvent = true local name = e[1] if name == "timer" and e[2] == deltaTimer then -- local clock = os.epoch("utc") -- local dt = (clock - lastClock)/1000 -- t = t + dt -- lastClock = clock -- deltaTimer = os.startTimer(0) -- hooks.tickAnimations(dt) elseif name == "timer" then receivedEvent = false elseif name == "monitor_touch" and e[2] == peripheral.getName(display.mon) then local x, y = e[3], e[4] local node = hooks.findNodeAt(context.aabb, x, y) if node then node.onClick() end elseif name == "term_resize" then terminal.ccCanvas:outputFlush(terminal.mon) elseif name == "mouse_click" then local x, y = e[3], e[4] local clearedInput = hooks.clearActiveInput(terminalContext.input, x, y) if clearedInput and clearedInput.onBlur then clearedInput.onBlur() end local node = hooks.findNodeAt(terminalContext.aabb, x, y) if node then node.onClick() end elseif name == "mouse_scroll" then local dir = e[2] local x, y = e[3], e[4] local node = hooks.findNodeAt(terminalContext.aabb, x, y) local cancelScroll = false if node and node.onScroll then if node.onScroll(dir) then cancelScroll = true end end if not cancelScroll then if dir >= 1 and terminalState.scroll < terminalState.maxScroll then terminalState.scroll = math.min(terminalState.scroll + dir, terminalState.maxScroll) elseif dir <= -1 and terminalState.scroll > 0 then terminalState.scroll = math.max(terminalState.scroll + dir, 0) end end elseif name == "char" then local char = e[2] local node = hooks.findActiveInput(terminalContext.input) if node then node.onChar(char) end elseif name == "key" then --if e[2] == keys.q then -- break --end local key, held = e[2] or 0, e[3] or false local node = hooks.findActiveInput(terminalContext.input) if node then node.onKey(key, held) end elseif name == "paste" then local contents = e[2] local node = hooks.findActiveInput(terminalContext.input) if node and node.onPaste then node.onPaste(contents) end elseif name == "terminate" then terminate = true break end end if terminate then break end end ---Profiler:deactivate() end) end) display.mon.clear() terminal.mon.setBackgroundColor(colors.black) terminal.mon.setTextColor(colors.white) terminal.mon.clear() terminal.mon.setCursorPos(1,1) for i = 1, #shopState.config.currencies do local currency = shopState.config.currencies[i] if (currency.krypton and currency.krypton.ws) then currency.krypton.ws:disconnect() end end os.pullEvent = oldPullEvent if not success then if eventHooks and eventHooks.programError then eventHook.execute(eventHooks.programError, err) end error(err) end print("Radon terminated, goodbye!") --Profiler:write_results(nil, "profile.txt")