· 4 years ago · Jun 05, 2021, 07:54 PM
1local data = { [ ".settings" ] = { data = "{\n [ \"shell.disk_startup\" ] = false,\n}", }, app = { [ "button.lua" ] = { data = "---@class button\n---@field buttons table\nbutton = {\n buttons = {}\n};\n\nfunction button.print(text, color, fun, parameters, group)\n local oldColor = term.getTextColor();\n term.setTextColor(color);\n\n local oldX, oldY = term.getCursorPos();\n term.write('[' .. text .. ']');\n local newX, newY = term.getCursorPos();\n\n term.setTextColor(oldColor);\n\n table.insert(button.buttons, {\n x1 = oldX,\n y1 = oldY,\n x2 = newX,\n y2 = newY,\n fun = fun,\n parameters = parameters or {},\n group = group or nil\n });\n\nend\n\nfunction button.call(x, y)\n for key, item in pairs(button.buttons) do\n if x >= item.x1 and x <= item.x2 and y >= item.y1 and y <= item.y2 then\n item.fun(table.unpack(item.parameters));\n end\n end\nend\n\nfunction button.clear(group)\n if group then\n for key, item in pairs(button.buttons) do\n button.buttons[key] = nil;\n end\n end\n\n button.buttons = {};\nend", }, [ "shop.lua" ] = { data = "\r\n---@shape shopData\r\n---@field inventoryInput inventory\r\n---@field inventoryOutput inventory\r\n---@field inventoryStorage inventory\r\n---@field inventoryStorageMoney inventory\r\n\r\n\r\n---@class shop\r\n---@field data shopData\r\n---@field overviewLine number\r\n---@field currentMoney number\r\nshop = {\r\n data = {},\r\n overviewLine = nil,\r\n currentMoney = 0\r\n};\r\n\r\nfunction shop.load()\r\n if not fs.exists('/disk/shop.json') then\r\n term.clear();\r\n term.setCursorPos(1, 1);\r\n print('Please mount a drive with shop.json. Press enter to reboot...');\r\n while not confirm() do end\r\n os.reboot();\r\n end\r\n\r\n local file = io.open('/disk/shop.json');\r\n local json = file:read('*all');\r\n local data = textutils.unserialiseJSON(json);\r\n\r\n data.inventoryInput = peripheral.inventory(data.inventoryInput);\r\n data.inventoryOutput = peripheral.inventory(data.inventoryOutput);\r\n data.inventoryStorage = peripheral.inventory(data.inventoryStorage);\r\n data.inventoryStorageMoney = peripheral.inventory(data.inventoryStorageMoney);\r\n shop.data = data;\r\n\r\nend\r\n\r\nfunction shop.text()\r\n return shop.data.text;\r\nend\r\n\r\nfunction shop.renderOverview(line)\r\n line = line or shop.overviewLine;\r\n term.setCursorPos(1, line);\r\n term.clearLine();\r\n\r\n local oldColor = term.getTextColor();\r\n term.setTextColor(colors.blue);\r\n term.write('Cost: ');\r\n term.setTextColor(colors.red);\r\n term.write(1000);\r\n term.setTextColor(colors.blue);\r\n term.write(' coins. Your money: ');\r\n term.write(shop.countMoney());\r\n term.write(' coins');\r\n term.setTextColor(oldColor);\r\n shop.overviewLine = line;\r\nend\r\n\r\nfunction shop.countMoney()\r\n local money = 0;\r\n\r\n local items = shop.data.inventoryInput:list();\r\n for slot, item in pairs(items) do\r\n if item.name == 'kubejs:money_coin' then\r\n money = money + (item.count);\r\n elseif item.name == 'kubejs:money_dollar' then\r\n money = money + (item.count * 8);\r\n elseif item.name == 'kubejs:money_bag' then\r\n money = money + (item.count * 64);\r\n end\r\n end\r\n\r\n shop.currentMoney = money;\r\n return money;\r\nend", }, [ "config.lua" ] = { data = "PRODUCTION = false;\n\nAUTO_SHUTDOWN = true;\nAUTO_SHUTDOWN_IDLE_TIME = 2;", }, [ "emu.lua" ] = { data = "ccemux.attach(\"top\", \"disk_drive\", { id = 0 });\n\nmockInventory.attach('back', 32, {\n [1] = {\n name = \"minecraft:iron_ingot\",\n tags = {\n [ \"forge:ingots\" ] = true,\n [ \"forge:ingots/iron\" ] = true,\n [ \"minecraft:beacon_payment_items\" ] = true,\n },\n count = 64,\n maxCount = 64,\n displayName = \"Iron Ingot\",\n },\n [2] = {\n durability = 0.128,\n maxDamage = 250,\n nbt = \"d5072b2ac645f1a139163d9357fb9c8e\",\n displayName = \"Iron Sword\",\n name = \"minecraft:iron_sword\",\n tags = {\n [ \"forge:tools/iron\" ] = true,\n },\n maxCount = 1,\n count = 1,\n damage = 32,\n }\n});\n\n\nmockInventory.attach('left', 64, {\n [1] = {\n name = \"kubejs:money_coin\",\n tags = {},\n count = 2,\n maxCount = 64,\n displayName = \"Throbcoin\",\n },\n [5] = {\n name = \"kubejs:money_dollar\",\n tags = {},\n count = 2,\n maxCount = 64,\n displayName = \"Throbdollar\",\n },\n [10] = {\n name = \"kubejs:money_bag\",\n tags = {},\n count = 2,\n maxCount = 64,\n displayName = \"Throbbag\",\n },\n});\n\nmockInventory.attach('right', 64, {\n});\n\nmockInventory.attach('bottom', 64, {\n});", }, [ "main.lua" ] = { data = "require('/app/button');\nrequire('/app/shop');\n\nfunction test(para)\n print('test: ' .. para);\nend\n\nfunction render()\n term.clear();\n button.clear();\n\n local screenX, screenY = term.getSize();\n\n term.setCursorPos(1, screenY);\n button.print('Click me', colors.orange, test, {\"wow\"}, 'test');\n button.print('Re-render', colors.blue, function()\n render();\n end);\n button.print('Count money', colors.blue, shop.renderOverview);\n term.setCursorPos(1, 1);\n\n print(shop.text());\n\n local cursorX, cursorY = term.getCursorPos();\n\n shop.renderOverview(cursorY);\nend\n\n\n--- This function loops on it's own\nfunction main()\n local event, _, x, y = os.pullEvent('mouse_click');\n\n if event == 'mouse_click' then\n button.call(x, y);\n end\nend\n\n--- This function is run once\nfunction init()\n shop.load();\n\n render();\nend\n\n", }, }, lib = { mock = { [ "mockInventory.lua" ] = { data = "---@class mockInventory\nmockInventory = {\n name = '',\n data = {\n size = 0,\n items = {}\n }\n};\nmockInventory.__index = mockInventory;\n\nfunction mockInventory.attach(name, size, items)\n mockPeripherals[name] = {\n type = 'inventory',\n size = size,\n items = items\n };\nend\n\nfunction mockInventory.get(name)\n if not mockPeripherals[name] then\n error('Peripheral \"' .. name .. '\" does not exist');\n end\n if not mockPeripherals[name].type == 'inventory' then\n error('Peripheral \"' .. name .. '\" is not an inventory peripheral');\n end\n\n local self = setmetatable({}, mockInventory);\n self.name = name;\n self.data = mockPeripherals[name];\n self.data.type = 'test';\n return self;\nend\n\nfunction mockInventory:size()\n return self.data.size;\nend\n\nfunction mockInventory:list()\n local list = {};\n\n for slot, item in pairs(self.data.items) do\n list[slot] = {\n name = item.name,\n count = item.count\n };\n\n if item.nbt then\n list[slot].nbt = item.nbt;\n end\n end\n\n return list;\nend\n\nfunction mockInventory:getItemDetail(slot)\n if self.data.items[slot] then\n return self.data.items[slot];\n end\n\n return nil;\nend\n\nfunction mockInventory:getItemLimit(slot)\n if self.data.items[slot] then\n return self.data.items[slot].maxCount;\n end\n\n return nil;\nend\n\nfunction mockInventory:pushItems(toName, fromSlot, --[[optional]]limit, --[[optional]]toSlot)\n local toInv = mockInventory.get(toName);\n\n if not self.data.items[fromSlot] then\n return 0;\n end\n\n local slot;\n if toSlot then\n slot = toSlot;\n\n if slot > toInv:size() then\n error('toSlot out of bounds');\n end\n\n if toInv:getItemDetail(slot) then\n error('Slot already in use');\n end\n end\n\n local count = self.data.items[fromSlot].count;\n\n if limit and count > limit then\n count = limit;\n\n local copy = deepcopy(self.data.items[fromSlot]);\n copy.count = count;\n\n if slot then\n toInv.data.items[slot] = copy;\n else\n table.insert(toInv.data.items, copy);\n end\n self.data.items[fromSlot].count = self.data.items[fromSlot].count - count;\n return count;\n end\n\n --- TODO stack items\n\n if slot then\n toInv.data.items[slot] = self.data.items[fromSlot];\n else\n table.insert(toInv.data.items, self.data.items[fromSlot]);\n end\n\n self.data.items[fromSlot] = nil;\n\n return count;\nend\n\nfunction mockInventory:pullItems(fromName, fromSlot, --[[optional]]limit, --[[optional]]toSlot)\n local fromInv = mockInventory.get(fromName);\n fromInv:pushItems(self.name, fromSlot, limit, toSlot);\nend", }, }, [ "startup.lua" ] = { data = "\r\n\r\nfunction run()\r\n ---@type boolean\r\n adminPromptOn = false;\r\n\r\n local function start()\r\n require('/lib/load');\r\n\r\n if ccemux then\r\n require('/app/emu');\r\n end\r\n\r\n require('/app/main');\r\n init();\r\n\r\n while true do\r\n local returnValue = main();\r\n if returnValue == false then\r\n return;\r\n end\r\n end\r\n end\r\n\r\n local function secretExitCodeDetector()\r\n local secretExitCode = {\r\n keys.p,\r\n keys.a,\r\n keys.s,\r\n keys.s,\r\n }\r\n\r\n secretExitCode.n = #secretExitCode\r\n local current = 1\r\n\r\n while true do\r\n local _, key = os.pullEventRaw(\"key\")\r\n\r\n if key == secretExitCode[current] then\r\n current = current + 1\r\n else\r\n current = 1\r\n end\r\n\r\n if current > secretExitCode.n then\r\n adminPromptOn = true;\r\n term.clear();\r\n term.setCursorPos(1, 1);\r\n print('Enter the admin password to exit:');\r\n sleep(0.1);\r\n if read('*') == 'secret' then\r\n return;\r\n end\r\n adminPromptOn = false;\r\n error('Wrong password');\r\n end\r\n end\r\n end\r\n\r\n local function errorOccurred()\r\n term.clear();\r\n term.setCursorPos(1, 1);\r\n print('An error has occurred! Press enter to reboot...');\r\n while true do\r\n local _, key = os.pullEventRaw(\"key\");\r\n if adminPromptOn == false and key == keys.enter then\r\n os.reboot();\r\n end\r\n end\r\n end\r\n\r\n if PRODUCTION then\r\n while true do\r\n local isSuccess, msg = pcall(parallel.waitForAny, start, secretExitCodeDetector);\r\n msg = msg or '';\r\n\r\n if isSuccess then\r\n return;\r\n elseif not string.find(msg, 'Wrong password') then\r\n while true do\r\n parallel.waitForAny(errorOccurred, secretExitCodeDetector)\r\n if adminPromptOn then\r\n return;\r\n end\r\n end\r\n end\r\n end\r\n else\r\n parallel.waitForAny(start, secretExitCodeDetector);\r\n end\r\nend\r\n\r\nif PRODUCTION then\r\n local oldPullEvent = os.pullEvent;\r\n os.pullEvent = os.pullEventRaw;\r\n\r\n run();\r\n\r\n os.pullEvent = oldPullEvent;\r\nelse\r\n run();\r\nend", }, [ "peripheral.lua" ] = { data = "mockPeripherals = {};\n\n---@return inventory\nfunction peripheral.inventory(name)\n if ccemux then\n return mockInventory.get(name);\n end\n\n local handle = peripheral.wrap(name);\n if not handle.getItemDetail then\n error('Peripheral with name \"' .. name .. '\" is not an inventory!');\n end\n return handle;\nend\n\nperipheral.oldGetName = peripheral.getName;\nfunction peripheral.getName(handle)\n if handle.name then\n return handle.name;\n end\n\n return peripheral.oldGetName(handle);\nend", }, [ "load.lua" ] = { data = "require('/lib/helpers');\nrequire('/lib/peripheral');\n\nif ccemux then\n require('/lib/mock/mockInventory');\nend", }, [ "helpers.lua" ] = { data = "function deepcopy(orig)\n local orig_type = type(orig)\n local copy\n if orig_type == 'table' then\n copy = {}\n for orig_key, orig_value in next, orig, nil do\n copy[deepcopy(orig_key)] = deepcopy(orig_value)\n end\n setmetatable(copy, deepcopy(getmetatable(orig)))\n else\n copy = orig\n end\n return copy\nend\n\nfunction confirm()\n local event, key = os.pullEventRaw(\"key\");\n if event == 'key' and adminPromptOn == false and key == keys.enter then\n return true;\n end\n return false;\nend", }, }, [ "cloud.lua" ] = { data = "package.preload[\"argparse\"] = function(...)\n local function errorf(msg, ...)\n error(msg:format(...), 0)\n end\n local function setter(arg, result, value)\n result[arg.name] = value or true\n end\n local parser = { __name = \"ArgParser\" }\n parser.__index = parser\n function parser:add(names, arg)\n if type(names) == \"string\" then names = { names } end\n arg.names = names\n for i = 1, #names do\n local name = names[i]\n if name:sub(1, 2) == \"--\" then self.options[name:sub(3)] = arg\n elseif name:sub(1, 1) == \"-\" then self.flags[name:sub(2)] = arg\n else self.arguments[#self.arguments + 1] = arg; arg.argument = true end\n end\n table.insert(self.list, #self.list, arg)\n if arg.action == nil then arg.action = setter end\n if arg.required == nil then arg.required = names[1]:sub(1, 1) ~= \"-\" end\n if arg.name == nil then arg.name = names[1]:gsub(\"^-+\", \"\") end\n if arg.mvar == nil then arg.mvar = arg.name:upper() end\n end\n function parser:parse(...)\n local args = table.pack(...)\n local i, n = 1, #args\n local arg_idx = 1\n local result = {}\n while i <= n do\n local arg = args[i]\n i = i + 1\n if arg:find(\"^%-%-([^=]+)=(.+)$\") then\n local name, value = arg:match(\"^%-%-([^=]+)=(.+)$\")\n local arg = self.options[name]\n if not arg then errorf(\"Unknown argument %q\", name) end\n if not arg.many and result[arg.name] ~= nil then errorf(\"%s has already been set\", name) end\n if not arg.value then errorf(\"%s does not accept a value\", name) end\n arg:action(result, value)\n elseif arg:find(\"^%-%-(.*)$\") then\n local name = arg:match(\"^%-%-(.*)$\")\n local arg = self.options[name]\n if not arg then errorf(\"Unknown argument %q\", name) end\n if not arg.many and result[arg.name] ~= nil then errorf(\"%s has already been set\", name) end\n if arg.value then\n local value = args[i]\n i = i + 1\n if not value then errorf(\"%s needs a value\", name) end\n arg:action(result, value)\n else\n arg:action(result)\n end\n elseif arg:find(\"^%-(.+)$\") then\n local flags = arg:match(\"^%-(.+)$\")\n for j = 1, #flags do\n local name = flags:sub(j, j)\n local arg = self.flags[name]\n if not arg then errorf(\"Unknown argument %q\", name) end\n if not arg.many and result[arg.name] ~= nil then errorf(\"%s has already been set\", name) end\n if arg.value then\n local value\n if j == #flags then\n value = args[i]\n i = i + 1\n else\n value = flags:sub(j + 1)\n end\n if not value then errorf(\"%s expects a value\", name) end\n arg:action(result, value)\n break\n else\n arg:action(result)\n end\n end\n else\n local argument = self.arguments[arg_idx]\n if argument then\n argument:action(result, arg)\n arg_idx = arg_idx + 1\n else\n errorf(\"Unexpected argument %q\", arg)\n end\n end\n end\n for i = 1, #self.list do\n local arg = self.list[i]\n if arg and arg.required and result[arg.name] == nil then\n errorf(\"%s is required (use -h to see usage)\", arg.name)\n end\n end\n return result\n end\n local function get_usage(arg)\n local name\n if arg.argument then name = arg.mvar\n elseif arg.value then name = arg.names[1] .. \"=\" .. arg.mvar\n else name = arg.names[1]\n end\n if #arg.names > 1 then name = name .. \",\" .. table.concat(arg.names, \",\", 2) end\n return name\n end\n local function create(prefix)\n local parser = setmetatable({\n options = {},\n flags = {},\n arguments = {},\n list = {},\n }, parser)\n parser:add({ \"-h\", \"--help\", \"-?\" }, {\n value = false, required = false,\n doc = \"Show this help message\",\n action = function()\n if prefix then print(prefix) print() end\n print(\"USAGE\")\n local max = 0\n for i = 1, #parser.list do max = math.max(max, #get_usage(parser.list[i])) end\n local format = \" %-\" .. max .. \"s %s\"\n for i = 1, #parser.list do\n local arg = parser.list[i]\n print(format:format(get_usage(arg), arg.doc or \"\"))\n end\n error(\"\", 0)\n end,\n })\n return parser\n end\n local function is_help(cmd)\n return cmd == \"help\" or cmd == \"--help\" or cmd == \"-h\" or cmd == \"-?\"\n end\n return { create = create, is_help = is_help }\nend\npackage.preload[\"framebuffer\"] = function(...)\n local stringify = require(\"json\").stringify\n local colour_lookup = {}\n for i = 0, 15 do\n colour_lookup[2 ^ i] = string.format(\"%x\", i)\n end\n local void = function() end\n local function empty(colour, width, height)\n local function is_colour() return colour end\n return {\n write = void, blit = void, clear = void, clearLine = void,\n setCursorPos = void, setCursorBlink = void,\n setPaletteColour = void, setPaletteColor = void,\n setTextColour = void, setTextColor = void, setBackgroundColour = void, setBackgroundColor = void,\n getTextColour = void, getTextColor = void, getBackgroundColour = void, getBackgroundColor = void,\n scroll = void,\n isColour = is_colour, isColor = is_colour,\n getSize = function() return width, height end,\n getPaletteColour = term.native().getPaletteColour, getPaletteColor = term.native().getPaletteColor,\n }\n end\n local function buffer(original)\n local text = {}\n local text_colour = {}\n local back_colour = {}\n local palette = {}\n local palette_24 = {}\n local cursor_x, cursor_y = 1, 1\n local cursor_blink = false\n local cur_text_colour = \"0\"\n local cur_back_colour = \"f\"\n local sizeX, sizeY = original.getSize()\n local color = original.isColor()\n local dirty = false\n local redirect = {}\n if original.getPaletteColour then\n for i = 0, 15 do\n local c = 2 ^ i\n palette[c] = { original.getPaletteColour( c ) }\n palette_24[colour_lookup[c]] = colours.rgb8(original.getPaletteColour( c ))\n end\n end\n function redirect.write(writeText)\n writeText = tostring(writeText)\n original.write(writeText)\n dirty = true\n if cursor_y > sizeY or cursor_y < 1 or cursor_x + #writeText <= 1 or cursor_x > sizeX then\n cursor_x = cursor_x + #writeText\n return\n end\n if cursor_x < 1 then\n writeText = writeText:sub(-cursor_x + 2)\n cursor_x = 1\n elseif cursor_x + #writeText > sizeX then\n writeText = writeText:sub(1, sizeX - cursor_x + 1)\n end\n local lineText = text[cursor_y]\n local lineColor = text_colour[cursor_y]\n local lineBack = back_colour[cursor_y]\n local preStop = cursor_x - 1\n local preStart = math.min(1, preStop)\n local postStart = cursor_x + #writeText\n local postStop = sizeX\n local sub, rep = string.sub, string.rep\n text[cursor_y] = sub(lineText, preStart, preStop)..writeText..sub(lineText, postStart, postStop)\n text_colour[cursor_y] = sub(lineColor, preStart, preStop)..rep(cur_text_colour, #writeText)..sub(lineColor, postStart, postStop)\n back_colour[cursor_y] = sub(lineBack, preStart, preStop)..rep(cur_back_colour, #writeText)..sub(lineBack, postStart, postStop)\n cursor_x = cursor_x + #writeText\n end\n function redirect.blit(writeText, writeFore, writeBack)\n original.blit(writeText, writeFore, writeBack)\n dirty = true\n if cursor_y > sizeY or cursor_y < 1 or cursor_x + #writeText <= 1 or cursor_x > sizeX then\n cursor_x = cursor_x + #writeText\n return\n end\n if cursor_x < 1 then\n writeText = writeText:sub(-cursor_x + 2)\n writeFore = writeFore:sub(-cursor_x + 2)\n writeBack = writeBack:sub(-cursor_x + 2)\n cursor_x = 1\n elseif cursor_x + #writeText > sizeX then\n writeText = writeText:sub(1, sizeX - cursor_x + 1)\n writeFore = writeFore:sub(1, sizeX - cursor_x + 1)\n writeBack = writeBack:sub(1, sizeX - cursor_x + 1)\n end\n local lineText = text[cursor_y]\n local lineColor = text_colour[cursor_y]\n local lineBack = back_colour[cursor_y]\n local preStop = cursor_x - 1\n local preStart = math.min(1, preStop)\n local postStart = cursor_x + #writeText\n local postStop = sizeX\n local sub = string.sub\n text[cursor_y] = sub(lineText, preStart, preStop)..writeText..sub(lineText, postStart, postStop)\n text_colour[cursor_y] = sub(lineColor, preStart, preStop)..writeFore..sub(lineColor, postStart, postStop)\n back_colour[cursor_y] = sub(lineBack, preStart, preStop)..writeBack..sub(lineBack, postStart, postStop)\n cursor_x = cursor_x + #writeText\n end\n function redirect.clear()\n for i = 1, sizeY do\n text[i] = string.rep(\" \", sizeX)\n text_colour[i] = string.rep(cur_text_colour, sizeX)\n back_colour[i] = string.rep(cur_back_colour, sizeX)\n end\n dirty = true\n return original.clear()\n end\n function redirect.clearLine()\n if cursor_y > sizeY or cursor_y < 1 then\n return\n end\n text[cursor_y] = string.rep(\" \", sizeX)\n text_colour[cursor_y] = string.rep(cur_text_colour, sizeX)\n back_colour[cursor_y] = string.rep(cur_back_colour, sizeX)\n dirty = true\n return original.clearLine()\n end\n function redirect.getCursorPos()\n return cursor_x, cursor_y\n end\n function redirect.setCursorPos(x, y)\n if type(x) ~= \"number\" then error(\"bad argument #1 (expected number, got \" .. type(x) .. \")\", 2) end\n if type(y) ~= \"number\" then error(\"bad argument #2 (expected number, got \" .. type(y) .. \")\", 2) end\n if x ~= cursor_x or y ~= cursor_y then\n cursor_x = math.floor(x)\n cursor_y = math.floor(y)\n dirty = true\n end\n return original.setCursorPos(x, y)\n end\n function redirect.setCursorBlink(b)\n if type(b) ~= \"boolean\" then error(\"bad argument #1 (expected boolean, got \" .. type(b) .. \")\", 2) end\n if cursor_blink ~= b then\n cursor_blink = b\n dirty = true\n end\n return original.setCursorBlink(b)\n end\n function redirect.getSize()\n return sizeX, sizeY\n end\n function redirect.scroll(n)\n if type(n) ~= \"number\" then error(\"bad argument #1 (expected number, got \" .. type(n) .. \")\", 2) end\n local empty_text = string.rep(\" \", sizeX)\n local empty_text_colour = string.rep(cur_text_colour, sizeX)\n local empty_back_colour = string.rep(cur_back_colour, sizeX)\n if n > 0 then\n for i = 1, sizeY do\n text[i] = text[i + n] or empty_text\n text_colour[i] = text_colour[i + n] or empty_text_colour\n back_colour[i] = back_colour[i + n] or empty_back_colour\n end\n elseif n < 0 then\n for i = sizeY, 1, -1 do\n text[i] = text[i + n] or empty_text\n text_colour[i] = text_colour[i + n] or empty_text_colour\n back_colour[i] = back_colour[i + n] or empty_back_colour\n end\n end\n dirty = true\n return original.scroll(n)\n end\n function redirect.setTextColour(clr)\n if type(clr) ~= \"number\" then error(\"bad argument #1 (expected number, got \" .. type(clr) .. \")\", 2) end\n local new_colour = colour_lookup[clr] or error(\"Invalid colour (got \" .. clr .. \")\" , 2)\n if new_colour ~= cur_text_colour then\n dirty = true\n cur_text_colour = new_colour\n end\n return original.setTextColour(clr)\n end\n redirect.setTextColor = redirect.setTextColour\n function redirect.setBackgroundColour(clr)\n if type(clr) ~= \"number\" then error(\"bad argument #1 (expected number, got \" .. type(clr) .. \")\", 2) end\n local new_colour = colour_lookup[clr] or error(\"Invalid colour (got \" .. clr .. \")\" , 2)\n if new_colour ~= cur_back_colour then\n dirty = true\n cur_back_colour = new_colour\n end\n return original.setBackgroundColour(clr)\n end\n redirect.setBackgroundColor = redirect.setBackgroundColour\n function redirect.isColour()\n return color == true\n end\n redirect.isColor = redirect.isColour\n function redirect.getTextColour()\n return 2 ^ tonumber(cur_text_colour, 16)\n end\n redirect.getTextColor = redirect.getTextColour\n function redirect.getBackgroundColour()\n return 2 ^ tonumber(cur_back_colour, 16)\n end\n redirect.getBackgroundColor = redirect.getBackgroundColour\n if original.getPaletteColour then\n function redirect.setPaletteColour(colour, r, g, b)\n local palcol = palette[colour]\n if not palcol then error(\"Invalid colour (got \" .. tostring(colour) .. \")\", 2) end\n if type(r) == \"number\" and g == nil and b == nil then\n palcol[1], palcol[2], palcol[3] = colours.rgb8(r)\n palette_24[colour_lookup[colour]] = r\n else\n if type(r) ~= \"number\" then error(\"bad argument #2 (expected number, got \" .. type(r) .. \")\", 2) end\n if type(g) ~= \"number\" then error(\"bad argument #3 (expected number, got \" .. type(g) .. \")\", 2) end\n if type(b) ~= \"number\" then error(\"bad argument #4 (expected number, got \" .. type(b ) .. \")\", 2 ) end\n palcol[1], palcol[2], palcol[3] = r, g, b\n palette_24[colour_lookup[colour]] = colours.rgb8(r, g, b)\n end\n dirty = true\n return original.setPaletteColour(colour, r, g, b)\n end\n redirect.setPaletteColor = redirect.setPaletteColour\n function redirect.getPaletteColour(colour)\n local palcol = palette[colour]\n if not palcol then error(\"Invalid colour (got \" .. tostring(colour) .. \")\", 2) end\n return palcol[1], palcol[2], palcol[3]\n end\n redirect.getPaletteColor = redirect.getPaletteColour\n end\n function redirect.is_dirty() return dirty end\n function redirect.clear_dirty() dirty = false end\n function redirect.serialise()\n return stringify {\n packet = 0x10,\n width = sizeX, height = sizeY,\n cursorX = cursor_x, cursorY = cursor_y, cursorBlink = cursor_blink,\n curFore = cur_text_colour, curBack = cur_back_colour,\n palette = palette_24,\n text = text, fore = text_colour, back = back_colour\n }\n end\n redirect.setCursorPos(1, 1)\n redirect.setBackgroundColor(colours.black)\n redirect.setTextColor(colours.white)\n redirect.clear()\n return redirect\n end\n return { buffer = buffer, empty = empty }\nend\npackage.preload[\"encode\"] = function(...)\n local function fletcher_32(str)\n local s1, s2, byte = 0, 0, string.byte\n if #str % 2 ~= 0 then str = str .. \"\\0\" end\n for i = 1, #str, 2 do\n local c1, c2 = byte(str, i, i + 1)\n s1 = (s1 + c1 + (c2 * 0x100)) % 0xFFFF\n s2 = (s2 + s1) % 0xFFFF\n end\n return s2 * 0x10000 + s1\n end\n return {\n fletcher_32 = fletcher_32\n }\nend\npackage.preload[\"json\"] = function(...)\n local tonumber = tonumber\n local function skip_delim(str, pos, delim, err_if_missing)\n pos = pos + #str:match('^%s*', pos)\n if str:sub(pos, pos) ~= delim then\n if err_if_missing then error('Expected ' .. delim) end\n return pos, false\n end\n return pos + 1, true\n end\n local esc_map = { b = '\\b', f = '\\f', n = '\\n', r = '\\r', t = '\\t' }\n local function parse_str_val(str, pos)\n local out, n = {}, 0\n if pos > #str then error(\"Malformed JSON (in string)\") end\n while true do\n local c = str:sub(pos, pos)\n if c == '\"' then return table.concat(out, \"\", 1, n), pos + 1 end\n n = n + 1\n if c == '\\\\' then\n local nextc = str:sub(pos + 1, pos + 1)\n if not nextc then error(\"Malformed JSON (in string)\") end\n if nextc == \"u\" then\n local num = tonumber(str:sub(pos + 2, pos + 5), 16)\n if not num then error(\"Malformed JSON (in unicode string) \") end\n if num <= 255 then\n pos, out[n] = pos + 6, string.char(num)\n else\n pos, out[n] = pos + 6, \"?\"\n end\n else\n pos, out[n] = pos + 2, esc_map[nextc] or nextc\n end\n else\n pos, out[n] = pos + 1, c\n end\n end\n end\n local function parse_num_val(str, pos)\n local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos)\n local val = tonumber(num_str)\n if not val then error('Error parsing number at position ' .. pos .. '.') end\n return val, pos + #num_str\n end\n local null = {}\n local literals = {['true'] = true, ['false'] = false, ['null'] = null }\n local escapes = {}\n for i = 0, 255 do\n local c = string.char(i)\n if i >= 32 and i <= 126\n then escapes[c] = c\n else escapes[c] = (\"\\\\u00%02x\"):format(i)\n end\n end\n escapes[\"\\t\"], escapes[\"\\n\"], escapes[\"\\r\"], escapes[\"\\\"\"], escapes[\"\\\\\"] = \"\\\\t\", \"\\\\n\", \"\\\\r\", \"\\\\\\\"\", \"\\\\\\\\\"\n local function parse(str, pos, end_delim)\n pos = pos or 1\n if pos > #str then error('Reached unexpected end of input.') end\n local pos = pos + #str:match('^%s*', pos)\n local first = str:sub(pos, pos)\n if first == '{' then\n local obj, key, delim_found = {}, true, true\n pos = pos + 1\n while true do\n key, pos = parse(str, pos, '}')\n if key == nil then return obj, pos end\n if not delim_found then error('Comma missing between object items.') end\n pos = skip_delim(str, pos, ':', true)\n obj[key], pos = parse(str, pos)\n pos, delim_found = skip_delim(str, pos, ',')\n end\n elseif first == '[' then\n local arr, val, delim_found = {}, true, true\n pos = pos + 1\n while true do\n val, pos = parse(str, pos, ']')\n if val == nil then return arr, pos end\n if not delim_found then error('Comma missing between array items.') end\n arr[#arr + 1] = val\n pos, delim_found = skip_delim(str, pos, ',')\n end\n elseif first == '\"' then\n return parse_str_val(str, pos + 1)\n elseif first == '-' or first:match('%d') then\n return parse_num_val(str, pos)\n elseif first == end_delim then\n return nil, pos + 1\n else\n for lit_str, lit_val in pairs(literals) do\n local lit_end = pos + #lit_str - 1\n if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end\n end\n local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10)\n error('Invalid json syntax starting at ' .. pos_info_str)\n end\n end\n local format, gsub, tostring, pairs, next, type, concat\n = string.format, string.gsub, tostring, pairs, next, type, table.concat\n local function stringify_impl(t, out, n)\n local ty = type(t)\n if ty == \"table\" then\n local first_ty = type(next(t))\n if first_ty == \"nil\" then\n out[n], n = \"{}\", n + 1\n return n\n elseif first_ty == \"string\" then\n out[n], n = \"{\", n + 1\n local first = true\n for k, v in pairs(t) do\n if first then first = false else out[n], n = \",\", n + 1 end\n out[n] = format(\"\\\"%s\\\":\", k)\n n = stringify_impl(v, out, n + 1)\n end\n out[n], n = \"}\", n + 1\n return n\n elseif first_ty == \"number\" then\n out[n], n = \"[\", n + 1\n for i = 1, #t do\n if i > 1 then out[n], n = \",\", n + 1 end\n n = stringify_impl(t[i], out, n)\n end\n out[n], n = \"]\", n + 1\n return n\n else\n error(\"Cannot serialize key \" .. first_ty)\n end\n elseif ty == \"string\" then\n if t:match(\"^[ -~]*$\") then\n out[n], n = gsub(format(\"%q\", t), \"\\n\", \"n\"), n + 1\n else\n out[n], n = \"\\\"\" .. gsub(t, \".\", escapes) .. \"\\\"\", n + 1\n end\n return n\n elseif ty == \"number\" or ty == \"boolean\" then\n out[n],n = tostring(t), n + 1\n return n\n else error(\"Cannot serialize type \" .. ty)\n end\n end\n local function stringify(object)\n local buffer = {}\n local n = stringify_impl(object, buffer, 1)\n return concat(buffer, \"\", 1, n - 1)\n end\n local function try_parse(msg)\n local ok, res = pcall(parse, msg)\n if ok then return res else return nil, res end\n end\n return {\n stringify = stringify,\n try_parse = try_parse,\n parse = parse,\n null = null\n }\nend\nlocal tonumber, type, keys = tonumber, type, keys\nlocal argparse = require \"argparse\"\nlocal framebuffer = require \"framebuffer\"\nlocal encode = require \"encode\"\nlocal json = require \"json\"\nif _G.cloud_catcher then\n local usage = ([[\n cloud: <subcommand> [args]\n Communicate with the cloud-catcher session.\n Subcommands:\n edit <file> Open a file on the remote server.\n token Display the token for this\n connection.\n ]]):gsub(\"^%s+\", \"\"):gsub(\"%s+$\", \"\"):gsub(\"\\n \", \"\\n\")\n local subcommand = ...\n if subcommand == \"edit\" or subcommand == \"e\" then\n local arguments = argparse.create(\"cloud edit: Edit a file in the remote viewer\")\n arguments:add({ \"file\" }, { doc = \"The file to upload\", required = true })\n local result = arguments:parse(select(2, ...))\n local file = result.file\n local resolved = shell.resolve(file)\n if not fs.exists(resolved) and not resolved:find(\"%.\") then\n local extension = settings.get(\"edit.default_extension\", \"\")\n if extension ~= \"\" and type(extension) == \"string\" then\n resolved = resolved .. \".\" .. extension\n end\n end\n if fs.isDir(resolved) then error((\"%q is a directory\"):format(file), 0) end\n if fs.isReadOnly(resolved) then\n if fs.exists(resolved) then\n print((\"%q is read only, will not be able to modify\"):format(file))\n else\n error((\"%q does not exist\"):format(file), 0)\n end\n end\n local ok, err = _G.cloud_catcher.edit(resolved)\n if not ok then error(err, 0) end\n elseif subcommand == \"token\" or subcommand == \"t\" then\n print(_G.cloud_catcher.token())\n elseif argparse.is_help(subcommand) then\n print(usage)\n elseif subcommand == nil then\n printError(usage)\n error()\n else\n error((\"%q is not a cloud catcher subcommand, run with --h for more info\"):format(subcommand), 0)\n end\n return\nend\nlocal current_path = shell.getRunningProgram()\nlocal current_name = fs.getName(current_path)\nlocal arguments = argparse.create(current_name .. \": Interact with this computer remotely\")\narguments:add({ \"token\" }, { doc = \"The token to use when connecting\" })\narguments:add({ \"--term\", \"-t\" }, { value = true, doc = \"Terminal dimensions or none to hide\" })\narguments:add({ \"--dir\", \"-d\" }, { value = true, doc = \"The directory to sync to. Defaults to the current one.\" })\narguments:add({ \"--http\", \"-H\" }, { value = false, doc = \"Use HTTP instead of HTTPs\" })\nlocal args = arguments:parse(...)\nlocal token = args.token\nif #token ~= 32 or token:find(\"[^%a%d]\") then\n error(\"Invalid token (must be 32 alpha-numeric characters)\", 0)\nend\nlocal capabilities = {}\nlocal term_opts = args.term\nlocal previous_term, parent_term = term.current()\nif term_opts == nil then\n parent_term = previous_term\nelse if term_opts == \"none\" then\n parent_term = nil\nelseif term_opts == \"hide\" then\n parent_term = framebuffer.empty(true, term.getSize())\nelseif term_opts:find(\"^(%d+)x(%d+)$\") then\n local w, h = term_opts:match(\"^(%d+)x(%d+)$\")\n if w == 0 or h == 0 then error(\"Terminal cannot have 0 size\", 0) end\n parent_term = framebuffer.empty(true, tonumber(w), tonumber(h))\nelse\n error(\"Unknown format for term: expected \\\"none\\\", \\\"hide\\\" or \\\"wxh\\\"\", 0)\n end\nend\nif parent_term then\n table.insert(capabilities, \"terminal:host\")\n local w, h = parent_term.getSize()\n if w * h > 5000 then error(\"Terminal is too large to handle\", 0) end\nend\nlocal sync_dir = shell.resolve(args.dir or \"./\")\nif not fs.isDir(sync_dir) then error((\"%q is not a directory\"):format(sync_dir), 0) end\ntable.insert(capabilities, \"file:host\")\nlocal url = (\"%s://cloud-catcher.squiddev.cc/connect?id=%s&capabilities=%s\"):format(\n args.http and \"ws\" or \"wss\", token, table.concat(capabilities, \",\"))\nlocal remote, err = http.websocket(url)\nif not remote then error(\"Cannot connect to cloud-catcher server: \" .. err, 0) end\nlocal server_term, server_file_edit, server_file_host = false, false, false\ndo\n local max_packet_size = 16384\n _G.cloud_catcher = {\n token = function() return token end,\n edit = function(file, force)\n if not server_file_edit then\n return false, \"There are no editors connected\"\n end\n local contents, exists\n local handle = fs.open(file, \"rb\")\n if handle then\n contents = handle.readAll()\n handle.close()\n exists = true\n else\n contents = \"\"\n exists = false\n end\n if #file + #contents + 5 > max_packet_size then\n return false, \"This file is too large to be edited remotely\"\n end\n local check = encode.fletcher_32(contents)\n local flag = 0x04\n if fs.isReadOnly(file) then flag = flag + 0x01 end\n if not exists then flag = flag + 0x08 end\n remote.send(json.stringify {\n packet = 0x22,\n id = 0,\n actions = {\n { file = file, checksum = check, flags = flag, action = 0, contents = contents }\n }\n })\n return true\n end\n }\n shell.setAlias(\"cloud\", \"/\" .. current_path)\n local function complete_multi(text, options)\n local results = {}\n for i = 1, #options do\n local option, add_spaces = options[i][1], options[i][2]\n if #option + (add_spaces and 1 or 0) > #text and option:sub(1, #text) == text then\n local result = option:sub(#text + 1)\n if add_spaces then table.insert( results, result .. \" \" )\n else table.insert( results, result )\n end\n end\n end\n return results\n end\n local subcommands = { { \"edit\", true }, { \"token\", false } }\n shell.setCompletionFunction(current_path, function(shell, index, text, previous_text)\n if _G.cloud_catcher == nil then return end\n if index == 1 then\n return complete_multi(text, subcommands)\n elseif index == 2 and previous_text[2] == \"edit\" then\n return fs.complete(text, shell.dir(), true, false)\n end\n end)\nend\nlocal co, buffer\nif parent_term ~= nil then\n buffer = framebuffer.buffer(parent_term)\n co = coroutine.create(shell.run)\n term.redirect(buffer)\nend\nlocal info_dirty, last_label, get_label = true, nil, os.getComputerLabel\nlocal function send_info()\n last_label = get_label()\n info_dirty = false\n remote.send(json.stringify {\n packet = 0x12,\n id = os.getComputerID(),\n label = last_label,\n })\nend\nlocal ok, res = true\nif co then ok, res = coroutine.resume(co, \"shell\") end\nlocal last_change, last_timer = os.clock(), nil\nlocal pending_events, pending_n = {}, 0\nlocal function push_event(event)\n pending_n = pending_n + 1\n pending_events[pending_n] = event\nend\nwhile ok and (not co or coroutine.status(co) ~= \"dead\") do\n if not info_dirty and last_label ~= get_label() then info_dirty = true end\n if server_term and last_timer == nil and (buffer.is_dirty() or info_dirty) then\n local now = os.clock()\n if now - last_change < 0.04 then\n last_timer = os.startTimer(0)\n else\n last_change = os.clock()\n if buffer.is_dirty() then\n remote.send(buffer.serialise())\n buffer.clear_dirty()\n end\n if info_dirty then send_info() end\n end\n end\n local event\n if pending_n >= 1 then\n event = table.remove(pending_events, 1)\n pending_n = pending_n - 1\n else\n event = table.pack(coroutine.yield())\n end\n if event[1] == \"timer\" and event[2] == last_timer then\n last_timer = nil\n if server_term then\n last_change = os.clock()\n if buffer.is_dirty() then remote.send(buffer.serialise()) buffer.clear_dirty() end\n if info_dirty then send_info() end\n end\n elseif event[1] == \"websocket_closed\" and event[2] == url then\n ok, res = false, \"Connection lost\"\n remote = nil\n elseif event[1] == \"websocket_message\" and event[2] == url then\n local packet = json.try_parse(event[3])\n local code = packet and packet.packet\n if type(code) ~= \"number\" then code = - 1 end\n if code >= 0x00 and code < 0x10 then\n if code == 0x00 then -- ConnectionUpdate\n server_term, server_file_edit, server_file_host = false, false, false\n for _, cap in ipairs(packet.capabilities) do\n if cap == \"terminal:view\" and buffer ~= nil then\n server_term = true\n remote.send(buffer.serialise()) buffer.clear_dirty()\n send_info()\n last_change = os.clock()\n elseif cap == \"file:host\" then\n server_file_host = true\n elseif cap == \"file:edit\" then\n server_file_edit = true\n end\n end\n elseif code == 0x02 then -- ConnectionPing\n remote.send([[{\"packet\":2}]])\n end\n elseif server_term and code >= 0x10 and code < 0x20 then\n if code == 0x11 then -- TerminalEvents\n for _, event in ipairs(packet.events) do\n if event.name == \"cloud_catcher_key\" then\n local key = keys[event.args[1]]\n if type(key) == \"number\" then push_event { n = 3, \"key\", key, event.args[2] } end\n elseif event.name == \"cloud_catcher_key_up\" then\n local key = keys[event.args[1]]\n if type(key) == \"number\" then push_event { n = 2, \"key_up\", key } end\n else\n push_event(table.pack(event.name, table.unpack(event.args)))\n end\n end\n end\n elseif code >= 0x20 and code < 0x30 then\n if code == 0x22 then -- FileAction\n local result = {}\n for i, action in pairs(packet.actions) do\n local ok = bit32.band(action.flags, 0x1) == 1\n local expected_checksum = 0\n local handle = fs.open(action.file, \"rb\")\n if handle then\n local contents = handle.readAll()\n handle.close()\n expected_checksum = encode.fletcher_32(contents)\n end\n if not ok then\n ok = expected_checksum == 0 or action.checksum == expected_checksum\n end\n if not ok then\n result[i] = { file = action.file, checksum = expected_checksum, result = 2 }\n elseif action.action == 0x0 then -- Replace\n handle = fs.open(action.file, \"wb\")\n if handle then\n handle.write(action.contents)\n handle.close()\n result[i] = { file = action.file, checksum = encode.fletcher_32(action.contents), result = 1 }\n else\n result[i] = { file = action.file, checksum = expected_checksum, result = 3 }\n end\n elseif action.action == 0x1 then -- Patch\n handle = fs.open(action.file, \"rb\")\n if handle then\n local out, n = {}, 0\n for _, delta in pairs(action.delta) do\n if delta.kind == 0 then -- Same\n n = n + 1\n out[n] = handle.read(delta.length)\n elseif delta.kind == 1 then -- Added\n n = n + 1\n out[n] = delta.contents\n elseif delta.kind == 2 then -- Removed\n handle.read(delta.length)\n end\n end\n handle.close()\n handle = fs.open(action.file, \"wb\")\n if handle then\n local contents = table.concat(out)\n handle.write(contents)\n handle.close()\n result[i] = { file = action.file, checksum = encode.fletcher_32(contents), result = 1 }\n else\n result[i] = { file = action.file, checksum = expected_checksum, result = 3 }\n end\n else\n result[i] = { file = action.file, checksum = expected_checksum, result = 2 }\n end\n elseif action.action == 0x02 then -- Delete\n local ok = fs.delete(action.file)\n result[i] = { file = action.file, checksum = action.checksum, result = ok and 1 or 3 }\n end\n end\n remote.send(json.stringify {\n packet = 0x23,\n id = packet.id,\n files = result,\n })\n end\n end\n elseif res == nil or event[1] == res or event[1] == \"terminate\" then\n if co then\n ok, res = coroutine.resume(co, table.unpack(event, 1, event.n))\n elseif event[1] == \"terminate\" then\n ok, res = false, \"Terminated\"\n end\n end\nend\nterm.redirect(previous_term)\nif previous_term == parent_term then\n term.clear()\n term.setCursorPos(1, 1)\n if previous_term.endPrivateMode then previous_term.endPrivateMode() end\nend\n_G.cloud_catcher = nil\nshell.clearAlias(\"cloud\")\nshell.getCompletionInfo()[current_path] = nil\nif remote ~= nil then remote.close() end\nif not ok then error(res, 0) end\n", }, [ "startup.lua" ] = { data = "if not fs.exists('/app/config.lua') then\r\n error('Config file does not exist');\r\nend\r\n\r\nrequire('/app/config');\r\nrequire('/lib/startup');", }, [ "install.lua" ] = { data = "", }, bin = { [ "install-script" ] = { [ "installer.lua" ] = { data = "local function writeFile(filename, content)\r\n local file = io.open(filename, 'w');\r\n file:write(content);\r\n file:close();\r\nend\r\n\r\nlocal function install(basepath, files)\r\n for key, file in pairs(files) do\r\n if file.data then\r\n writeFile(basepath .. key, file.data);\r\n else\r\n install(basepath .. key .. '/', file);\r\n end\r\n end\r\nend\r\n\r\ninstall('/', data);", }, }, [ "genInstallScript.lua" ] = { data = "---@type table\r\nlocal args = table.pack(...);\r\n\r\nlocal excluded = {\r\n '/rom',\r\n '/disk',\r\n '/install.lua',\r\n '/app/emu.lua'\r\n};\r\n\r\nlocal autoPastebin = false;\r\n\r\nfor key, arg in pairs(args) do\r\n if arg == '--exclude-app' then\r\n table.insert(excluded, '/app');\r\n elseif arg == '--pastebin' then\r\n autoPastebin = true;\r\n end\r\nend\r\n\r\nlocal function processFile(basepath, name)\r\n local path = basepath .. '/' .. name;\r\n if basepath == '/' then\r\n path = basepath .. name;\r\n end\r\n\r\n local file = io.open(path);\r\n\r\n print(path);\r\n\r\n return {\r\n data = file:read('*all')\r\n };\r\nend\r\n\r\nfunction process(basepath, name)\r\n local path = basepath .. '/' .. name;\r\n if basepath == '/' then\r\n path = basepath .. name;\r\n end\r\n\r\n if not fs.isDir(path) then\r\n return processFile(basepath, name);\r\n end\r\n\r\n local files = fs.list(path);\r\n\r\n for key, file in pairs(files) do\r\n for k, exclude in pairs(excluded) do\r\n if path .. file == exclude then\r\n table.remove(files, key);\r\n end\r\n end\r\n end\r\n\r\n local fileData = {};\r\n\r\n for key, file in pairs(files) do\r\n fileData[file] = process(path, file);\r\n end\r\n\r\n return fileData;\r\nend\r\n\r\n\r\nlocal file = io.open('/install.lua', 'w');\r\nlocal content = textutils.serialise(process('', ''), { compact = true });\r\n\r\ncontent = content:gsub(\"\\\\\\n\", '\\\\n');\r\ncontent = content:gsub(\"\\n\", ' ');\r\n\r\ncontent = 'local data = ' .. content .. \"; \";\r\n\r\n\r\nlocal installer = io.open('/bin/install-script/installer.lua', 'r');\r\nlocal installerContent = installer:read('*all');\r\ninstaller:close();\r\n\r\ninstallerContent = installerContent:gsub('\\n', ' ');\r\ninstallerContent = installerContent:gsub(' +', ' ');\r\n\r\ncontent = content .. installerContent;\r\n\r\nfile:write(content);\r\nfile:flush();\r\n\r\nif autoPastebin then\r\n shell.run('pastebin', 'put', 'install.lua');\r\nend", }, }, }; local function writeFile(filename, content) local file = io.open(filename, 'w'); file:write(content); file:close(); end local function install(basepath, files) for key, file in pairs(files) do if file.data then writeFile(basepath .. key, file.data); else install(basepath .. key .. '/', file); end end end install('/', data);