diff --git a/apps/farm.lua b/apps/farm.lua deleted file mode 100644 index 088c412..0000000 --- a/apps/farm.lua +++ /dev/null @@ -1,38 +0,0 @@ -_G.requireInjector(_ENV) - -local Point = require('point') -local Util = require('util') - -local scanner = device['plethora:scanner'] - -local function scan() - local blocks = scanner.scan() - Util.filterInplace(blocks, function(v) - return v.name == 'minecraft:wheat' and - scanner.getBlockMeta(v.x, v.y, v.z).metadata == 7 - end) - - return blocks -end - -local function harvest(blocks) - Point.eachClosest(turtle.point, blocks, function(b) - Util.print(b) - turtle.goto(Point.above(b)) - turtle.digDown() - turtle.placeDown('minecraft:wheat_seeds') - end) -end - -turtle.reset() -local directions = { [5] = 2, [3] = 3, [4] = 0, [2] = 1, } -turtle.placeUp('minecraft:chest') -local _, bi = turtle.inspectUp() -turtle.digUp() -turtle.point.heading = directions[bi.metadata] - -while true do - local blocks = scan() - harvest(blocks) - os.sleep(10) -end \ No newline at end of file diff --git a/apps/logMonitor.lua b/apps/logMonitor.lua deleted file mode 100644 index df98c46..0000000 --- a/apps/logMonitor.lua +++ /dev/null @@ -1,126 +0,0 @@ -requireInjector(getfenv(1)) - -local Event = require('event') -local Message = require('message') -local UI = require('ui') -local Util = require('util') - -multishell.setTitle(multishell.getCurrent(), 'Log Monitor') - -if not device.wireless_modem then - error('Wireless modem is required') -end -device.wireless_modem.open(59998) - -local ids = { } -local messages = { } -local terminal = UI.term - -if device.openperipheral_bridge then - - UI.Glasses = require('glasses') - - terminal = UI.Glasses({ - x = 4, - y = 175, - height = 40, - width = 64, - textScale = .5, - backgroundOpacity = .65, - - }) -elseif device.monitor then - terminal = UI.Device({ - deviceType = 'monitor', - textScale = .5 - }) -end - ---[[-- ScrollingText --]]-- -UI.ScrollingText = class(UI.Window) -UI.ScrollingText.defaults = { - UIElement = 'ScrollingText', - backgroundColor = colors.black, - buffer = { }, -} -function UI.ScrollingText:appendLine(text) - if #self.buffer+1 >= self.height then - table.remove(self.buffer, 1) - end - table.insert(self.buffer, text) -end - -function UI.ScrollingText:clear() - self.buffer = { } - UI.Window.clear(self) -end - -function UI.ScrollingText:draw() - for k,text in ipairs(self.buffer) do - self:write(1, k, Util.widthify(text, self.width), self.backgroundColor) - end -end - -terminal:clear() - -function getClient(id) - if not ids[id] then - ids[id] = { - titleBar = UI.TitleBar({ title = 'ID: ' .. id, parent = terminal }), - scrollingText = UI.ScrollingText({ parent = terminal }) - } - local clientCount = Util.size(ids) - local clientHeight = math.floor((terminal.height - clientCount) / clientCount) - terminal:clear() - local y = 1 - for k,v in pairs(ids) do - v.titleBar.y = y - y = y + 1 - v.scrollingText.height = clientHeight - v.scrollingText.y = y - y = y + clientHeight - v.scrollingText:clear() - - v.titleBar:draw() - v.scrollingText:draw() - end - end - return ids[id] -end - -Event.on('logMessage', function() - local t = { } - while #messages > 0 do - local msg = messages[1] - table.remove(messages, 1) - local client = getClient(msg.id) - client.scrollingText:appendLine(string.format('%d %s', math.floor(os.clock()), msg.text)) - t[msg.id] = client - end - for _,client in pairs(t) do - client.scrollingText:draw() - end - terminal:sync() -end) - -Message.addHandler('log', function(h, id, msg) - table.insert(messages, { id = id, text = msg.contents }) - os.queueEvent('logMessage') -end) - -Event.on('monitor_touch', function() - terminal:reset() - ids = { } -end) - -Event.on('mouse_click', function() - terminal:reset() - ids = { } -end) - -Event.on('char', function() - Event.exitPullEvents() -end) - -Event.pullEvents(logWriter) -terminal:reset() diff --git a/apps/mirror.lua b/apps/mirror.lua deleted file mode 100644 index f2817d4..0000000 --- a/apps/mirror.lua +++ /dev/null @@ -1,28 +0,0 @@ -_G.requireInjector() - -local Terminal = require('terminal') - -local shell = _ENV.shell -local term = _G.term - -local args = { ... } -local mon = _G.device[table.remove(args, 1) or 'monitor'] -if not mon then - error('mirror: Invalid device') -end - -mon.clear() -mon.setTextScale(.5) -mon.setCursorPos(1, 1) - -local oterm = Terminal.copy(term.current()) -Terminal.mirror(term.current(), mon) - -term.current().getSize = mon.getSize - -if #args > 0 then - shell.run(unpack(args)) - Terminal.copy(oterm, term.current()) - - mon.setCursorBlink(false) -end diff --git a/apps/namedb.lua b/apps/namedb.lua deleted file mode 100644 index 7d5fac9..0000000 --- a/apps/namedb.lua +++ /dev/null @@ -1,50 +0,0 @@ -_G.requireInjector() - -local itemDB = require('itemDB') -local json = require('json') -local Util = require('util') - -local args = { ... } -local mod = args[1] or error('Syntax: namedb MOD') - ---[[ - "double_plant": { - "name": ["Sunflower", - "Lilac", - "Double Tallgrass", - "Large Fern", - "Rose Bush", - "Peony"], - }, ---]] - -local list = { } - -for _,v in pairs(itemDB.data) do - local t = Util.split(v.name, '(.-):') - - if t[1] == mod then - local name = t[2] - local damage = v.damage or 0 - local entry = list[name] - if not entry then - entry = { } - list[name] = entry - end - if not entry.name and damage == 0 then - entry.name = v.displayName - else - if not entry.name then - entry.name = { } - elseif type(entry.name) == 'string' then - entry.name = { entry.name } - end - while #entry.name < damage do - entry.name[#entry.name + 1] = '' - end - entry.name[damage + 1] = v.displayName - end - end -end - -json.encodeToFile(string.format('usr/etc/names/%s.json', mod), list) diff --git a/builder/.package b/builder/.package new file mode 100644 index 0000000..be49c2c --- /dev/null +++ b/builder/.package @@ -0,0 +1,9 @@ +{ + required = { + 'core', + }, + title = 'Schematic Builder', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/builder', + description = [[ Build structures from schematic files using a turtle or command computer. ]], + licence = 'MIT', +} diff --git a/apis/base64.lua b/builder/apis/base64.lua similarity index 100% rename from apis/base64.lua rename to builder/apis/base64.lua diff --git a/apis/builder/blocks.lua b/builder/apis/builder/blocks.lua similarity index 99% rename from apis/builder/blocks.lua rename to builder/apis/builder/blocks.lua index 0ab6fc8..a69f031 100644 --- a/apis/builder/blocks.lua +++ b/builder/apis/builder/blocks.lua @@ -10,7 +10,7 @@ local JSON = require('json') local blockDB = TableDB() function blockDB:load() - local blocks = JSON.decodeFromFile('usr/etc/names/minecraft.json') + local blocks = JSON.decodeFromFile('packages/core/etc/names/minecraft.json') if not blocks then error('Unable to read blocks.json') diff --git a/apis/builder/builder.lua b/builder/apis/builder/builder.lua similarity index 100% rename from apis/builder/builder.lua rename to builder/apis/builder/builder.lua diff --git a/apis/builder/commands.lua b/builder/apis/builder/commands.lua similarity index 100% rename from apis/builder/commands.lua rename to builder/apis/builder/commands.lua diff --git a/apis/builder/schematic.lua b/builder/apis/builder/schematic.lua similarity index 99% rename from apis/builder/schematic.lua rename to builder/apis/builder/schematic.lua index b530ee2..43228d2 100644 --- a/apis/builder/schematic.lua +++ b/builder/apis/builder/schematic.lua @@ -23,7 +23,7 @@ local gzipMagic = 0x1f8b local Spinner = class() function Spinner:init(args) local defaults = { - timeout = .095, + timeout = .075, c = os.clock(), spinIndex = 0, spinSymbols = { '-', '/', '|', '\\' } diff --git a/apis/builder/turtle.lua b/builder/apis/builder/turtle.lua similarity index 100% rename from apis/builder/turtle.lua rename to builder/apis/builder/turtle.lua diff --git a/apis/deflatelua.lua b/builder/apis/deflatelua.lua similarity index 100% rename from apis/deflatelua.lua rename to builder/apis/deflatelua.lua diff --git a/apps/base64dl.lua b/builder/base64dl.lua similarity index 100% rename from apps/base64dl.lua rename to builder/base64dl.lua diff --git a/apps/builder.lua b/builder/builder.lua similarity index 100% rename from apps/builder.lua rename to builder/builder.lua diff --git a/apps/supplier.lua b/builder/supplier.lua similarity index 100% rename from apps/supplier.lua rename to builder/supplier.lua diff --git a/core/.package b/core/.package new file mode 100644 index 0000000..7079717 --- /dev/null +++ b/core/.package @@ -0,0 +1,6 @@ +{ + title = 'Core apps and apis', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/core', + description = [[Provides common files used by Opus applications. Also includes various useful applications.]], + licence = 'MIT', +} diff --git a/apps/Appstore.lua b/core/Appstore.lua similarity index 100% rename from apps/Appstore.lua rename to core/Appstore.lua diff --git a/apps/Devices.lua b/core/Devices.lua similarity index 100% rename from apps/Devices.lua rename to core/Devices.lua diff --git a/apps/Events.lua b/core/Events.lua similarity index 100% rename from apps/Events.lua rename to core/Events.lua diff --git a/apps/Music.lua b/core/Music.lua similarity index 100% rename from apps/Music.lua rename to core/Music.lua diff --git a/apps/Script.lua b/core/Script.lua similarity index 99% rename from apps/Script.lua rename to core/Script.lua index 20c72f5..82e0b30 100644 --- a/apps/Script.lua +++ b/core/Script.lua @@ -11,8 +11,8 @@ local fs = _G.fs local os = _G.os local shell = _ENV.shell -local GROUPS_PATH = 'usr/groups' -local SCRIPTS_PATH = 'usr/etc/scripts' +local GROUPS_PATH = 'usr/config/groups' +local SCRIPTS_PATH = 'packages/core/etc/scripts' UI:configure('script', ...) diff --git a/apps/Turtles.lua b/core/Turtles.lua similarity index 94% rename from apps/Turtles.lua rename to core/Turtles.lua index a91fd32..2af92e3 100644 --- a/apps/Turtles.lua +++ b/core/Turtles.lua @@ -31,7 +31,7 @@ local options = { desc = 'Displays the options' }, } -local SCRIPTS_PATH = 'usr/etc/scripts' +local SCRIPTS_PATH = 'packages/core/etc/scripts' local nullTerm = Terminal.getNullTerm(term.current()) local socket @@ -43,7 +43,7 @@ local page = UI.Page { }, tabs = UI.Tabs { x = 1, y = 5, ey = -2, - scripts = UI.Grid { + scripts = UI.ScrollingGrid { tabTitle = 'Run', backgroundColor = UI.TabBar.defaults.selectedBackgroundColor, columns = { @@ -53,7 +53,7 @@ local page = UI.Page { sortColumn = 'label', autospace = true, }, - turtles = UI.Grid { + turtles = UI.ScrollingGrid { tabTitle = 'Select', backgroundColor = UI.TabBar.defaults.selectedBackgroundColor, columns = { @@ -66,7 +66,7 @@ local page = UI.Page { sortColumn = 'label', autospace = true, }, - inventory = UI.Grid { + inventory = UI.ScrollingGrid { backgroundColor = UI.TabBar.defaults.selectedBackgroundColor, tabTitle = 'Inv', columns = { @@ -78,7 +78,7 @@ local page = UI.Page { sortColumn = 'index', }, --[[ - policy = UI.Grid { + policy = UI.ScrollingGrid { tabTitle = 'Mod', backgroundColor = UI.TabBar.defaults.selectedBackgroundColor, columns = { @@ -196,7 +196,7 @@ function page.tabs.inventory:getRowTextColor(row, selected) if page.turtle and row.selected then return colors.yellow end - return UI.Grid.getRowTextColor(self, row, selected) + return UI.ScrollingGrid.getRowTextColor(self, row, selected) end function page.tabs.inventory:draw() @@ -217,7 +217,7 @@ function page.tabs.inventory:draw() end self:adjustWidth() self:update() - UI.Grid.draw(self) + UI.ScrollingGrid.draw(self) end function page.tabs.inventory:eventHandler(event) @@ -225,7 +225,7 @@ function page.tabs.inventory:eventHandler(event) local fn = string.format('turtle.select(%d)', event.selected.index) page:runFunction(fn) else - return UI.Grid.eventHandler(self, event) + return UI.ScrollingGrid.eventHandler(self, event) end return true end @@ -238,14 +238,14 @@ function page.tabs.scripts:draw() table.insert(self.values, { label = path, path = fs.combine(SCRIPTS_PATH, path) }) end self:update() - UI.Grid.draw(self) + UI.ScrollingGrid.draw(self) end function page.tabs.scripts:eventHandler(event) if event.type == 'grid_select' then page:runScript(event.selected.label) else - return UI.Grid.eventHandler(self, event) + return UI.ScrollingGrid.eventHandler(self, event) end return true end @@ -269,7 +269,7 @@ function page.tabs.turtles:draw() end end self:update() - UI.Grid.draw(self) + UI.ScrollingGrid.draw(self) end function page.tabs.turtles:eventHandler(event) @@ -283,7 +283,7 @@ function page.tabs.turtles:eventHandler(event) socket = nil end else - return UI.Grid.eventHandler(self, event) + return UI.ScrollingGrid.eventHandler(self, event) end return true end diff --git a/apis/chestAdapter.lua b/core/apis/chestAdapter.lua similarity index 99% rename from apis/chestAdapter.lua rename to core/apis/chestAdapter.lua index 71bffc9..ce879ba 100644 --- a/apis/chestAdapter.lua +++ b/core/apis/chestAdapter.lua @@ -110,7 +110,7 @@ function ChestAdapter:listItems(throttle) return items end else - debug(m) + _debug(m) end end diff --git a/apis/chestAdapter18.lua b/core/apis/chestAdapter18.lua similarity index 79% rename from apis/chestAdapter18.lua rename to core/apis/chestAdapter18.lua index a0829d1..71610b8 100644 --- a/apis/chestAdapter18.lua +++ b/core/apis/chestAdapter18.lua @@ -8,6 +8,7 @@ local ChestAdapter = class() function ChestAdapter:init(args) local defaults = { name = 'chest', + adapter = 'ChestAdapter18' } Util.merge(self, defaults) Util.merge(self, args) @@ -71,6 +72,16 @@ end -- provide a consolidated list of items function ChestAdapter:listItems(throttle) + for _ = 1, 5 do + local list = self:listItemsInternal(throttle) + if list then + return list + end + end + error('Error accessing inventory: ' .. self.direction) +end + +function ChestAdapter:listItemsInternal(throttle) local cache = { } local items = { } throttle = throttle or Util.throttle() @@ -99,10 +110,8 @@ function ChestAdapter:listItems(throttle) end itemDB:flush() - if not Util.empty(items) then - self.cache = cache - return items - end + self.cache = cache + return items end function ChestAdapter:getItemInfo(item) @@ -122,32 +131,35 @@ function ChestAdapter:getPercentUsed() end function ChestAdapter:provide(item, qty, slot, direction) - local s, m = pcall(function() + local total = 0 + + local _, m = pcall(function() local stacks = self.list() for key,stack in Util.rpairs(stacks) do if stack.name == item.name and - (not item.damage or stack.damage == item.damage) and - (not item.nbtHash or stack.nbtHash == item.nbtHash) then + stack.damage == item.damage and + stack.nbtHash == item.nbtHash then local amount = math.min(qty, stack.count) if amount > 0 then - self.pushItems(direction or self.direction, key, amount, slot) + amount = self.pushItems(direction or self.direction, key, amount, slot) end qty = qty - amount + total = total + amount if qty <= 0 then break end end end end) - return s, m + return total, m end -function ChestAdapter:extract(slot, qty, toSlot) - self.pushItems(self.direction, slot, qty, toSlot) +function ChestAdapter:extract(slot, qty, toSlot, direction) + return self.pushItems(direction or self.direction, slot, qty, toSlot) end -function ChestAdapter:insert(slot, qty, toSlot) - self.pullItems(self.direction, slot, qty, toSlot) +function ChestAdapter:insert(slot, qty, toSlot, direction) + return self.pullItems(direction or self.direction, slot, qty, toSlot) end return ChestAdapter diff --git a/apis/controllerAdapter.lua b/core/apis/controllerAdapter.lua similarity index 100% rename from apis/controllerAdapter.lua rename to core/apis/controllerAdapter.lua diff --git a/apis/inventoryAdapter.lua b/core/apis/inventoryAdapter.lua similarity index 100% rename from apis/inventoryAdapter.lua rename to core/apis/inventoryAdapter.lua diff --git a/apis/itemDB.lua b/core/apis/itemDB.lua similarity index 68% rename from apis/itemDB.lua rename to core/apis/itemDB.lua index 95bd135..a5adfa4 100644 --- a/apis/itemDB.lua +++ b/core/apis/itemDB.lua @@ -31,6 +31,7 @@ local function safeString(text) end function itemDB:makeKey(item) + if not item then error('itemDB:makeKey: item is required', 2) end return table.concat({ item.name, item.damage or '*', item.nbtHash }, ':') end @@ -51,6 +52,7 @@ function itemDB:splitKey(key, item) end function itemDB:get(key) + if not key then error('itemDB:get: key is required', 2) end if type(key) == 'string' then key = self:splitKey(key) end @@ -95,6 +97,16 @@ function itemDB:get(key) end end +local function formatTime(t) + local m = math.floor(t/60) + local s = t % 60 + if s < 10 then + s = '0' .. s + end + + return m .. ':' .. s +end + --[[ If the base item contains an NBT hash, then the NBT hash uniquely identifies this item. @@ -113,25 +125,60 @@ function itemDB:add(baseItem) nItem.maxCount = baseItem.maxCount nItem.maxDamage = baseItem.maxDamage - for k,item in pairs(self.data) do - if nItem.name == item.name and - nItem.displayName == item.displayName then + -- enchanted items + if baseItem.enchantments then + if nItem.name == 'minecraft:enchanted_book' then + nItem.displayName = 'Book: ' + else + nItem.displayName = nItem.displayName .. ': ' + end + for k, v in ipairs(baseItem.enchantments) do + if k > 1 then + nItem.displayName = nItem.displayName .. ', ' + end + nItem.displayName = nItem.displayName .. v.fullName + end - if nItem.nbtHash ~= item.nbtHash and nItem.damage ~= item.damage then - nItem.damage = '*' - nItem.nbtHash = nil - nItem.ignoreNBT = true - self.data[k] = nil - break - elseif nItem.damage ~= item.damage then - nItem.damage = '*' - self.data[k] = nil - break - elseif nItem.nbtHash ~= item.nbtHash then - nItem.nbtHash = nil - nItem.ignoreNBT = true - self.data[k] = nil - break + -- disks + elseif baseItem.media then + -- don't ignore nbt... as disks can be labeled + if baseItem.media.recordTitle then + nItem.displayName = nItem.displayName .. ': ' .. baseItem.media.recordTitle + end + + -- potions + elseif nItem.name == 'minecraft:potion' or nItem.name == 'minecraft:lingering_potion' then + if baseItem.effects then + local effect = baseItem.effects[1] + if effect.amplifier == 1 then + nItem.displayName = nItem.displayName .. ' II' + end + if effect.duration and effect.duration > 0 then + nItem.displayName = string.format('%s (%s)', nItem.displayName, formatTime(effect.duration)) + end + end + + else + for k,item in pairs(self.data) do + if nItem.name == item.name and + nItem.displayName == item.displayName then + + if nItem.nbtHash ~= item.nbtHash and nItem.damage ~= item.damage then + nItem.damage = '*' + nItem.nbtHash = nil + nItem.ignoreNBT = true + self.data[k] = nil + break + elseif nItem.damage ~= item.damage then + nItem.damage = '*' + self.data[k] = nil + break + elseif nItem.nbtHash ~= item.nbtHash then + nItem.nbtHash = nil + nItem.ignoreNBT = true + self.data[k] = nil + break + end end end end diff --git a/apis/meAdapter.lua b/core/apis/meAdapter.lua similarity index 99% rename from apis/meAdapter.lua rename to core/apis/meAdapter.lua index af7d1af..41d12bd 100644 --- a/apis/meAdapter.lua +++ b/core/apis/meAdapter.lua @@ -102,7 +102,7 @@ function MEAdapter:refresh() itemDB:flush() if not s and m then - debug(m) + _debug(m) end if s and not failed and hasItems and self.items and not Util.empty(self.items) then diff --git a/apis/meAdapter18.lua b/core/apis/meAdapter18.lua similarity index 100% rename from apis/meAdapter18.lua rename to core/apis/meAdapter18.lua diff --git a/apis/message.lua b/core/apis/message.lua similarity index 100% rename from apis/message.lua rename to core/apis/message.lua diff --git a/apis/nameDB.lua b/core/apis/nameDB.lua similarity index 95% rename from apis/nameDB.lua rename to core/apis/nameDB.lua index f21851e..c068c14 100644 --- a/apis/nameDB.lua +++ b/core/apis/nameDB.lua @@ -3,7 +3,7 @@ local TableDB = require('tableDB') local fs = _G.fs -local NAME_DIR = '/usr/etc/names' +local NAME_DIR = '/packages/core/etc/names' local nameDB = TableDB() diff --git a/apis/refinedAdapter.lua b/core/apis/refinedAdapter.lua similarity index 99% rename from apis/refinedAdapter.lua rename to core/apis/refinedAdapter.lua index 6739a74..b0d84c1 100644 --- a/apis/refinedAdapter.lua +++ b/core/apis/refinedAdapter.lua @@ -70,7 +70,7 @@ function RefinedAdapter:listItems(throttle) end) if not s and m then - debug(m) + _debug(m) end itemDB:flush() diff --git a/apis/tableDB.lua b/core/apis/tableDB.lua similarity index 100% rename from apis/tableDB.lua rename to core/apis/tableDB.lua diff --git a/apis/turtle/craft.lua b/core/apis/turtle/craft.lua similarity index 99% rename from apis/turtle/craft.lua rename to core/apis/turtle/craft.lua index 1b7b676..0d5c017 100644 --- a/apis/turtle/craft.lua +++ b/core/apis/turtle/craft.lua @@ -4,7 +4,7 @@ local Util = require('util') local fs = _G.fs local turtle = _G.turtle -local RECIPES_DIR = 'usr/etc/recipes' +local RECIPES_DIR = 'packages/core/etc/recipes' local USER_RECIPES = 'usr/config/recipes.db' local Craft = { } diff --git a/apis/turtle/crafting.lua b/core/apis/turtle/crafting.lua similarity index 100% rename from apis/turtle/crafting.lua rename to core/apis/turtle/crafting.lua diff --git a/apis/turtle/home.lua b/core/apis/turtle/home.lua similarity index 100% rename from apis/turtle/home.lua rename to core/apis/turtle/home.lua diff --git a/apis/turtle/level.lua b/core/apis/turtle/level.lua similarity index 100% rename from apis/turtle/level.lua rename to core/apis/turtle/level.lua diff --git a/core/debugMonitor.lua b/core/debugMonitor.lua new file mode 100644 index 0000000..6b9b34f --- /dev/null +++ b/core/debugMonitor.lua @@ -0,0 +1,33 @@ +_G.requireInjector(_ENV) + +local Util = require('util') + +local device = _G.device +local os = _G.os +local term = _G.term + +local args = { ... } +local mon = device[args[1] or 'monitor'] or error('Syntax: debug ') + +mon.clear() +mon.setTextScale(.5) +mon.setCursorPos(1, 1) + +local oldDebug = _G._debug + +_G._debug = function(...) + local oldTerm = term.redirect(mon) + Util.print(...) + term.redirect(oldTerm) +end + +repeat + local e, side = os.pullEventRaw('monitor_touch') + if e == 'monitor_touch' and side == mon.side then + mon.clear() + mon.setTextScale(.5) + mon.setCursorPos(1, 1) + end +until e == 'terminate' + +_G._debug = oldDebug diff --git a/apps/edit.lua b/core/edit.lua similarity index 100% rename from apps/edit.lua rename to core/edit.lua diff --git a/etc/apps/opus-apps.db b/core/etc/apps/opus-apps.db similarity index 53% rename from etc/apps/opus-apps.db rename to core/etc/apps/opus-apps.db index ee5ea7d..450301f 100644 --- a/etc/apps/opus-apps.db +++ b/core/etc/apps/opus-apps.db @@ -1,37 +1,52 @@ { +--[[ +Needs work [ "90ef98d4b6fd15466f0a1f212ec1db8d9ebe018c" ] = { title = "Turtles", category = "Apps", icon = " \0305 \030c \0305 \030 \ \030d \030c \0305 \030c \0308 \030d\031f\"\ \0308\031f.\030 \031 \0308\031f.\030 \031 ", + iconExt = "\030 \031f\030f\031f\128\0305\135\030c\031c\128\128\0305\031f\139\0307\149\0308\0317\143\0307\128\ +\030 \031f\030c\031f\145\031c\128\030d\132\136\030c\128\0307\149\0318\143\133\ +\030 \031f\030f\031f\128\0317\143\031f\128\128\0317\143\031f\128\128\128", run = "Turtles.lua", }, [ "381e3298b2b8f6caeb2208b57d805ada38402f0b" ] = { + title = "Scripts", category = "Apps", icon = "\0300\0317if\031 \0307 \ \0300\0317turt\ \0300\0317retu", - title = "Scripts", + iconExt = "\0300\0317if\140\140\140\ +\0300\0317\140\031fthen\ +\0300\0317else\140", run = "Script.lua", }, +]] + c5497bca58468ae64aed6c0fd921109217988db3 = { + title = "Events", + category = "System", + icon = "\0304\031f \030 \0311e\ +\030f\031f \0304 \030 \0311ee\031f \ +\030f\031f \0304 \030 \0311e\031f ", + iconExt = "\0300\031f\159\135\030f\0310\156\0301\031f\159\030f\0311\144\0300\031f\147\139\030f\0310\144\ +\0300\128\128\030f\149\0311\157\142\0300\031f\149\0310\128\128\ +\130\139\141\0311\130\131\0310\142\135\129", + run = "Events.lua", + }, [ "7ef35cac539f84722b0a988caee03b2df734c56a" ] = { title = "AppStore", category = "System", icon = "\030 \0310=\0300 \030 XX\0300\031f \030 \ \030 \031f \0300 \030 \ \030 \031f \0310o \031f \0310o\031f ", + iconExt = "\031e\139\0318\151\151\151\151\151\149\ +\030 \031 \0308\031f\136\136\136\136\030f\0318\135\ +\030 \031 \030f\0317\130\030 \031 \030f\0317\130", run = "Appstore.lua", }, - [ "4486006f811b88cacd5f211fd579717e29b600cd" ] = { - title = "Miner", - category = "Apps", - icon = " \0315\\\030 \031 \ - \0304\031f _ \030 \031c/\0315\\\ - \0304 ", - run = "simpleMiner.lua", - requires = 'turtle', - }, +--[[ [ "131260cbfbb0c821f8eae5e7c3c296c7aa4d50b9" ] = { title = "Music", category = "Apps", @@ -42,15 +57,6 @@ run = "usr/apps/Music.lua", requires = 'turtle', }, ---[[ - [ "81c0d915fa6d82fd30661c5e66e204cea52bb2b5" ] = { - title = "Activity", - category = "Apps", - icon = "\0318/\030f\031 \030 \0318\\\ -\030f \0308\0319o\030f\031 \ -\0318\\\030f\031 \030 \0318/", - run = "storageActivity.lua", - }, [ "89307d419a2fe4fbb69af92b3d3af27b6ec14d3e" ] = { title = "Telnet", category = "Apps", @@ -67,33 +73,6 @@ \031e\\/\031 \0319c", run = "vnc.lua", }, - [ "8d59207c8a84153b3e9f035cc3b6ec7a23671323" ] = { - title = "Micropaint", - category = "Apps", - icon = "\030 \031f \030f \ -\030 \031f^ \0300 \030f \ -\030 \031fv \0300 \030 ", - run = "http://pastebin.com/raw/tMRzJXx2", - requires = "advancedComputer", - }, ---]] - [ "9e092dda4f0e27d0c7686ddd00272079e678b6e6" ] = { - title = "Storage", - category = "Apps", - icon = "\0307 \ -\0307 \0308\0311 \0305 \0308\031 \0307 \0308 \0301 \ -\0307 ", - run = "chestManager.lua", - requires = 'turtle', - }, - [ "114edfc04a1ab03541bdc80ce064f66a7cfcedbb" ] = { - title = "Recorder", - category = "Apps", - icon = "\030 \031f \031b \031foo \ -\030 \031f \030e\031b \030 \031f/\ -\030 \031b \030e \030 \031f\\", - run = "recorder.lua", - }, [ "131f0b008f44298812221d120d982940609be781" ] = { title = "Builder", category = "Apps", @@ -109,13 +88,28 @@ run = "http://pastebin.com/raw/VXAyXqBv", requires = "turtle", }, - [ "53a5d150062b1e03206b9e15854b81060e3c7552" ] = { - title = "Minesweeper", - category = "Games", - icon = "\030f\031f \03131\0308\031f \030f\031d2\ -\030f\031f \031d2\03131\0308\031f \030f\03131\ -\030f\03131\0308\031f \030f\03131\031e3", - run = "https://pastebin.com/raw/nsKrHTbN", +--]] + df485c871329671f46570634d63216761441bcd6 = { + title = "Devices", + category = "System", + icon = "\0304 \030 \ +\030f \0304 \0307 \030 \031 \031f_\ +\030f \0304 \0307 \030 \031f/", + iconExt = "\031f\128\128\128\0308\159\143\0300\0317\151\0307\0310\140\148\ +\0314\151\131\0304\031f\148\030f\0318\138\148\0307\0310\138\131\129\ +\0304\031f\138\143\133\030f\0318\131\129\031f\128\128\128", + run = "Devices.lua", + }, + [ "114edfc04a1ab03541bdc80ce064f66a7cfcedbb" ] = { + title = "Recorder", + category = "Apps", + icon = "\030 \031f \031b \031foo \ +\030 \031f \030e\031b \030 \031f/\ +\030 \031b \030e \030 \031f\\", + iconExt = "\030 \031f\030f\031f\128\030e\143\030f\031e\144\031f\128\0304\149\0307\0314\131\131\030f\149\ +\030 \031f\030e\031f\129\031e\128\128\030f\148\0304\031f\149\0307\0318\140\140\030f\0314\149\ +\030 \031f\030f\031e\139\030e\128\030f\159\129\0314\130\131\131\129", + run = "recorder.lua", }, [ "a2accffe95b2c8be30e8a05e0c6ab7e8f5966f43" ] = { title = "Strafe", @@ -123,39 +117,20 @@ icon = "\0308\031f \0300 \0308 \ \0308\031f \0300 \030f \ \0300\031f \030f ", + iconExt = "\0308\0318\128\0300\159\129\0310\128\0308\159\129\0318\128\ +\0300\0318\135\0310\128\128\030f\135\0300\031f\143\159\030f\0310\144\ +\0300\128\030f\159\129\138\0300\031f\143\149\030f\0310\134", run = "https://pastebin.com/raw/jyDH7mLH", }, - [ "48d6857f6b2869d031f463b13aa34df47e18c548" ] = { - title = "Breakout", - category = "Games", - icon = "\0301\031f \0309 \030c \030b \030e \030c \0306 \ -\030 \031f \ -\030 \031f \0300 \0310 ", - run = "https://gist.github.com/LDDestroier/c7528d95bc0103545c2a/raw", - }, - [ "d78f28759f255a0db76604ee560b87c4715a0da5" ] = { - title = "Sketch", - category = "Apps", - icon = " \031bskch\ -\0303\031f \030d \ -\030d\031f ", - run = "http://pastebin.com/raw/Mm5hd97E", - }, [ "58ec8d6e36e346d9f42eb43935652e3e58e2c829" ] = { + title = "Mwm", category = "Apps", icon = "\030f\031f \0304 \ \030f\031dshell]\0304\0314 \ \0304\031f ", - title = "Mwm", + iconExt = "\030 \031f\0305\031f\155\030f\128\031d\152\140\030d\031f\151\030f\128\128\0304\0314\128\ +\030 \031f\030f\0315\152\129\030d\031f\141\030f\031d\153\030d\031f\149\030f\031d\131\148\0304\0314\128\ +\030 \031f\0304\031f\131\131\131\131\131\131\131\030e\0314\131", run = "mwm.lua usr/config/mwm", }, - [ "8d1b0a73bedc0dc492377c2f6ab880940b97ec6e" ] = { - icon = "\030 \031f \0305 \030 \030d \030 \ -\0305\031f \030d \030 \030d \0305 \030d \ -\030 \031f \030c \030 \0304 \030 \030c \030 ", - category = "Apps", - title = "Treefarm", - run = "treefarm.lua", - requires = "turtle", - }, } diff --git a/etc/names/minecraft.json b/core/etc/names/minecraft.json similarity index 100% rename from etc/names/minecraft.json rename to core/etc/names/minecraft.json diff --git a/etc/recipes/minecraft.db b/core/etc/recipes/minecraft.db similarity index 99% rename from etc/recipes/minecraft.db rename to core/etc/recipes/minecraft.db index 241889e..c5614d4 100644 --- a/etc/recipes/minecraft.db +++ b/core/etc/recipes/minecraft.db @@ -2223,6 +2223,12 @@ [ 6 ] = "minecraft:stone:0", }, }, + [ "minecraft:blaze_powder:0" ] = { + count = 2, + ingredients = { + "minecraft:blaze_rod:0", + }, + }, [ "minecraft:end_rod:0" ] = { count = 4, ingredients = { diff --git a/etc/scripts/abort b/core/etc/scripts/abort similarity index 100% rename from etc/scripts/abort rename to core/etc/scripts/abort diff --git a/etc/scripts/follow b/core/etc/scripts/follow similarity index 100% rename from etc/scripts/follow rename to core/etc/scripts/follow diff --git a/etc/scripts/goHome b/core/etc/scripts/goHome similarity index 100% rename from etc/scripts/goHome rename to core/etc/scripts/goHome diff --git a/etc/scripts/moveTo b/core/etc/scripts/moveTo similarity index 100% rename from etc/scripts/moveTo rename to core/etc/scripts/moveTo diff --git a/etc/scripts/obsidian b/core/etc/scripts/obsidian similarity index 100% rename from etc/scripts/obsidian rename to core/etc/scripts/obsidian diff --git a/etc/scripts/reboot b/core/etc/scripts/reboot similarity index 100% rename from etc/scripts/reboot rename to core/etc/scripts/reboot diff --git a/etc/scripts/setHome b/core/etc/scripts/setHome similarity index 100% rename from etc/scripts/setHome rename to core/etc/scripts/setHome diff --git a/etc/scripts/shutdown b/core/etc/scripts/shutdown similarity index 100% rename from etc/scripts/shutdown rename to core/etc/scripts/shutdown diff --git a/etc/scripts/summon b/core/etc/scripts/summon similarity index 100% rename from etc/scripts/summon rename to core/etc/scripts/summon diff --git a/core/lavaRefuel.lua b/core/lavaRefuel.lua new file mode 100644 index 0000000..3cb0653 --- /dev/null +++ b/core/lavaRefuel.lua @@ -0,0 +1,58 @@ +_G.requireInjector(_ENV) + +local Point = require('point') + +local device = _G.device +local turtle = _G.turtle + +local MAX_FUEL = turtle.getFuelLimit() + +local scanner = device['plethora:scanner'] or + turtle.equip('right', 'plethora:module:2') and device['plethora:scanner'] or + error('Plethora scanner required') + +if not turtle.select('minecraft:bucket') then + error('bucket required') +end + +local s, m = turtle.run(function() + turtle.setMovementStrategy('goto') + + local facing = scanner.getBlockMeta(0, 0, 0).state.facing + turtle.setPoint({ x = 0, y = 0, z = 0, heading = Point.facings[facing].heading }) + + local blocks = scanner.scan() + local first, last = blocks[#blocks].y, blocks[1].y + + for y = first, last, -1 do + if turtle.getFuelLevel() >= (MAX_FUEL - 1000) then + print('I am full') + break + end + local t = { } + for _,v in pairs(blocks) do + if v.y == y then + if (v.name == 'minecraft:lava' and v.metadata == 0) or + (v.name == 'minecraft:flowing_lava' and v.metadata == 0) then + table.insert(t, v) + end + end + end + print(y .. ': ' .. #t) + Point.eachClosest(turtle.point, t, function(b) + if turtle.getFuelLevel() >= (MAX_FUEL - 1000) then + return true + end + turtle.placeDownAt(b) + turtle.refuel() + print(turtle.getFuelLevel()) + end) + end +end) + +turtle.gotoY(0) +turtle._goto({ x = 0, y = 0, z = 0 }) + +if not s and m then + error(m) +end diff --git a/core/mirror.lua b/core/mirror.lua new file mode 100644 index 0000000..61a710e --- /dev/null +++ b/core/mirror.lua @@ -0,0 +1,52 @@ +_G.requireInjector() + +local Terminal = require('terminal') +local Util = require('util') + +local shell = _ENV.shell +local term = _G.term + +local options = { + scale = { arg = 's', type = 'flag', value = false, + desc = 'Set monitor to .5 text scaling' }, + resize = { arg = 'r', type = 'flag', value = false, + desc = 'Resize terminal to monitor size' }, + execute = { arg = 'e', type = 'string', + desc = 'Execute a program' }, + monitor = { arg = 'm', type = 'string', value = 'monitor', + desc = 'Name of monitor' }, + help = { arg = 'h', type = 'flag', value = false, + desc = 'Displays the options' }, +} + +local args = { ... } +if not Util.getOptions(options, args) then + return +end + +local mon = _G.device[options.monitor.value] +if not mon then + error('mirror: Invalid device') +end + +mon.clear() + +if options.scale.value then + mon.setTextScale(.5) +end +mon.setCursorPos(1, 1) + +local oterm = Terminal.copy(term.current()) +Terminal.mirror(term.current(), mon) + +if options.resize.value then + term.current().getSize = mon.getSize +end + +if options.execute.value then + -- TODO: allow args to be passed + shell.run(options.execute.value) -- unpack(args)) + Terminal.copy(oterm, term.current()) + + mon.setCursorBlink(false) +end diff --git a/apps/mirrorClient.lua b/core/mirrorClient.lua similarity index 100% rename from apps/mirrorClient.lua rename to core/mirrorClient.lua diff --git a/apps/mirrorHost.lua b/core/mirrorHost.lua similarity index 100% rename from apps/mirrorHost.lua rename to core/mirrorHost.lua diff --git a/apps/mwm.lua b/core/mwm.lua similarity index 99% rename from apps/mwm.lua rename to core/mwm.lua index 590879e..2858942 100644 --- a/apps/mwm.lua +++ b/core/mwm.lua @@ -28,8 +28,9 @@ local monitor local defaultEnv = Util.shallowCopy(_ENV) defaultEnv.multishell = multishell - -if args[2] then +if args[3] then + monitor = _G.device[args[3]] +elseif args[2] then monitor = peripheral.wrap(args[2]) or syntax() else monitor = peripheral.find('monitor') or syntax() diff --git a/apps/persist.lua b/core/persist.lua similarity index 100% rename from apps/persist.lua rename to core/persist.lua diff --git a/apps/recorder.lua b/core/recorder.lua similarity index 100% rename from apps/recorder.lua rename to core/recorder.lua diff --git a/apps/shapes.lua b/core/shapes.lua similarity index 100% rename from apps/shapes.lua rename to core/shapes.lua diff --git a/apps/t.lua b/core/t.lua similarity index 91% rename from apps/t.lua rename to core/t.lua index 49f2aa1..aef9b51 100644 --- a/apps/t.lua +++ b/core/t.lua @@ -1,4 +1,6 @@ -function doCommand(command, moves) +local turtle = _G.turtle + +local function doCommand(command, moves) local function format(value) if type(value) == 'boolean' then @@ -49,6 +51,8 @@ function doCommand(command, moves) [ 'r' ] = turtle.turnRight, [ 'l' ] = turtle.turnLeft, [ 'ta' ] = turtle.turnAround, + [ 'el' ] = turtle.equipLeft, + [ 'er' ] = turtle.equipRight, [ 'DD' ] = turtle.digDown, [ 'DU' ] = turtle.digUp, [ 'D' ] = turtle.dig, @@ -64,7 +68,7 @@ function doCommand(command, moves) if cmds[command] then runCommand(cmds[command], moves) elseif repCmds[command] then - for i = 1, moves do + for _ = 1, moves do if not runCommand(repCmds[command]) then break end @@ -79,7 +83,7 @@ if #args > 0 then else print('Enter command (q to quit):') while true do - local cmd = read() + local cmd = _G.read() if cmd == 'q' then break end args = { } diff --git a/apps/termShare.lua b/core/termShare.lua similarity index 100% rename from apps/termShare.lua rename to core/termShare.lua diff --git a/apps/trace.lua b/core/trace.lua similarity index 100% rename from apps/trace.lua rename to core/trace.lua diff --git a/farms/.package b/farms/.package new file mode 100644 index 0000000..191141a --- /dev/null +++ b/farms/.package @@ -0,0 +1,13 @@ +{ + required = { + 'core', + }, + title = 'Programs for farming resources', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/farms', + description = [[Includes: + * Tree Farm + * Cow/Sheep Rancher + * Farmer +]], + licence = 'MIT', +} diff --git a/farms/attack.lua b/farms/attack.lua new file mode 100644 index 0000000..f5a2ad0 --- /dev/null +++ b/farms/attack.lua @@ -0,0 +1,175 @@ +_G.requireInjector(_ENV) + +local Peripheral = require('peripheral') +local Point = require('point') +local Util = require('util') + +local device = _G.device +local os = _G.os +local turtle = _G.turtle + +local args = { ... } +local mobType = args[1] or error('Syntax: attack ') + +local chest -- a chest/dispenser that is accessible +local mobTypes = Util.transpose(args) + +local Runners = { + Cow = true, + Chicken = true, + Blaze = false, +} + +local function equip(side, item, rawName) + local equipped = Peripheral.lookup('side/' .. side) + + if equipped and equipped.type == item then + return true + end + + if not turtle.equip(side, rawName or item) then + if not turtle.selectSlotWithQuantity(0) then + error('No slots available') + end + turtle.equip(side) + if not turtle.equip(side, item) then + error('Unable to equip ' .. item) + end + end + + turtle.select(1) +end + +equip('left', 'minecraft:diamond_sword') + +equip('right', 'plethora:scanner', 'plethora:module:2') +local scanner = device['plethora:scanner'] +local facing = scanner.getBlockMeta(0, 0, 0).state.facing +turtle.point.heading = Point.facings[facing].heading + +equip('right', 'plethora:sensor', 'plethora:module:3') +local sensor = device['plethora:sensor'] + +turtle.setMovementStrategy('goto') +turtle.setPolicy(turtle.policies.attack) + +function Point.iterateClosest(spt, ipts) + local pts = Util.shallowCopy(ipts) + return function() + local pt = Point.closest(spt, pts) + if pt then + Util.removeByValue(pts, pt) + return pt + end + end +end + +local function findChests() + if chest then + return { chest } + end + equip('right', 'plethora:scanner', 'plethora:module:2') + local chests = scanner.scan() + equip('right', 'plethora:sensor', 'plethora:module:3') + + Util.filterInplace(chests, function(b) + if b.name == 'minecraft:chest' or + b.name == 'minecraft:dispenser' or + b.name == 'minecraft:hopper' then + b.x = Util.round(b.x) + turtle.point.x + b.y = Util.round(b.y) + turtle.point.y + b.z = Util.round(b.z) + turtle.point.z + return true + end + end) + return chests +end + +local function dropOff() + local inv = turtle.getSummedInventory() + for _, slot in pairs(inv) do + if slot.count >= 16 then + if turtle.getFuelLevel() < 1000 then + turtle.refuel(slot.name, 16) + end + end + end + + inv = turtle.getSummedInventory() + for _, slot in pairs(inv) do + if slot.count >= 16 then + local chests = findChests() + for c in Point.iterateClosest(turtle.point, chests) do + if turtle.dropDownAt(c, slot.name) then + chest = c + break + end + end + end + end +end + +local function normalize(b) + b.x = Util.round(b.x) + turtle.point.x + b.y = Util.round(b.y) + turtle.point.y + b.z = Util.round(b.z) + turtle.point.z +end + +while true do + local blocks = sensor.sense() + local mobs = Util.filterInplace(blocks, function(b) + if mobTypes[b.name] then + normalize(b) + return true + end + end) + + if turtle.getFuelLevel() == 0 then + error('Out of fuel') + end + + if #mobs == 0 then + os.sleep(3) + else + if Runners[mobType] then + -- if this mob runs away, just attack next closest + Point.eachClosest(turtle.point, mobs, function(b) + if turtle.faceAgainst(b) then + repeat until not turtle.attack() + end + end) + os.sleep(2) --- give a little time for mobs to calm down + else + local attacked = false + + local function attack() + if turtle.attack() then + attacked = true + return attacked + end + end + + for mob in Point.iterateClosest(turtle.point, mobs) do + -- this mob doesn't run, attack and follow until dead + if turtle.faceAgainst(mob) then + repeat + repeat until not turtle.attack() + mob = sensor.getMetaByID(mob.id) + if not mob or Util.empty(mob) then + break + end + normalize(mob) + if not turtle.faceAgainst(mob) then + break + end + until not mob + end + if attacked then + break + end + end + end + end + + dropOff() +end diff --git a/farms/etc/apps/apps.db b/farms/etc/apps/apps.db new file mode 100644 index 0000000..38806c4 --- /dev/null +++ b/farms/etc/apps/apps.db @@ -0,0 +1,23 @@ +{ + [ "8d1b0a73bedc0dc492377c2f6ab880940b97ec6e" ] = { + title = "Treefarm", + icon = "\030 \031f \0305 \030 \030d \030 \ +\0305\031f \030d \030 \030d \0305 \030d \ +\030 \031f \030c \030 \0304 \030 \030c \030 ", + category = "Farms", + run = "treefarm.lua", + requires = "turtle", + }, + [ "ff892e4e9538168021ccf2c7add8a46bf97fb180" ] = { + title = "Rancher", + category = "Farms", + run = "rancher.lua", + requires = "turtle", + }, + [ "e5c4e470299e30c9eea1e25de228c14db8c1fd40" ] = { + title = "Farmer", + category = "Farms", + run = "farmer.lua", + requires = "turtle", + }, +} diff --git a/farms/farmer.lua b/farms/farmer.lua new file mode 100644 index 0000000..1a94f74 --- /dev/null +++ b/farms/farmer.lua @@ -0,0 +1,226 @@ +_G.requireInjector(_ENV) + +local Point = require('point') +local Util = require('util') + +local device = _G.device +local fs = _G.fs +local os = _G.os +local peripheral = _G.peripheral +local turtle = _G.turtle + +local CONFIG_FILE = 'usr/config/farmer' +local STARTUP_FILE = 'usr/autorun/farmer.lua' + +local scanner = device['plethora:scanner'] or + turtle.equip('right', 'plethora:module:2') and device['plethora:scanner'] or + error('Plethora scanner required') + +local crops = Util.readTable(CONFIG_FILE) or { + ['minecraft:wheat'] = + { seed = 'minecraft:wheat_seeds', mature = 7, action = 'plant' }, + ['minecraft:carrots'] = + { seed = 'minecraft:carrot', mature = 7, action = 'plant' }, + ['minecraft:potatoes'] = + { seed = 'minecraft:potato', mature = 7, action = 'plant' }, + ['minecraft:beetroots'] = + { seed = 'minecraft:beetroot_seeds', mature = 3, action = 'plant' }, + ['minecraft:nether_wart'] = + { seed = 'minecraft:nether_wart', mature = 3, action = 'plant' }, + ['minecraft:cocoa'] = + { seed = 'minecraft:dye:3', mature = 8, action = 'pick' }, + ['minecraft:reeds'] = { action = 'bash' }, + ['minecraft:chorus_flower'] = { action = 'bash' }, + ['minecraft:chorus_plant'] = + { seed = 'minecraft:chorus_flower', mature = 0, action = 'bash-smash', }, + ['minecraft:melon_block'] = { action = 'smash' }, + ['minecraft:pumpkin'] = { action = 'smash' }, + ['minecraft:chest'] = { action = 'drop' }, + ['minecraft:cactus'] = { action = 'smash' }, +} + +if not fs.exists(CONFIG_FILE) then + Util.writeTable(CONFIG_FILE, crops) +end + +if not fs.exists(STARTUP_FILE) then + Util.writeFile(STARTUP_FILE, + [[os.sleep(1) +shell.openForegroundTab('farmer.lua')]]) + print('Autorun program created: ' .. STARTUP_FILE) +end + +local retain = Util.transpose { + "minecraft:diamond_pickaxe", + "plethora:module:2", + "plethora:module:3", +} + +for _, v in pairs(crops) do + if v.seed then + retain[v.seed] = true + end +end + +local function scan() + local blocks = scanner.scan() + local summed = turtle.getSummedInventory() + local doDropOff + + for _,v in pairs(summed) do + if v.count > 32 then + doDropOff = true + break + end + end + + Util.filterInplace(blocks, function(b) + b.action = crops[b.name] and crops[b.name].action + + if b.action == 'bash' then + return b.y == 0 + end + if b.action == 'drop' then + return doDropOff and b.y == -1 + end + if b.action == 'bash-smash' then + if b.y == -1 then + b.action = 'smash' + end + if b.y == 0 then + b.action = 'bash' + end + return b.action ~= 'bash-smash' + end + + if b.action == 'smash' then + return b.y == -1 + end + if b.action == 'pick' then + return b.y == 0 and b.state.age == 2 + end + if b.action == 'bump' then + return b.y == 0 + end + return b.action == 'plant' and + b.metadata == crops[b.name].mature and + b.y == -1 + end) + + local harvestCount = 0 + for _,b in pairs(blocks) do + b.x = b.x + turtle.point.x + b.y = b.y + turtle.point.y + b.z = b.z + turtle.point.z + if b.action ~= 'drop' then + harvestCount = harvestCount + 1 + end + end + + return blocks, harvestCount +end + +local function harvest(blocks) + turtle.equip('right', 'minecraft:diamond_pickaxe') + + local dropped + + Point.eachClosest(turtle.point, blocks, function(b) + turtle.select(1) + + if b.action == 'bash' then + turtle.digForwardAt(b) + + elseif b.action == 'drop' and not dropped then + if turtle._goto(Point.above(b)) then + turtle.eachFilledSlot(function(slot) + if not retain[slot.name] and not retain[slot.key] then + turtle.select(slot.index) + turtle.dropDown() + end + end) + local summed = turtle.getSummedInventory() + for k,v in pairs(summed) do + if v.count > 16 then + turtle.dropDown(k, v.count - 16) + end + end + + dropped = true + turtle.condense() + end + + elseif b.action == 'smash' then + if turtle.digDownAt(b) then + if crops[b.name].seed then + turtle.placeDown(crops[b.name].seed) + end + end + + elseif b.action == 'plant' then + if turtle.digDownAt(b) then + turtle.placeDown(crops[b.name].seed) + end + + elseif b.action == 'bump' then + if turtle.faceAgainst(b) then + turtle.equip('right', 'plethora:module:3') + os.sleep(.5) + -- search the ground for the dropped cactus + local sensed = peripheral.call('right', 'sense') + turtle.equip('right', 'minecraft:diamond_pickaxe') + Util.filterInplace(sensed, function(s) + if s.displayName == 'item.tile.cactus' then + s.x = Util.round(s.x) + turtle.point.x + s.z = Util.round(s.z) + turtle.point.z + s.y = -1 + if Point.distance(b, s) < 6 then + return true + end + end + end) + Point.eachClosest(turtle.point, sensed, function(s) + turtle.suckDownAt(s) + end) + end + + elseif b.action == 'pick' then + local h = Point.facings[b.state.facing].heading + local hi = Point.headings[(h + 2) % 4] -- opposite heading + + -- without pathfinding, will be unable to circle log + if turtle._goto({ x = b.x + hi.xd, z = b.z + hi.zd, heading = h }) then + if turtle.dig() then + turtle.place(crops[b.name].seed) + end + end + end + end) + turtle.equip('right', 'plethora:module:2') +end + +local s, m = turtle.run(function() + local facing = scanner.getBlockMeta(0, 0, 0).state.facing + turtle.point.heading = Point.facings[facing].heading + + print('Fuel: ' .. turtle.getFuelLevel()) + + --turtle.setPolicy('digOnly') + turtle.setMovementStrategy('goto') + repeat + local blocks, harvestCount = scan() + if harvestCount > 0 then + turtle.setStatus('Harvesting') + harvest(blocks) + turtle.setStatus('Sleeping') + end + os.sleep(10) + if turtle.getFuelLevel() < 10 then + error('Out of fuel') + end + until turtle.isAborted() +end) + +if not s and m then + error(m) +end diff --git a/farms/rancher.lua b/farms/rancher.lua new file mode 100644 index 0000000..3ff984b --- /dev/null +++ b/farms/rancher.lua @@ -0,0 +1,190 @@ +_G.requireInjector(_ENV) + +local Config = require('config') +local Util = require('util') +local Adapter = require('chestAdapter18') +local Peripheral = require('peripheral') + +local device = _G.device +local fs = _G.fs +local os = _G.os +local turtle = _G.turtle + +local STARTUP_FILE = 'usr/autorun/rancher.lua' + +local retain = Util.transpose { + 'minecraft:shears', + 'minecraft:wheat', + 'minecraft:diamond_sword', + 'plethora:module:3', +} +local config = { + animal = 'Cow', + max_animals = 15, +} +Config.load('rancher', config) + +local ANIMALS = { + Pig = { min = 0, food = 'minecraft:carrot' }, + Sheep = { min = .5, food = 'minecraft:wheat' }, + Cow = { min = .5, food = 'minecraft:wheat' }, +} + +local animal = ANIMALS[config.animal] + +local function equip(side, item, rawName) + local equipped = Peripheral.lookup('side/' .. side) + + if equipped and equipped.type == item then + return true + end + + if not turtle.equip(side, rawName or item) then + if not turtle.selectSlotWithQuantity(0) then + error('No slots available') + end + turtle.equip(side) + if not turtle.equip(side, item) then + error('Unable to equip ' .. item) + end + end + + turtle.select(1) +end + +local function getLocalName() + if not device.wired_modem then + error('wired modem or chest not found') + end + return device.wired_modem.getNameLocal() +end + +equip('left', 'minecraft:diamond_sword') +equip('right', 'plethora:sensor', 'plethora:module:3') + +local sensor = device['plethora:sensor'] +local c = Peripheral.lookup('type/minecraft:chest') or error('Missing chest') + +local directions = { top = 'down', bottom = 'up' } +local direction = directions[c.side] or getLocalName() +local chest = Adapter({ side = c.side, direction = direction }) or error('missing chest') + +if not fs.exists(STARTUP_FILE) then + Util.writeFile(STARTUP_FILE, + [[os.sleep(1) +shell.openForegroundTab('rancher.lua')]]) + print('Autorun program created: ' .. STARTUP_FILE) +end + +local dispenser = Peripheral.lookup('type/minecraft:dispenser') +local integrator = Peripheral.lookup('type/redstone_integrator') + +local function pulse() + integrator.setOutput('north', true) + os.sleep(.25) + integrator.setOutput('north', false) +end + +local function turnOffWater() + if dispenser then + local list = dispenser.list() + if list[1].name == 'minecraft:bucket' then + pulse() + os.sleep(2) + end + end +end + +local function turnOnWater() + if dispenser then + if dispenser.list()[1].name == 'minecraft:water_bucket' then + pulse() + end + end +end + +local function getAnimalCount() + local blocks = sensor.sense() + + local grown = 0 + local babies = 0 + local xpCount = 0 + + Util.filterInplace(blocks, function(v) + if v.name == config.animal then + if v.y > -.5 then grown = grown + 1 end + if v.y < -.5 then babies = babies + 1 end + return v.y > -.5 + elseif v.name == 'XPOrb' then + xpCount = xpCount + 1 + end + end) + + Util.print('%d grown, %d babies, %d xp', grown, babies, xpCount) + + return #blocks, xpCount +end + +local function butcher() + turtle.equip('right', 'minecraft:diamond_sword') + turtle.select(1) + + turtle.attack() + for _ = 1, 3 do + turtle.turnRight() + turtle.attack() + end + turtle.equip('right', 'plethora:module:3') + + turtle.eachFilledSlot(function(slot) + if not retain[slot.name] then + chest:insert(slot.index, 64) + end + end) +end + +local function breed() + turtle.select(1) + + if config.animal == 'Sheep' then + turtle.place('minecraft:shears') + end + turtle.place('minecraft:wheat') + for _ = 1, 3 do + turtle.turnRight() + if config.animal == 'Sheep' then + turtle.place('minecraft:shears') + end + turtle.place('minecraft:wheat') + end +end + +local s, m = turtle.run(function() + turnOffWater() + + repeat + local animalCount, xpCount = getAnimalCount() + if animalCount > config.max_animals then + turtle.setStatus('Butchering') + butcher() + elseif turtle.getItemCount(animal.food) == 0 then + if chest:provide({ name = animal.food, damage = 0 }, 64) == 0 then + print('Out of ' .. animal.food) + turtle.setStatus('Out of food') + end + else + turtle.setStatus('Breeding') + breed() + end + if xpCount > 2 then + turnOnWater() + os.sleep(8) + turnOffWater() + end + os.sleep(5) + until turtle.isAborted() +end) + +if not s and m then + error(m) +end diff --git a/apps/treefarm.lua b/farms/treefarm.lua similarity index 95% rename from apps/treefarm.lua rename to farms/treefarm.lua index 6f7c146..8bf9040 100644 --- a/apps/treefarm.lua +++ b/farms/treefarm.lua @@ -6,7 +6,7 @@ _G.requireInjector() Area around turtle must be flat and can only be dirt or grass (10 blocks in each direction from turtle) Turtle must have: crafting table, chest - Turtle must have a pick equipped + Turtle must have a pick equipped on the LEFT side Optional: Add additional sapling types that can grow with a single sapling @@ -23,10 +23,13 @@ _G.requireInjector() local Point = require('point') local Util = require('util') +local fs = _G.fs local os = _G.os local read = _G.read local turtle = _G.turtle +local STARTUP_FILE = 'usr/autorun/treefarm.lua' + local FUEL_BASE = 0 local FUEL_DIRE = FUEL_BASE + 10 local FUEL_GOOD = FUEL_BASE + 2000 @@ -75,6 +78,13 @@ local state = Util.readTable('usr/config/treefarm') or { } } +if not fs.exists(STARTUP_FILE) then + Util.writeFile(STARTUP_FILE, + [[os.sleep(1) +shell.openForegroundTab('treefarm.lua')]]) + print('Autorun program created: ' .. STARTUP_FILE) +end + local clock = os.clock() local function inspect(fn) @@ -140,8 +150,22 @@ local function craftItem(item, qty) return success end +local function emptyFurnace() + if state.cooking then + + print('Emptying furnace') + + turtle.suckDownAt(state.furnace) + turtle.suckForwardAt(state.furnace) + turtle.suckUpAt(state.furnace) + setState('cooking') + end +end + local function cook(item, count, result, fuel, fuelCount) + emptyFurnace() + setState('cooking', true) fuel = fuel or CHARCOAL @@ -154,9 +178,19 @@ local function cook(item, count, result, fuel, fuelCount) count = count + turtle.getItemCount(result) turtle.select(1) turtle.pathfind(Point.below(state.furnace)) + + local lastSuck = os.clock() repeat os.sleep(1) - turtle.suckUp() + if turtle.suckUp() then + lastSuck = os.clock() + end + + if os.clock() - lastSuck > 10 then + -- sponge bug + Util.print('Timed out waiting for furnace') + return + end until turtle.getItemCount(result) >= count setState('cooking') @@ -225,18 +259,6 @@ local function makeCharcoal() return true end -local function emptyFurnace() - if state.cooking then - - print('Emptying furnace') - - turtle.suckDownAt(state.furnace) - turtle.suckForwardAt(state.furnace) - turtle.suckUpAt(state.furnace) - setState('cooking') - end -end - local function getCobblestone(count) local slots = turtle.getSummedInventory() @@ -602,7 +624,20 @@ local function findGround() break end - if b == COBBLESTONE or b == STONE then + if b == COBBLESTONE then + turtle.back() + local s2, b2 = turtle.inspectDown() + if not s2 then + error('lost') + end + if b2.name == COBBLESTONE then + turtle.turnLeft() + turtle.back() + end + break + end + + if b == STONE then error('lost') end diff --git a/forestry/.package b/forestry/.package new file mode 100644 index 0000000..0b8913e --- /dev/null +++ b/forestry/.package @@ -0,0 +1,6 @@ +{ + title = 'Forestry mod applications', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/forestry', + description = [[WIP]], + licence = 'MIT', +} diff --git a/forestry/alveary.lua b/forestry/alveary.lua new file mode 100644 index 0000000..b25a701 --- /dev/null +++ b/forestry/alveary.lua @@ -0,0 +1,106 @@ +_G.requireInjector(_ENV) + +local Event = require('event') +local UI = require('ui') + +redstone.setBundledOutput('bottom', 0) + +local function regulate(humidity, heat) + local heater = heat == 'Up 1' or heat == 'Both 1' + local lava = heat == 'Both 1' + local water = humidity == 'Up 1' + + local c = colors.combine( + lava and colors.green or 0, + heater and colors.red or 0, + water and colors.blue or 0) + + redstone.setBundledOutput('bottom', c) +end + +function create(alveary, terminal) + local window = UI.Window({ + alveary = alveary, + parent = UI.Device({ + device = terminal, + textScale = 0.5, + backgroundColor = colors.green + }), + progressBar = UI.ProgressBar({ + y = 3, + x = 2, ex = -2, + }), +--[[ + heater = UI.Button { + x = 2, y = -2, width = 7, + text = 'heater', + }, + humidifier = UI.Button { + x = 2, y = -4, + text = 'Humidify', + }, + dehumidifier = UI.Button { + x = 2, y = -6, + text = 'Dehumidify', + }, +--]] + }) + + function window:draw() + + local queen = self.alveary.getQueen() + if not queen then + self:clear(colors.black) + regulate() + else + self.backgroundColor = self.alveary.canBreed() and colors.green or colors.red + self:clear() + local percDone = 100 - math.floor(queen.health * 100 / queen.maxHealth) + if not queen.canSpawn then + percDone = 0 + end + self.progressBar.value = percDone + --self.progressBar:draw() + for _,c in pairs(self.children) do + c:draw() + end + + self:centeredWrite(2, queen.displayName) + self:centeredWrite(4, percDone .. '%') + self:write(1, 6, 'Generation: ' .. queen.generation) + self:setCursorPos(1, 7) + if queen.active then + regulate( + queen.active.humidityTolerance, + queen.active.temperatureTolerance) + + if queen.active.flowerProvider ~= 'Flowers' then + self:print(queen.active.flowerProvider .. '\n') + end + if queen.active.effect ~= 'None' then + self:print('Effect: ' .. queen.active.effect) + end + else + self:print('(pure)') + end + end + end + + return window +end + +local pages = { + create(device.items, device.monitor), + --create(device.items_6, device.monitor_22), + --create(device.items_5, device.monitor_21), +} + +Event.onInterval(5, function() + for _,v in pairs(pages) do + v:draw() + v:sync() + end +end) + +UI:pullEvents() + diff --git a/forestry/beeInfo.lua b/forestry/beeInfo.lua new file mode 100644 index 0000000..7a4fdfd --- /dev/null +++ b/forestry/beeInfo.lua @@ -0,0 +1,176 @@ +_G.requireInjector(_ENV) + +local Event = require('event') +local UI = require('ui') +local Util = require('util') + +local chest = peripheral.wrap('bottom') + +local data +local monitor = UI.Device({ + deviceType = 'monitor', + textScale = .5 +}) + +UI:setDefaultDevice(monitor) + +local breedingPage = UI.Page({ + titleBar = UI.TitleBar(), + grid = UI.Grid({ + columns = { + { heading = ' ', key = 'chance' }, + { heading = 'Princess', key = 'princess', }, + { heading = 'Drone', key = 'drone' }, + { heading = 'Result', key = 'result', }, + }, + y = 2, ey = -8, + sortColumn = 'result', + autospace = true + }), + specialConditions = UI.Window({ + backgroundColor = colors.red, + y = -7, + height = 2 + }), + buttons = UI.Window({ + y = monitor.height - 4, + width = monitor.width, + height = 5, + backgroundColor = colors.gray, + prevButton = UI.Button({ + event = 'previous', + x = 2, + y = 2, + height = 3, + width = 5, + text = ' < ' + }), + resetButton = UI.Button({ + event = 'clear', + x = 8, + y = 2, + height = 3, + width = monitor.width - 14, + text = 'Clear' + }), + nextButton = UI.Button({ + event = 'next', + x = monitor.width - 5, + y = 2, + height = 3, + width = 5, + text = ' > ' + }) + }) +}) + +function breedingPage:getBreedingData() + self.grid.values = { } + local stacks = chest.getAllStacks(false) + local stack = stacks[1] + self.titleBar.title = stack.individual.displayName + if stack.individual.active then + end + for _,d in pairs(data) do + if d.allele1 == stack.individual.displayName or + d.allele2 == stack.individual.displayName then + local ind = '' + if d.specialConditions then + ind = '*' + end + table.insert(self.grid.values, { + princess = d.allele1 .. ind, + drone = d.allele2, + result = d.result, + chance = d.chance .. '%', + specialConditions = d.specialConditions + }) + end + end + self.grid.index = 1 + self.grid:adjustWidth() + self.grid:update() + self:draw() + self:sync() +end + +function breedingPage.specialConditions:draw() + local selected = self.parent.grid:getSelected() + if selected and selected.specialConditions then + local sc = '' + if selected.specialConditions then + for _,v in ipairs(selected.specialConditions) do + if sc ~= '' then + sc = sc .. ', ' + end + sc = sc .. v + end + end + self:clear() + self:setCursorPos(2, 1) + self:print(sc) + else + self:clear(colors.red) + end +end + +function breedingPage.grid:draw() + UI.Grid.draw(self) + self.parent.specialConditions:draw() +end + +function breedingPage:eventHandler(event) + if event.type == 'next' then + self.grid:setPage(self.grid:getPage() + 1) + elseif event.type == 'previous' then + self.grid:setPage(self.grid:getPage() - 1) + elseif event.type == 'clear' then + self.grid:setTable({}) + self.grid:draw() + elseif event.type == 'grid_focus_row' then + self.specialConditions:draw() + else + return UI.Page.eventHandler(self, event) + end + return false +end + +Event.on('turtle_inventory', function() + local slot = turtle.selectSlotWithQuantity(1) + + if slot then + turtle.dropDown() + breedingPage:getBreedingData() + turtle.suckDown() + turtle.drop() + end + +end) + +if not fs.exists('.bee.data') then + local p = peripheral.wrap("back") + local data = p.getBeeBreedingData() + local t = { } + for _,d in pairs(data) do + d = Util.shallowCopy(d) + if type(d.specialConditions) == 'string' then + if d.specialConditions == '[]' then + d.specialConditions = '' + end + end + if #d.specialConditions == 0 then + d.specialConditions = nil + else + d.specialConditions = Util.shallowCopy(d.specialConditions) + end + table.insert(t, d) + end + Util.writeTable('.bee.data', t) +else + data = Util.readTable('.bee.data') +end + +UI:setPage(breedingPage) + +UI:pullEvents() + diff --git a/forestry/filing.lua b/forestry/filing.lua new file mode 100644 index 0000000..17423fd --- /dev/null +++ b/forestry/filing.lua @@ -0,0 +1,34 @@ +_G.requireInjector(_ENV) + +local Event = require('event') +local Util = require('util') + +local chest = peripheral.wrap('top') + +function getOpenChestSlot(stacks) + for i = 1, chest.getInventorySize() do + if not stacks[i] then + return i + end + end +end + +Event.on('turtle_inventory', function() + for i = 1, 16 do + if turtle.getItemCount(i) > 0 then + chest.pullItem('down', i, 1) + os.sleep(.5) + local stacks = chest.getAllStacks(false) + local _,slot = Util.find(stacks, 'qty', 2) + if slot then + print('Duplicate') + chest.pushItem('north', slot, 1) + else + print('New Serum') + end + end + end +end) + +Event.pullEvents() + diff --git a/forestry/serums.lua b/forestry/serums.lua new file mode 100644 index 0000000..7eb3e1e --- /dev/null +++ b/forestry/serums.lua @@ -0,0 +1,34 @@ +_G.requireInjector(_ENV) + +local Event = require('event') +local Util = require('util') + +local chest = peripheral.wrap('top') + +function getOpenChestSlot(stacks) + for i = 1, chest.getInventorySize() do + if not stacks[i] then + return i + end + end +end + +Event.on('turtle_inventory', function() + for i = 1, 16 do + if turtle.getItemCount(i) > 0 then + local stacks = chest.getAllStacks(false) + local slot = getOpenChestSlot(stacks) + chest.pullItemIntoSlot('down', i, 1, slot) + local serum = chest.getStackInSlot(slot) + if Util.find(stacks, 'nbt_hash', serum.nbt_hash) then + print('Duplicate') + chest.pushItem('north', slot, 1) + else + print('New Serum') + end + end + end + +end) + +Event.pullEvents() diff --git a/glasses/.package b/glasses/.package new file mode 100644 index 0000000..beb116e --- /dev/null +++ b/glasses/.package @@ -0,0 +1,6 @@ +{ + title = 'Plethora overlay glasses support', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/glasses', + description = [[WIP]], + licence = 'MIT', +} diff --git a/glasses/apis/shatter.lua b/glasses/apis/shatter.lua new file mode 100644 index 0000000..6c58810 --- /dev/null +++ b/glasses/apis/shatter.lua @@ -0,0 +1,481 @@ +local os = _G.os +local parallel = _G.parallel +local peripheral = _G.peripheral + +local mods = peripheral.wrap("back") +--ensure glasses are present +if not mods.canvas then + error("Shatter requires Overlay Glasses", 2) +end + +--###TERMINAL API CODE###-- +--colors, for reference use +local colors = { + white = 0xf0f0f000, + orange = 0xf2b23300, + magenta = 0xe57fd800, + lightBlue = 0x99b2f200, + yellow = 0xdede6c00, + lime = 0x7fcc1900, + pink = 0xf2b2cc00, + gray = 0x4c4c4c00, + lightGray = 0x99999900, + cyan = 0x4c99b200, + purple = 0xb266e500, + blue = 0x3366cc00, + brown = 0x7f664c00, + green = 0x57a64e00, + red = 0xcc4c4c00, + black = 0x19191900 +} +--colors by number + --colors by number +local cbn = { + colors.white, + colors.orange, + colors.magenta, + colors.lightBlue, + colors.yellow, + colors.lime, + colors.pink, + colors.gray, + colors.lightGray, + colors.cyan, + colors.purple, + colors.blue, + colors.brown, + colors.green, + colors.red, + colors.black +} +--scaled character factor +local ox, oy = 6, 9 +-- character size +local sx, sy = 6, 9 +--term bg and fg colors and alpha values +local bg, fg, bgbn, fgbn, fga, bga, fgabn, bgabn = colors.black, colors.white, 2^(#cbn-1), 2^0, 255, 255, 1, 1 +--cursor, pos, and blink +local csr, cx, cy, cb = nil, 1, 1, true +local textScale = 1 +local can = mods.canvas() + +--term size +local tx, ty = can.getSize() +tx, ty = math.floor(tx/ox), math.floor(ty/oy) +--screen rendering in a table +local screen = {} +--populate that table +local function tPop() + local x, y = can.getSize() + for i = 1, math.floor(x/ox) do + screen[i] = {} + for j = 1, math.floor(y/oy) do + screen[i][j] = {bg = {}, fg = {}} + end + end +end +tPop() +--write text in grid fashion and add to table +local function write(x, y, char, color) + x, y = math.floor(x), math.floor(y) + if x > 0 and y > 0 and x <= tx and y <= ty then + if not screen[x][y].fg.getColor then + screen[x][y].fg = can.addText({((x-1)*ox)+1, ((y-1)*oy)+1}, char, color, ox/sx) + else + screen[x][y].fg.setColor(color) + screen[x][y].fg.setText(char) + end + end +end +--draw pixel in grid fashion and add to table +local function draw(x, y, color) + x, y = math.floor(x), math.floor(y) + if x > 0 and y > 0 and x <= tx and y <= ty then + if not screen[x][y].bg.getColor then + screen[x][y].bg = can.addRectangle((x-1)*ox, (y-1)*oy, ox, oy, color) + else + screen[x][y].bg.setColor(color) + end + end +end +--get the data of a particular pixel +local function getData(pixel) + if pixel then + return {bgc = bit32.band(pixel.bg.getColor(), 2^32-1), -- Credit to MC:Anavrins for bit32 ingenuity + fgc = bit32.band(pixel.fg.getColor(), 2^32-1), + txt = pixel.fg.getText()} + end +end +--set the data of a particular pixel +local function setData(pixel, data) + if pixel and data then + if pixel.bg.getPosition then + local x, y = pixel.bg.getPosition() + draw(math.floor(x/ox)+1, math.floor(y/oy)+1, data.bgc) + write(math.floor(x/ox)+1, math.floor(y/oy)+1, data.txt, data.fgc) + end + end +end +--move a row to an entirely different line +local function move(line, to) + if line > 0 and line <= ty and to > 0 and to <= ty then + for i = 1, tx do + setData(screen[i][to], getData(screen[i][line])) + end + end +end +--populate term with default bg and fg colors. +local function repopulate() + local x, y = can.getSize() + for i = 1, math.floor(x/ox) do + for j = 1, math.floor(y/oy) do + draw(i, j, bg) + end + end + for i = 1, math.floor(x/ox) do + for j = 1, math.floor(y/oy) do + write(i, j, "", fg) + end + end +end + +local function resize(x, y) + ox, oy = math.ceil(textScale*sx), math.ceil(textScale*sy) + tx, ty = x, y + csr.remove() + local oldscr = screen -- replicate the screen + screen = {} -- remove it for repopulation of table w/ new scale + tPop() -- repopulate table + repopulate() -- add objects + csr = can.addText({cx*ox, (cy*oy)+1}, "", 0xffffffff, textScale) --recreate cursor + for i = 1, #oldscr do --rerender screen in new scale + for j = 1, #oldscr[i] do + if oldscr[i][j].bg.getColor ~= nil then + --if screen[i] and screen[i][j] then + -- setData(screen[i][j], getData(oldscr[i][j])) + --end + oldscr[i][j].bg.remove() + oldscr[i][j].fg.remove() + end + end + os.sleep(0) + end + os.queueEvent("monitor_resize", "glasses") +end + +local out = {} +out.write = function(str) +--term.write + str = tostring(str) + for i = 1, #str do + write(cx+i-1, cy, str:sub(i, i), fg+fga) + draw(cx+i-1, cy, bg+bga) + end + cx = cx+#str +end +out.blit = function(str, tfg, tbg) +--term.blit + if type(str) ~= "string" then + error("bad argument #1 (expected string, got "..type(str)..")", 2) + elseif type(tfg) ~= "string" then + error("bad argument #2 (expected string, got "..type(tfg)..")", 2) + elseif type(tbg) ~= "string" then + error("bad argument #3 (expected string, got "..type(tbg)..")", 2) + end + for i = 1, #str do + local nfg = cbn[tonumber(tfg:sub(i,i), 16)+1] + local nbg = cbn[tonumber(tbg:sub(i,i), 16)+1] + draw(cx+i-1, cy, nbg+bga) + write(cx+i-1, cy, str:sub(i,i), nfg+fga) + end + cx = cx+#str +end +out.clear = function() +--term.clear + for i = 1, tx do + for j = 1, ty do + write(i, j, "", fg+fga) + draw(i, j, bg+bga) + end + end +end +out.clearLine = function() +--term.clearLine + if cy > 0 and cy <= ty then + for i = 1, tx do + draw(i, cy, bg+bga) + write(i, cy, "", fg+fga) + end + end +end +out.getCursorPos = function() +--term.getCursorPos + return cx, cy +end +out.setCursorPos = function(x, y) +--term.setCursorPos + if type(x) ~= "number" then + error("bad argument #1 (expected number, got "..type(x)..")", 2) + elseif type(y) ~= "number" then + error("bad argument #2 (expected number, got "..type(y)..")", 2) + end + csr.setPosition((x-1)*ox, ((y-1)*oy)+1) + cx, cy = x, y +end + +out.setCursorBlink = function(b) + if type(b) ~= "boolean" then + error("bad argument #1 (expected boolean, got "..type(b)..")", 2) + end + cb = b +end + +out.isColor = function() + return true, "now with more alpha!" +end + +out.isColour = out.isColor + +out.getSize = function() + return tx, ty +end + +out.setSize = function(x, y) + resize(x, y) +end + +out.scroll = function(amount) + local _, tcy = out.getCursorPos() + if type(amount) ~= "number" then + error("bad argument #1 (expected number, got "..type(amount)..")", 2) + end + if amount > 0 then + for i = 1, tx do + move(i, i-amount) + end + elseif amount < 0 then + for i = tx, 1, -1 do + move(i, i-amount) + end + end + out.setCursorPos(1, tcy-amount-1) +end +local function invCol(col) +--A simple error message I am too lazy to type twice +--used in the following few functions + error("invalid color (got "..col..")", 2) +end +local function lb2(num) +--very basic implementation of base 2 logarithm + return math.log(num)/math.log(2) +end +out.setTextColor = function(col) +--term.setTextColor + if type(col) ~= "number" then + error("bad argument #1 (number expected, got "..type(col)..")", 2) + end + if lb2(col) > #cbn or lb2(col) ~= math.ceil(lb2(col)) then + invCol(col) + else + fg = cbn[lb2(col)+1] + fgbn = col + end +end +out.setBackgroundColor = function(col) +--term.setBackgroundColor + if type(col) ~= "number" then + error("bad argument #1 (expected number, got "..type(col)..")", 2) + end + if lb2(col) > #cbn or lb2(col) ~= math.ceil(lb2(col)) then + invCol(col) + else + bg = cbn[lb2(col)+1] + bgbn = col + end +end +-- Text & BG Alpha innovated by MC:Ale32bit +out.setTextAlpha = function(val) +-- set the alpha value of the text + if type(val) ~= "number" then + error("bad argument #1 (expected number, got "..type(val)..")", 2) + end + if val > 1 then val = 1 elseif val < 0 then val = 0 end + fga = math.floor(val*255) + fgabn = val +end +out.setBackgroundAlpha = function(val) +-- set the alpha value of the background + if type(val) ~= "number" then + error("bad argument #1 (expected number, got "..type(val)..")", 2) + end + if val > 1 then val = 1 elseif val < 0 then val = 0 end + bga = math.floor(val*255) + bgabn = val +end +out.setTextHex = function(hex) +-- set the hex color value of the text + if type(tonumber(hex, 16)) ~= "number" then + error("bad argument #1 (expected number, got "..type(hex)..")", 2) + end + fg = hex + fgbn = 1 +end +out.setBackgroundHex = function(hex) +-- set the hex color value of the background + if type(tonumber(hex, 16)) ~= "number" then + error("bad argument #1 (expected number, got "..type(hex)..")", 2) + end + bg = hex + bgbn = 1 +end +out.getTextColor = function() +--term.getTextColor + return fgbn +end +out.getBackgroundColor = function() +--term.getBackgroundColor + return bgbn +end +out.getTextAlpha = function() +-- get the alpha value of the text + return fgabn +end +out.getBackgroundAlpha = function() +--get the alpha value of the background + return bgabn +end +out.getTextHex = function() +-- get the hex color value of the text + return fg +end +out.getBackgroundHex = function() +-- get the hex color value of the background + return bg +end +local function torgba(hex) +-- Converts a hex value into 3 seperate r, g, and b values +-- Technically also gets a value, but it thrown out due to what this is needed for +-- Credit to MC:valithor2 for this algorithm + local vals = {} + for i = 1, 4 do + vals[i] = hex%256 + hex = (hex-vals[i])/256 + end + return vals[4]/255, vals[3]/255, vals[2]/255 +end +local function refreshColor(oc, nc) +-- refreshes terminal when palette values are manipulated + for i = 1, #screen do + for j = 1, #screen[i] do + local op, changed = getData(screen[i][j]), false + if op.bgc == oc then + op.bgc = nc + changed = true + end + if op.fgc == oc then + op.fgc = nc + changed = true + end + if changed then + setData(screen[i][j], op) + end + end + end +end +out.getPaletteColor = function(col) +--term.getPaletteColor + if type(col) ~= "number" then + error("bad argument #1 (number expected, got "..type(col)..")", 2) + end + if lb2(col) > #cbn or lb2(col) ~= math.ceil(lb2(col)) then + invCol(col) + end + return torgba(cbn[lb2(col)+1]) +end +out.setPaletteColor = function(cnum, r, g, b) +--term.setPaletteColor + local oc = cbn[lb2(cnum)+1] + if type(cnum) ~= "number" then + error("bad argument #1 (number expected, got "..type(cnum)..")", 2) + end + if type(r) ~= "number" then + error("bad argument #2 (number expected, got "..type(r)..")", 2) + end + if g then + if type(g) ~= "number" then + error("bad argument #3 (number expected, got "..type(g)..")", 2) + elseif type(b) ~= "number" then + error("bad argument #4 (number expected, got "..type(b)..")", 2) + end + if r > 1 then r = 1 elseif r < 0 then r = 0 end + if g > 1 then g = 1 elseif g < 0 then g = 0 end + if b > 1 then b = 1 elseif b < 0 then b = 0 end + cbn[lb2(cnum)+1] = (((r*255)*(16^6))+((g*255)*(16^4))+((b*255)*(16^2))) + else + cbn[lb2(cnum)+1] = (r*256) + end + if bgbn == cnum then + out.setBackgroundColor(bgbn) + end + if fgbn == cnum then + out.setTextColor(fgbn) + end + --refreshColor(oc, cbn[lb2(cnum)+1]) -- errors +end +--compat for all those UK'ers +out.setTextColour = out.setTextColor +out.setBackgroundColour = out.setBackgroundColor +out.setPaletteColour = out.setPaletteColor +out.getTextColour = out.getTextColor +out.getBackgroundColour = out.getBackgroundColor +out.getPaletteColour = out.getPaletteColor + +out.getTextScale = function() return textScale end +out.setTextScale = function(scale) + if type(scale) ~= "number" then + error("bad argument #1 (number expected, got "..type(scale)..")", 2) + end + if 0.4 >= scale or scale > 10 then + error("Expected number in range 0.5-10", 2) + end + if textScale ~= scale then + local factor = textScale/scale + textScale = scale + resize(tx*factor, ty*factor) + end +end + +--###TERMINAL CREATION CODE###-- +csr = can.addText({cx*ox, (cy*oy)+1}, "", 0xffffffff, ox/sx) +repopulate() +out.cursorRoutine = function() + parallel.waitForAll(function() + --cursor flicker + while true do + if not cb then + csr.setText(" ") + os.sleep() + else + csr.setText("_") + os.sleep(.4) + csr.setText(" ") + os.sleep(.4) + end + end + end, + function() + --glasses event handler conversion + while true do + local e = {os.pullEvent()} + if e[1]:find("glasses") then + local _, b = e[1]:find("glasses") + e[1] = "mouse"..e[1]:sub(b+1, -1) + if e[1] ~= "mouse_scroll" then + e[3], e[4] = math.ceil(e[3]/ox), math.ceil(e[4]/oy) + end + os.queueEvent(unpack(e)) + end + end + end) +end +return out diff --git a/glasses/glassesDriver.lua b/glasses/glassesDriver.lua new file mode 100644 index 0000000..9b17688 --- /dev/null +++ b/glasses/glassesDriver.lua @@ -0,0 +1,31 @@ +_G.requireInjector(_ENV) + +local device = _G.device +local kernel = _G.kernel +local os = _G.os + +local glasses = require('shatter') +glasses.name = 'glasses' +glasses.type = 'rayban' +glasses.size = 'face' +device.glasses = glasses + +glasses.setTextScale(.5) +glasses.setSize(100, 40) + +kernel.hook({ 'glasses_click', 'glasses_up', 'glasses_drag' }, function(event, eventData) + local sx, sy = 6, 9 + local scale = glasses.getTextScale() + local ox, oy = math.ceil(scale*sx), math.ceil(scale*sy) + + local lookup = { + glasses_click = 'monitor_touch', + glasses_up = 'monitor_up', + glasses_drag = 'monitor_drag', + } + local x, y = math.floor(eventData[2]/ox) + 1, math.floor(eventData[3]/oy) + 1 + os.queueEvent(lookup[event], 'glasses', x, y) + + glasses.setCursorPos(x, y) + glasses.write('X ' .. eventData[3]) +end) diff --git a/milo/.package b/milo/.package new file mode 100644 index 0000000..6946b44 --- /dev/null +++ b/milo/.package @@ -0,0 +1,11 @@ +{ + required = { + 'core', + }, + 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.]], + licence = 'MIT', +} diff --git a/milo/Milo.lua b/milo/Milo.lua new file mode 100644 index 0000000..ab7b60e --- /dev/null +++ b/milo/Milo.lua @@ -0,0 +1,229 @@ +--[[ + Provides: autocrafting, resource limits, on-demand crafting. + + Turtle crafting: + 1. The turtle must have a crafting table equipped. + 2. Equip the turtle with an introspection module. +]]-- + +_G.requireInjector(_ENV) + +local Config = require('config') +local Event = require('event') +local Milo = require('milo') +local Sound = require('sound') +local Storage = require('storage') +local UI = require('ui') +local Util = require('util') + +local device = _G.device +local fs = _G.fs +local multishell = _ENV.multishell +local os = _G.os +local shell = _ENV.shell +local turtle = _G.turtle + +if multishell then + multishell.setTitle(multishell.getCurrent(), 'Milo') +end + +local nodes = Config.load('milo', { }) + +-- TODO: remove - temporary +if nodes.remoteDefaults then + nodes.nodes = nodes.remoteDefaults + nodes.remoteDefaults = nil +end + +-- TODO: remove - temporary +if nodes.nodes then + local categories = { + input = 'custom', + trashcan = 'custom', + machine = 'machine', + brewingStand = 'machine', + activity = 'display', + jobs = 'display', + ignore = 'ignore', + hidden = 'ignore', + manipulator = 'custom', + storage = 'storage', + } + for _, node in pairs(nodes.nodes) do + if node.lock and type(node.lock) == 'string' then + node.lock = { + [ node.lock ] = true, + } + end + if not node.category then + node.category = categories[node.mtype] + if not node.category then + Util.print(node) + error('invalid node') + end + end + end + nodes = nodes.nodes +end + +local function Syntax(msg) + print([[ +Turtle must be equipped with: + * Introspection module + * Workbench + +Turtle must be connected to: + * Wired modem +]]) + + error(msg) +end + +local modem +for _,v in pairs(device) do + if v.type == 'wired_modem' then + if modem then + Syntax('Only 1 wired modem can be connected') + end + modem = v + end +end + +if not modem or not modem.getNameLocal then + Syntax('Wired modem missing') +end + +if not modem.getNameLocal() then + Syntax('Wired modem is not active') +end + +local introspection = device['plethora:introspection'] or + turtle.equip('left', 'plethora:module:0') and device['plethora:introspection'] or + Syntax('Introspection module missing') + +if not device.workbench then + turtle.equip('right', 'minecraft:crafting_table:0') + if not device.workbench then + Syntax('Workbench missing') + end +end + +local localName = modem.getNameLocal() + +local context = { + resources = Util.readTable(Milo.RESOURCE_FILE) or { }, + + state = { }, + craftingQueue = { }, + tasks = { }, + queue = { }, + + storage = Storage(nodes), + turtleInventory = { + name = localName, + mtype = 'hidden', + adapter = introspection.getInventory(), + } +} + +nodes[localName] = context.turtleInventory +nodes[localName].adapter.name = localName + +_G._p = context --debug + +Milo:init(context) +context.storage:initStorage() + +-- TODO: fix +context.storage.turtleInventory = context.turtleInventory + +local function loadDirectory(dir) + for _, file in pairs(fs.list(dir)) do + local s, m = Util.run(_ENV, fs.combine(dir, file)) + if not s and m then + _G.printError('Error loading: ' .. file) + error(m or 'Unknown error') + end + end +end + +local programDir = fs.getDir(shell.getRunningProgram()) +loadDirectory(fs.combine(programDir, 'core')) +loadDirectory(fs.combine(programDir, 'plugins')) + +table.sort(context.tasks, function(a, b) + return a.priority < b.priority +end) + +_debug('Tasks\n-----') +for _, task in ipairs(context.tasks) do + _debug('%d: %s', task.priority, task.name) +end + +Milo:clearGrid() + +UI:setPage(UI:getPage('listing')) +Sound.play('ui.toast.challenge_complete') + +local processing + +Event.on('milo_cycle', function() + if not processing and not Milo:isCraftingPaused() then + processing = true + Milo:resetCraftingStatus() + + for _, task in ipairs(context.tasks) do + local s, m = pcall(function() task:cycle(context) end) + if not s and m then + _G._debug(task.name .. ' crashed') + _G._debug(m) + -- _G.printError(task.name .. ' crashed') + -- _G.printError(m) + end + end + processing = false + if not Util.empty(context.queue) then + os.queueEvent('milo_queue') + end + end +end) + +Event.on('milo_queue', function() + if not processing and context.storage:isOnline() then + processing = true + + for _, key in pairs(Util.keys(context.queue)) do + local entry = context.queue[key] + entry.callback(entry.request) + context.queue[key] = nil + end + + processing = false + end +end) + +Event.on('turtle_inventory', function() + if not processing and not Milo:isCraftingPaused() then + Milo:clearGrid() + end +end) + +Event.onInterval(5, function() + if not Milo:isCraftingPaused() then + os.queueEvent('milo_cycle') + end +end) + +Event.on({ 'storage_offline', 'storage_online' }, function() + if context.storage:isOnline() then + Milo:resumeCrafting({ key = 'storageOnline' }) + else + Milo:pauseCrafting({ key = 'storageOnline', msg = 'Storage offline' }) + end +end) + +os.queueEvent( + context.storage:isOnline() and 'storage_online' or 'storage_offline', + context.storage:isOnline()) + +UI:pullEvents() diff --git a/milo/MiloRemote.lua b/milo/MiloRemote.lua new file mode 100644 index 0000000..ee9853f --- /dev/null +++ b/milo/MiloRemote.lua @@ -0,0 +1,550 @@ +_G.requireInjector(_ENV) + +local Config = require('config') +local Event = require('event') +local Sound = require('sound') +local Socket = require('socket') +local sync = require('sync').sync +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors +local device = _G.device +local fs = _G.fs +local os = _G.os +local string = _G.string + +local SHIELD_SLOT = 2 +local STARTUP_FILE = 'usr/autorun/miloRemote.lua' + +local config = Config.load('miloRemote', { displayMode = 0 }) + +local socket +local depositMode = { + [ true ] = { text = '\25', textColor = colors.black, help = 'Deposit enabled' }, + [ false ] = { text = '\215', textColor = colors.red, help = 'Deposit disabled' }, +} + +local displayModes = { + [0] = { text = 'A', help = 'Showing all items' }, + [1] = { text = 'I', help = 'Showing inventory items' }, +} + +local page = UI.Page { + menuBar = UI.MenuBar { + y = 1, height = 1, + buttons = { + { + text = 'Refresh', + x = -12, + event = 'refresh' + }, + { + text = '\206', + x = -3, + dropdown = { + { text = 'Setup', event = 'setup' }, + UI.MenuBar.spacer, + { + text = 'Rescan storage', + event = 'rescan', + help = 'Rescan all inventories' + }, + }, + }, + }, + infoBar = UI.StatusBar { + x = 1, ex = -16, + backgroundColor = colors.lightGray, + }, + }, + grid = UI.Grid { + y = 2, ey = -2, + columns = { + { heading = ' Qty', key = 'count' , width = 4, justify = 'right' }, + { heading = 'Name', key = 'displayName' }, + }, + values = { }, + sortColumn = config.sortColumn or 'count', + inverseSort = config.inverseSort, + help = '^(s)tack, ^(a)ll' + }, + statusBar = UI.Window { + y = -1, + filter = UI.TextEntry { + x = 1, ex = -12, + limit = 50, + shadowText = 'filter', + backgroundColor = colors.cyan, + backgroundFocusColor = colors.cyan, + accelerators = { + [ 'enter' ] = 'eject', + }, + }, + amount = UI.TextEntry { + x = -11, ex = -7, + limit = 3, + shadowText = '1', + shadowTextColor = colors.gray, + backgroundColor = colors.black, + backgroundFocusColor = colors.black, + accelerators = { + [ 'enter' ] = 'eject_specified', + }, + help = 'Request amount', + }, + depositToggle = UI.Button { + x = -6, + event = 'toggle_deposit', + text = '\215', + }, + display = UI.Button { + x = -3, + event = 'toggle_display', + text = displayModes[config.displayMode].text, + help = displayModes[config.displayMode].help, + }, + }, + accelerators = { + r = 'refresh', + [ 'control-r' ] = 'refresh', + [ 'control-e' ] = 'eject', + [ 'control-s' ] = 'eject_stack', + [ 'control-a' ] = 'eject_all', + + q = 'quit', + }, + setup = UI.SlideOut { + backgroundColor = colors.cyan, + titleBar = UI.TitleBar { + title = 'Remote Setup', + }, + form = UI.Form { + x = 2, ex = -2, y = 2, ey = -1, + values = config, + [1] = UI.TextEntry { + formLabel = 'Server', formKey = 'server', + help = 'ID for the server', + shadowText = 'Milo server ID', + limit = 6, + validate = 'numeric', + required = true, + }, + [2] = UI.TextEntry { + formLabel = 'Return Slot', formKey = 'slot', + help = 'Use a slot for sending to storage', + shadowText = 'Inventory slot #', + limit = 5, + validate = 'numeric', + required = false, + }, + [3] = UI.Checkbox { + formLabel = 'Shield Slot', formKey = 'useShield', + help = 'Or, use the shield slot for sending' + }, + [4] = UI.Checkbox { + formLabel = 'Run on startup', formKey = 'runOnStartup', + help = 'Run this program on startup' + }, + info = UI.TextArea { + x = 1, ex = -1, y = 6, ey = -4, + textColor = colors.yellow, + marginLeft = 0, + marginRight = 0, + value = [[The Milo turtle must connect to a manipulator with a ]] .. + [[bound introspection module. The neural interface must ]] .. + [[also have an introspection module.]], + }, + }, + statusBar = UI.StatusBar { + backgroundColor = colors.cyan, + }, + }, + items = { }, +} + +local function getPlayerName() + local neural = device.neuralInterface + + if neural and neural.getName then + return neural.getName() + end +end + +function page:setStatus(status) + self.menuBar.infoBar:setStatus(status) + self:sync() +end + +function page:processMessages(s) + Event.addRoutine(function() + repeat + local response = s:read() + if not response then + break + end + if response.type == 'received' then + Sound.play('entity.item.pickup') + local ritem = self.items[response.key] + if ritem then + ritem.count = response.count + self.grid:draw() + self:sync() + end + + elseif response.type == 'list' then + self.items = self:expandList(response.list) + self:applyFilter() + self.grid:draw() + self.grid:sync() + + elseif response.type == 'transfer' then + if response.count > 0 then + Sound.play('entity.item.pickup') + local item = self.items[response.key] + if item then + item.count = response.current + self.grid:draw() + self:sync() + end + end + if response.craft then + if response.craft > 0 then + self:setStatus(response.craft .. ' crafting ...') + elseif response.craft + response.count < response.requested then + if response.craft + response.count == 0 then + Sound.play('entity.villager.no') + end + self:setStatus((response.craft + response.count) .. ' available ...') + end + end + end + if response.msg then + self:setStatus(response.msg) + end + until not s.connected + + s:close() + s = nil + self:setStatus('disconnected ...') + Sound.play('entity.villager.no') + end) +end + +function page:sendRequest(data, statusMsg) + if not config.server then + self:setStatus('Invalid configuration') + return + end + + local player = getPlayerName() + if not player then + self:setStatus('Missing neural or introspection') + return + end + + local success + sync(self, function() + local msg + for _ = 1, 2 do + if not socket or not socket.connected then + self:setStatus('connecting ...') + socket, msg = Socket.connect(config.server, 4242) + if socket then + socket:write(player) + local r = socket:read(2) + if r and not r.msg then + self:setStatus('connected ...') + self:processMessages(socket) + else + msg = r and r.msg or 'Timed out' + socket:close() + socket = nil + end + end + end + if socket then + if statusMsg then + self:setStatus(statusMsg) + Event.onTimeout(2, function() + self:setStatus('') + end) + end + if socket:write(data) then + success = true + return + end + socket:close() + socket = nil + end + end + self:setStatus(msg or 'Failed to connect') + end) + + return success +end + +function page.grid:getRowTextColor(row, selected) + if row.is_craftable then + return colors.yellow + end + if row.has_recipe then + return colors.cyan + end + return UI.Grid:getRowTextColor(row, selected) +end + +function page.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + row.count = row.count > 0 and Util.toBytes(row.count) or '' + return row +end + +function page.grid:sortCompare(a, b) + if self.sortColumn ~= 'displayName' then + if a[self.sortColumn] == b[self.sortColumn] then + if self.inverseSort then + return a.displayName > b.displayName + end + return a.displayName < b.displayName + end + if a[self.sortColumn] == 0 then + return self.inverseSort + end + if b[self.sortColumn] == 0 then + return not self.inverseSort + end + return a[self.sortColumn] < b[self.sortColumn] + end + return UI.Grid.sortCompare(self, a, b) +end + +function page.grid:eventHandler(event) + if event.type == 'grid_sort' then + config.sortColumn = event.sortColumn + config.inverseSort = event.inverseSort + Config.update('miloRemote', config) + end + return UI.Grid.eventHandler(self, event) +end + +function page:transfer(item, count, msg) + self:sendRequest({ request = 'transfer', item = item, count = count }, msg) +end + +function page.setup:eventHandler(event) + if event.type == 'focus_change' then + self.statusBar:setStatus(event.focused.help) + end + return UI.SlideOut.eventHandler(self, event) +end + +function page:eventHandler(event) + if event.type == 'quit' then + UI:exitPullEvents() + + elseif event.type == 'setup' then + self.setup.form:setValues(config) + self.setup:show() + + elseif event.type == 'toggle_deposit' then + config.deposit = not config.deposit + Util.merge(self.statusBar.depositToggle, depositMode[config.deposit]) + self.statusBar:draw() + self:setStatus(depositMode[config.deposit].help) + Config.update('miloRemote', config) + + elseif event.type == 'form_complete' then + Config.update('miloRemote', config) + self.setup:hide() + self:refresh('list') + self.grid:draw() + self:setFocus(self.statusBar.filter) + + if config.runOnStartup then + if not fs.exists(STARTUP_FILE) then + Util.writeFile(STARTUP_FILE, + [[os.sleep(1) +shell.openForegroundTab('packages/milo/MiloRemote')]]) + end + elseif fs.exists(STARTUP_FILE) then + fs.delete(STARTUP_FILE) + end + + elseif event.type == 'form_cancel' then + self.setup:hide() + self:setFocus(self.statusBar.filter) + + elseif event.type == 'focus_change' then + self.menuBar.infoBar:setStatus(event.focused.help) + + elseif event.type == 'eject' or event.type == 'grid_select' then + local item = self.grid:getSelected() + if item then + self:transfer(item, 1, 'requesting 1 ...') + end + + elseif event.type == 'eject_stack' then + local item = self.grid:getSelected() + if item then + self:transfer(item, 'stack', 'requesting stack ...') + end + + elseif event.type == 'eject_all' then + local item = self.grid:getSelected() + if item then + self:transfer(item, 'all', 'requesting all ...') + end + + elseif event.type == 'eject_specified' then + local item = self.grid:getSelected() + local count = tonumber(self.statusBar.amount.value) + if item and count then + self.statusBar.amount:reset() + self:setFocus(self.statusBar.filter) + self:transfer(item, count, 'requesting ' .. count .. ' ...') + else + Sound.play('entity.villager.no') + self:setStatus('nope ...') + end + + elseif event.type == 'rescan' then + self:setFocus(self.statusBar.filter) + self:refresh('scan') + self.grid:draw() + + elseif event.type == 'refresh' then + self:setFocus(self.statusBar.filter) + self:refresh('list') + self.grid:draw() + + elseif event.type == 'toggle_display' then + config.displayMode = (config.displayMode + 1) % 2 + Util.merge(event.button, displayModes[config.displayMode]) + event.button:draw() + self:applyFilter() + self:setStatus(event.button.help) + self.grid:draw() + Config.update('miloRemote', config) + + elseif event.type == 'text_change' and event.element == self.statusBar.filter then + self.filter = event.text + if #self.filter == 0 then + self.filter = nil + end + self:applyFilter() + self.grid:draw() + + else + UI.Page.eventHandler(self, event) + end + return true +end + +function page:enable() + self:setFocus(self.statusBar.filter) + Util.merge(self.statusBar.depositToggle, depositMode[config.deposit]) + UI.Page.enable(self) + if not config.server then + self.setup:show() + end + Event.onTimeout(.1, function() + self:refresh('list') + self.grid:draw() + self:sync() + end) +end + +local function splitKey(key) + local t = Util.split(key, '(.-):') + local item = { } + if #t[#t] > 8 then + item.nbtHash = table.remove(t) + end + item.damage = tonumber(table.remove(t)) + item.name = table.concat(t, ':') + return item +end + +function page:expandList(list) + local t = { } + for k,v in pairs(list) do + local item = splitKey(k) + item.has_recipe, item.count, item.displayName = v:match('(%d+):(%d+):(.+)') + item.count = tonumber(item.count) or 0 + item.lname = item.displayName:lower() + item.has_recipe = item.has_recipe == '1' + t[k] = item + end + return t +end + +function page:refresh(requestType) + self:sendRequest({ request = requestType }, 'refreshing...') +end + +function page:applyFilter() + local function filterItems(t, filter, displayMode) + if filter or displayMode > 0 then + local r = { } + if filter then + filter = filter:lower() + end + for _,v in pairs(t) do + if not filter or string.find(v.lname, filter, 1, true) then + if filter or --displayMode == 0 or + displayMode == 1 and v.count > 0 then + table.insert(r, v) + end + end + end + return r + end + return t + end + local t = filterItems(self.items, self.filter, config.displayMode) + self.grid:setValues(t) +end + +Event.addRoutine(function() + local lastTransfer + while true do + local sleepTime = 1.5 + if lastTransfer and os.clock() - lastTransfer < 3 then + sleepTime = .25 + end + + os.sleep(socket and sleepTime or 5) + if config.deposit then + local neural = device.neuralInterface + local inv = config.useShield and 'getEquipment' or 'getInventory' + if not neural or not neural[inv] then + _G._debug('missing Introspection module') + elseif config.server and (config.useShield or config.slot) then + local s, m = pcall(function() + local method = neural[inv] + local item = method and method().list()[config.useShield and SHIELD_SLOT or config.slot] + if item then + if page:sendRequest({ + request = 'deposit', + slot = config.useShield and 'shield' or config.slot, + count = item.count, + }) then + lastTransfer = os.clock() + end + end + end) + if not s and m then + _debug(m) + end + end + end + end +end) + +UI:setPage(page) +UI:pullEvents() + +if socket then + socket:close() +end diff --git a/milo/apis/craft2.lua b/milo/apis/craft2.lua new file mode 100644 index 0000000..daa76bd --- /dev/null +++ b/milo/apis/craft2.lua @@ -0,0 +1,413 @@ +local itemDB = require('itemDB') +local Util = require('util') + +local fs = _G.fs +local turtle = _G.turtle + +local Craft = { + STATUS_INFO = 'info', + STATUS_WARNING = 'warning', + STATUS_ERROR = 'error', + STATUS_SUCCESS = 'success', + + RECIPES_DIR = 'packages/core/etc/recipes', + USER_RECIPES = 'usr/config/recipes.db', + MACHINE_LOOKUP = 'usr/config/machine_crafting.db', +} + +local function splitKey(key) + local t = Util.split(key, '(.-):') + local item = { } + if #t[#t] > 8 then + item.nbtHash = table.remove(t) + end + item.damage = tonumber(table.remove(t)) + item.name = table.concat(t, ':') + return item +end + +local function makeRecipeKey(item) + if type(item) == 'string' then + item = splitKey(item) + end + return table.concat({ item.name, item.damage or 0, item.nbtHash }, ':') +end + +function Craft.clearGrid(storage) + local success = true + + for index, slot in pairs(storage.turtleInventory.adapter.list()) do + if storage:import(storage.turtleInventory, index, slot.count, slot) ~= slot.count then + success = false + end + end + return success +end + +function Craft.getItemCount(items, item) + if type(item) == 'string' then + item = splitKey(item) + end + + local count = 0 + for _,v in pairs(items) do + if v.name == item.name and + (not item.damage or v.damage == item.damage) and + v.nbtHash == item.nbtHash then + if item.damage then + return v.count + end + count = count + v.count + end + end + return count +end + +function Craft.sumIngredients(recipe) + -- produces { ['minecraft:planks:0'] = 8 } + local t = { } + for _,item in pairs(recipe.ingredients) do + t[item] = (t[item] or 0) + 1 + end + return t +end + +local function machineCraft(recipe, storage, machineName, request, count, item) + local machine = storage.nodes[machineName] + if not machine then + request.status = 'machine not found' + request.statusCode = Craft.STATUS_ERROR + return + end + + if not machine.adapter or not machine.adapter.online then + request.status = 'machine offline' + request.statusCode = Craft.STATUS_ERROR + return + end + + local list = machine.adapter.list() + for k in pairs(recipe.ingredients) do + if list[k] then + request.status = 'machine in use' + request.statusCode = Craft.STATUS_WARNING + return + end + end + + local xferred = { } + for k,v in pairs(recipe.ingredients) do + local provided = storage:export(machine, k, count, splitKey(v)) + xferred[k] = { + key = v, + count = provided, + } + if provided ~= count then + -- take back out whatever we put in + for k2,v2 in pairs(xferred) do + if v2.count > 0 then + storage:import(machine, k2, v2.count, splitKey(v2.key)) + end + end + request.status = 'Invalid recipe' + request.statusCode = Craft.STATUS_ERROR + return + end + end + request.status = 'processing' + request.statusCode = Craft.STATUS_INFO + item.pending[recipe.result] = count * recipe.count +end + +local function turtleCraft(recipe, storage, request, count) + if not Craft.clearGrid(storage) then + request.status = 'grid in use' + request.statusCode = Craft.STATUS_ERROR + return + end + + for k,v in pairs(recipe.ingredients) do + local item = splitKey(v) + if storage:export(storage.turtleInventory, k, count, item) ~= count then + request.status = 'unknown error' + request.statusCode = Craft.STATUS_ERROR + return + end + end + + turtle.select(1) + if turtle.craft() then + request.crafted = request.crafted + count * recipe.count + request.status = 'crafted' + request.statusCode = Craft.STATUS_SUCCESS + else + request.status = 'Failed to craft' + request.statusCode = Craft.STATUS_ERROR + end + Craft.clearGrid(storage) + return request.statusCode == Craft.STATUS_SUCCESS +end + +function Craft.processPending(item, storage) + for key, count in pairs(item.pending) do + local imported = storage.activity[key] + if imported then + local amount = math.min(imported, count) + storage.activity[key] = imported - amount + item.pending[key] = count - amount + item.ingredients[key].crafted = item.ingredients[key].crafted + amount + if item.pending[key] <= 0 then + item.pending[key] = nil + end + end + end +end + +function Craft.craftRecipe(recipe, count, storage, origItem) + if type(recipe) == 'string' then + recipe = Craft.recipes[recipe] + if not recipe then + return 0, 'No recipe' + end + end + + return Craft.craftRecipeInternal(recipe, count, storage, origItem) +end + +local function adjustCounts(recipe, count, ingredients) + -- decrement ingredients used + for key,icount in pairs(Craft.sumIngredients(recipe)) do + ingredients[key].count = ingredients[key].count - (icount * count) + end + + -- increment crafted + local result = ingredients[recipe.result] + result.count = result.count + (count * recipe.count) +end + +function Craft.craftRecipeInternal(recipe, count, storage, origItem) + local request = origItem.ingredients[recipe.result] + + if origItem.pending[recipe.result] then + request.status = 'processing' + request.statusCode = Craft.STATUS_INFO + return 0 + end + + local canCraft = Craft.getCraftableAmount(recipe, count, origItem.ingredients) + if not origItem.forceCrafting and canCraft == 0 then + return 0 + end + + canCraft = math.ceil(canCraft / recipe.count) + if origItem.forceCrafting then + count = math.ceil(count / recipe.count) + else + count = canCraft + end + +_G._debug({'eval', recipe.result, count }) + + --local maxCount = math.floor((recipe.maxCount or 64) / recipe.count) + local maxCount = recipe.maxCount or math.floor(64 / recipe.count) + + for key,icount in pairs(Craft.sumIngredients(recipe)) do + local itemCount = Craft.getItemCount(origItem.ingredients, key) + local need = icount * count + if recipe.craftingTools and recipe.craftingTools[key] then + need = 1 + end + maxCount = math.min(maxCount, itemDB:getMaxCount(key)) + if itemCount < need then + local irecipe = Craft.findRecipe(key) + if not irecipe then + return 0 + end + + local iqty = need - itemCount + local crafted = Craft.craftRecipeInternal(irecipe, iqty, storage, origItem) + if not origItem.forceCrafting and crafted < iqty then + return 0 + end + if origItem.forceCrafting and crafted < iqty then + canCraft = math.floor((itemCount + crafted) / icount) + end + end + end + + local crafted = 0 + while canCraft > 0 do + local batch = math.min(canCraft, maxCount) + local machine = Craft.machineLookup[recipe.result] +_G._debug({ 'crafting', recipe.result, batch }) + if machine then + if not machineCraft(recipe, storage, machine, request, batch, origItem) then + break + end + elseif not turtleCraft(recipe, storage, request, batch) then + break + end + + adjustCounts(recipe, batch, origItem.ingredients) + + crafted = crafted + batch + canCraft = canCraft - maxCount + end + + return crafted * recipe.count +end + +function Craft.findRecipe(key) + if type(key) ~= 'string' then + key = itemDB:makeKey(key) + end + + local item = itemDB:splitKey(key) + if item.damage then + return Craft.recipes[makeRecipeKey(item)] + end + + -- handle cases where the request is like : IC2:reactorVent:* + for rkey,recipe in pairs(Craft.recipes) do + local r = itemDB:splitKey(rkey) + if item.name == r.name and + (not item.nbtHash or r.nbtHash == item.nbtHash) then + return recipe + end + end +end + +-- determine the full list of ingredients needed to craft +-- a quantity of a recipe. +function Craft.getResourceList(inRecipe, items, inCount, pending) + local summed = { } + + local function sumItems(recipe, key, count) + local item = itemDB:splitKey(key) + local summedItem = summed[key] + if not summedItem then + summedItem = Util.shallowCopy(item) + summedItem.recipe = Craft.findRecipe(key) + summedItem.count = Craft.getItemCount(items, item) + summedItem.displayName = itemDB:getName(item) + summedItem.total = 0 + summedItem.need = 0 + summedItem.used = 0 + summed[key] = summedItem + end + local total = count + local used = math.min(summedItem.count, total) + local need = total - used + + if pending and pending[key] then + need = need - pending[key] + end + + if recipe.craftingTools and recipe.craftingTools[key] then + summedItem.total = 1 + if summedItem.count > 0 then + summedItem.used = 1 + summedItem.need = 0 + need = 0 + elseif not summedItem.recipe then + summedItem.need = 1 + need = 1 + else + need = 1 + end + else + summedItem.total = summedItem.total + total + summedItem.count = summedItem.count - used + summedItem.used = summedItem.used + used + if not summedItem.recipe then + summedItem.need = summedItem.need + need + end + end + + if need > 0 and summedItem.recipe then + need = math.ceil(need / summedItem.recipe.count) + for ikey,iqty in pairs(Craft.sumIngredients(summedItem.recipe)) do + sumItems(summedItem.recipe, ikey, math.ceil(need * iqty)) + end + end + end + + inCount = math.ceil(inCount / inRecipe.count) + if pending and pending[inRecipe.result] then + inCount = inCount - pending[inRecipe.result] + end + if inCount > 0 then + for ikey,iqty in pairs(Craft.sumIngredients(inRecipe)) do + sumItems(inRecipe, ikey, math.ceil(inCount * iqty)) + end + end + + return summed +end + +function Craft.getResourceList4(inRecipe, items, count) + local summed = Craft.getResourceList(inRecipe, items, count) +-- filter down to just raw materials + return Util.filter(summed, function(a) return a.used > 0 or a.need > 0 end) +end + +-- given a certain quantity, return how many of those can be crafted +function Craft.getCraftableAmount(inRecipe, inCount, items, missing) + local function sumItems(recipe, summedItems, count) + local canCraft = 0 + + for _ = 1, count do + for _,item in pairs(recipe.ingredients) do + local summedItem = summedItems[item] or Craft.getItemCount(items, item) + + local irecipe = Craft.findRecipe(item) + if irecipe and summedItem <= 0 then + summedItem = summedItem + sumItems(irecipe, summedItems, 1) + end + if summedItem <= 0 then + if missing and not irecipe then + missing.name = item + end + return canCraft + end + if not recipe.craftingTools or not recipe.craftingTools[item] then + summedItems[item] = summedItem - 1 + end + end + canCraft = canCraft + recipe.count + end + + return canCraft + end + + return sumItems(inRecipe, { }, math.ceil(inCount / inRecipe.count)) +end + +function Craft.loadRecipes() + Craft.recipes = { } + + Util.merge(Craft.recipes, (Util.readTable(fs.combine(Craft.RECIPES_DIR, 'minecraft.db')) or { }).recipes) + + local config = Util.readTable('usr/config/recipeBooks.db') or { } + for _, book in pairs(config) do + local recipeFile = Util.readTable(book) + Util.merge(Craft.recipes, recipeFile.recipes) + end + + local recipes = Util.readTable(Craft.USER_RECIPES) or { } + Util.merge(Craft.recipes, recipes) + + for k,v in pairs(Craft.recipes) do + v.result = k + end + + Craft.machineLookup = Util.readTable(Craft.MACHINE_LOOKUP) or { } +end + +function Craft.canCraft(item, count, items) + return Craft.getCraftableAmount(Craft.recipes[item], count, items) == count +end + +Craft.loadRecipes() + +return Craft diff --git a/milo/apis/milo.lua b/milo/apis/milo.lua new file mode 100644 index 0000000..b69e72c --- /dev/null +++ b/milo/apis/milo.lua @@ -0,0 +1,312 @@ +local Config = require('config') +local Craft = require('craft2') +local itemDB = require('itemDB') +local Sound = require('sound') +local Util = require('util') + +local os = _G.os +local turtle = _G.turtle + +local Milo = { + RESOURCE_FILE = 'usr/config/resources.db', +} + +function Milo:init(context) + self.context = context + context.userRecipes = Util.readTable(Craft.USER_RECIPES) or { } +end + +function Milo:getContext() + return self.context +end + +function Milo:pauseCrafting(reason) + local _, key = Util.find(self.context.state, 'key', reason.key) + if not key then + table.insert(self.context.state, reason) + os.queueEvent('milo_pause', reason) + end +end + +function Milo:resumeCrafting(reason) + local _, key = Util.find(self.context.state, 'key', reason.key) + if key then + table.remove(self.context.state, key) + local n = self.context.state[#self.context.state] + if n then + os.queueEvent('milo_pause', n) + else + os.queueEvent('milo_resume') + end + end +end + +function Milo:isCraftingPaused() + return self.context.state[#self.context.state] +end + +function Milo:getState(key) + if not self.state then + self.state = { } + Config.load('milo.state', self.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) + 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 = { } + + for _,key in pairs(Util.keys(self.context.craftingQueue)) do + local item = self.context.craftingQueue[key] + if item.crafted >= item.requested then + self.context.craftingQueue[key] = nil + end + end +end + +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 +end + +-- returns a list of items that matches along with a total count +function Milo:getMatches(item, flags) + local t = { } + local count = 0 + local items = self:listItems() + + if not flags.ignoreDamage and not flags.ignoreNbtHash then + local key = item.key or Milo:uniqueKey(item) + local v = items[key] + if v then + t[key] = Util.shallowCopy(v) + count = v.count + end + + else + for key,v in pairs(items) do + if item.name == v.name and + (flags.ignoreDamage or item.damage == v.damage) and + (flags.ignoreNbtHash or item.nbtHash == v.nbtHash) then + + t[key] = Util.shallowCopy(v) + count = count + v.count + end + end + end + + return t, count +end + +function Milo:clearGrid() + return Craft.clearGrid(self.context.storage) +end + +function Milo:getTurtleInventory() + local list = { } + + for i in pairs(self.context.turtleInventory.adapter.list()) do + local item = self.context.turtleInventory.adapter.getItemMeta(i) + if item and not itemDB:get(item) then + itemDB:add(item) + end + list[i] = item + end + + itemDB:flush() + return list +end + +function Milo:requestCrafting(item) + local key = Milo:uniqueKey(item) + + if not self.context.craftingQueue[key] then + item.crafted = 0 + item.pending = { } + item.key = key + self.context.craftingQueue[key] = item + os.queueEvent('milo_cycle') + end +end + +-- queue up an action that reliees on the crafting grid +function Milo:queueRequest(request, callback) + if Util.empty(self.context.queue) then + os.queueEvent('milo_queue') + end + table.insert(self.context.queue, { + request = request, + callback = callback + }) +end + +function Milo:craftAndEject(item, count) + local request = self:makeRequest(item, count, function(request) + -- eject rest when finished crafted + return self:eject(item, request.requested) + end) + + return request +end + +function Milo:makeRequest(item, count, callback) + local current = Milo:getItem(Milo:listItems(), item) or { count = 0 } + + if count <= 0 then + return { + requested = 0, + craft = 0, + count = 0, + current = current.count, + item = item, + key = item.key or Milo:uniqueKey(item), + } + end + + local toCraft = count - math.min(current.count, count) + if toCraft > 0 then + local recipe = Craft.findRecipe(self:uniqueKey(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(), { })) + end + end + + local request = { + requested = count, + craft = toCraft, + count = math.min(count, current.count), + current = current.count, + item = item, + key = item.key or Milo:uniqueKey(item), + } + + if request.count > 0 then + Milo:queueRequest(request, callback) + end + + if request.craft > 0 then + item = Util.shallowCopy(item) + item.requested = request.craft + item.callback = callback + self:requestCrafting(item) + end + + return request +end + +function Milo:eject(item, count) + count = self.context.storage:export(self.context.turtleInventory, nil, count, item) + Sound.play('ui.button.click') + turtle.emptyInventory() + return count +end + +function Milo:saveMachineRecipe(recipe, result, machine) + local key = Milo:uniqueKey(result) + + -- save the recipe + self.context.userRecipes[key] = recipe + Util.writeTable(Craft.USER_RECIPES, self.context.userRecipes) + + -- save the machine association + Craft.machineLookup[key] = machine + Util.writeTable(Craft.MACHINE_LOOKUP, Craft.machineLookup) + + Craft.loadRecipes() +end + +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) + if item then + item = Util.shallowCopy(item) + else + item = key + item.count = 0 + item.key = k + end + Util.merge(item, v) + item.resource = v + t[item.key] = item + end + + for k in pairs(Craft.recipes) do + local v = itemDB:splitKey(k) + local item = self:getItem(t, v) + if not item then + item = v + item.count = 0 + item.key = k + else + item = Util.shallowCopy(item) + end + t[item.key] = item + item.has_recipe = true + end + + for key in pairs(Craft.machineLookup) do + local item = t[key] + if item then + item = Util.shallowCopy(item) + item.is_craftable = true + t[item.key] = item + end + end + + for _,v in pairs(t) do + if not v.displayName then + v.displayName = itemDB:getName(v) + end + v.lname = v.displayName:lower() + end + + return t +end + +function Milo:saveResources() + Util.writeTable(self.RESOURCE_FILE, self.context.resources) +end + +-- Return a list of everything in the system +function Milo:listItems(forceRefresh, throttle) + return forceRefresh and self.context.storage:refresh(throttle) or + self.context.storage:listItems(throttle) +end + +return Milo diff --git a/milo/apis/miniAdapter.lua b/milo/apis/miniAdapter.lua new file mode 100644 index 0000000..90d472b --- /dev/null +++ b/milo/apis/miniAdapter.lua @@ -0,0 +1,56 @@ +local class = require('class') +local itemDB = require('itemDB') +local Util = require('util') + +local device = _G.device + +local Adapter = class() + +function Adapter:init(args) + if args.side then + local inventory = device[args.side] + if inventory then + Util.merge(self, inventory) + end + end +end + +function Adapter:listItems(throttle) + local cache = { } + throttle = throttle or Util.throttle() + + for k,v in pairs(self.list()) do + if v.count > 0 then + local key = table.concat({ v.name, v.damage, v.nbtHash }, ':') + + local entry = cache[key] + if not entry then + local cached = itemDB:get(v) + if cached then + cached = Util.shallowCopy(cached) + else + cached = self.getItemMeta(k) + if cached then + cached = Util.shallowCopy(itemDB:add(cached)) + end + end + if cached then + entry = cached + entry.count = 0 + cache[key] = entry + else + _G._debug('Adapter: failed to get item details') + end + end + + if entry then + entry.count = entry.count + v.count + end + throttle() + end + end + + self.cache = cache +end + +return Adapter diff --git a/milo/apis/storage.lua b/milo/apis/storage.lua new file mode 100644 index 0000000..ba0409f --- /dev/null +++ b/milo/apis/storage.lua @@ -0,0 +1,505 @@ +local Adapter = require('miniAdapter') +local class = require('class') +local Config = require('config') +local Event = require('event') +local itemDB = require('itemDB') +local sync = require('sync').sync +local Util = require('util') + +local device = _G.device +local os = _G.os +local parallel = _G.parallel + +local Storage = class() + +function Storage:init(nodes) + local defaults = { + nodes = nodes or { }, + dirty = true, + activity = { }, + storageOnline = true, + } + Util.merge(self, defaults) + + Event.on({ 'device_attach', 'device_detach' }, function(e, dev) +_G._debug('%s: %s', e, tostring(dev)) + self:initStorage() + end) + Event.onInterval(15, function() + self:showStorage() + end) +end + +function Storage:showStorage() + local t = { } + local ignores = { + ignore = true, + hidden = true, + } + for k,v in pairs(self.nodes) do + local online = v.adapter and v.adapter.online + if not online and not ignores[v.mtype] then + table.insert(t, k) + end + end + if #t > 0 then + _G._debug('Adapter:') + for _, k in pairs(t) do + _G._debug(' offline: ' .. k) + end + _G._debug('') + end +end + +function Storage:isOnline() + return self.storageOnline +end + +function Storage:initStorage() + local online = true + + _G._debug('Initializing storage') + for k,v in pairs(self.nodes) do + if v.mtype ~= 'hidden' then + if v.adapter then + v.adapter.online = not not device[k] + elseif device[k] and device[k].list and device[k].size and device[k].pullItems then + v.adapter = Adapter({ side = k }) + v.adapter.online = true + v.adapter.dirty = true + elseif device[k] then + v.adapter = device[k] + v.adapter.online = true + end + if v.mtype == 'storage' then + online = online and not not (v.adapter and v.adapter.online) + end + end + end + + if online ~= self.storageOnline then + self.storageOnline = online + -- TODO: if online, then list items + os.queueEvent(self.storageOnline and 'storage_online' or 'storage_offline', online) + _G._debug('Storage: %s', self.storageOnline and 'online' or 'offline') + end +end + +function Storage:saveConfiguration() + local t = { } + for k,v in pairs(self.nodes) do + t[k] = v.adapter + v.adapter = nil + end + + -- TODO: Should be named 'storage' + Config.update('milo', self.nodes) + + for k,v in pairs(t) do + self.nodes[k].adapter = v + end + self:initStorage() +end + +function Storage:getSingleNode(mtype) + local node = Util.find(self.nodes, 'mtype', mtype) + if node and node.adapter and node.adapter.online then + return node + end +end + +function Storage:filterNodes(mtype, filter) + local iter = { } + for _, v in pairs(self.nodes) do + if v.mtype == mtype then + if not filter or filter(v) then + table.insert(iter, v) + end + end + end + + local i = 0 + return function() + i = i + 1 + return iter[i] + end +end + +function Storage:filterActive(mtype, filter) + return self:filterNodes(mtype, function(v) + if v.adapter and v.adapter.online then + return not filter and true or filter(v) + end + end) +end + +function Storage:onlineAdapters() + local iter = { } + for _, v in pairs(self.nodes) do + if v.adapter and v.adapter.online and v.mtype == 'storage' then + table.insert(iter, v) + end + end + + table.sort(iter, function(a, b) + if not a.priority then + return false + elseif not b.priority then + return true + end + return a.priority > b.priority + end) + + local i = 0 + return function() + i = i + 1 + local a = iter[i] + if a then + return a, a.adapter + end + end +end + +function Storage:setDirty() + self.dirty = true +end + +function Storage:refresh(throttle) + self.dirty = true +_G._debug('STORAGE: Forcing full refresh') + for _, adapter in self:onlineAdapters() do + adapter.dirty = true + end + return self:listItems(throttle) +end + +local function Timer() + local ct = os.clock() + return function() + return os.clock() - ct + end +end + +-- provide a consolidated list of items +function Storage:listItems(throttle) + --sync(self, function() + if not self.dirty then + return self.cache + end + + local timer = Timer() + local cache = { } + throttle = throttle or Util.throttle() + + local t = { } + for _, adapter in self:onlineAdapters() do + if adapter.dirty then + table.insert(t, function() + adapter:listItems(throttle) + adapter.dirty = false + end) + end + end + + if #t > 0 then + _G._debug('STORAGE: refreshing ' .. #t .. ' inventories') + parallel.waitForAll(table.unpack(t)) + end + + for _, adapter in self:onlineAdapters() do + if adapter.dirty then + _G._debug('STORAGE: refreshing ' .. adapter.name) + --adapter:listItems(throttle) + --adapter.dirty = false + end + local rcache = adapter.cache or { } + for key,v in pairs(rcache) do + local entry = cache[key] + if not entry then + entry = Util.shallowCopy(v) + entry.count = v.count + entry.key = key + cache[key] = entry + else + entry.count = entry.count + v.count + end + + throttle() + end + end + itemDB:flush() + _G._debug('STORAGE: refresh in ' .. timer()) + + self.dirty = false + self.cache = cache + --end) + return self.cache +end + +function Storage:updateCache(adapter, item, count) + if not adapter.cache then + adapter.dirty = true + self.dirty = true + return + end + + local key = item.key or table.concat({ item.name, item.damage, item.nbtHash }, ':') + local entry = adapter.cache[key] + + if not entry then + if count < 0 then + _G._debug('STORAGE: update cache - count < 0', 4) + else + entry = Util.shallowCopy(item) + entry.count = count + entry.key = key + adapter.cache[key] = entry + end + else + entry.count = entry.count + count + if entry.count <= 0 then + adapter.cache[key] = nil + end + end + + if not entry then + _G._debug('STORAGE: item missing details') + adapter.dirty = true + self.dirty = true + else + local sentry = self.cache[key] + if sentry then + sentry.count = sentry.count + count + if sentry.count <= 0 then + self.cache[key] = nil + end + elseif count > 0 then + sentry = Util.shallowCopy(entry) + sentry.count = count + self.cache[key] = sentry + else + self.dirty = true + end + end +end + +function Storage:_sn(name) + if not name then + error('Invalid target', 3) + end + + local node = self.nodes[name] + if node and node.displayName then + return node.displayName + end + local t = { name:match(':(.+)_(%d+)$') } + if #t ~= 2 then + return name + end + return table.concat(t, '_') +end + +local function isValidTransfer(adapter, target) + for _,v in pairs(adapter.getTransferLocations()) do + if v == target then + return true + end + end +end + +local function rawExport(source, target, item, qty, slot) + local total = 0 + local push = isValidTransfer(source, target.name) + + local s, m = pcall(function() + local stacks = source.list() + for key,stack in Util.rpairs(stacks) do + if stack.name == item.name and + stack.damage == item.damage and + stack.nbtHash == item.nbtHash then + local amount = math.min(qty, stack.count) + if amount > 0 then + if push then + amount = source.pushItems(target.name, key, amount, slot) + else + amount = target.pullItems(source.name, key, amount, slot) + end + end + qty = qty - amount + total = total + amount + if qty <= 0 then + break + end + end + end + end) + + if not s and m then + _debug(m) + end + + return total, m +end + +function Storage:export(target, slot, count, item) + local total = 0 + local key = item.key or table.concat({ item.name, item.damage, item.nbtHash }, ':') + + local function provide(adapter) + local amount = rawExport(adapter, target.adapter, item, count, slot) + if amount > 0 then + + _G._debug('EXT: %s(%d): %s -> %s%s', + item.displayName or item.name, amount, self:_sn(adapter.name), self:_sn(target.name), + slot and string.format('[%d]', slot) or '[*]') + + self:updateCache(adapter, item, -amount) + end + count = count - amount + total = total + amount + end + + -- request from adapters with this item + for _, adapter in self:onlineAdapters() do + if adapter.cache and adapter.cache[key] then + provide(adapter) + if count <= 0 then + return total + end + end + end + + _G._debug('MISS: %s(%d): %s%s %s', + item.displayName or item.name, count, self:_sn(target.name), + slot and string.format('[%d]', slot) or '[*]', key) + +-- TODO: If there are misses when a slot is specified than something is wrong... +-- The caller should confirm the quantity beforehand +-- If no slot and full amount is not exported, then no need to check rest of adapters +-- ... so should not reach here + + return total +end + +local function rawInsert(source, target, slot, qty) + local count = 0 + + local s, m = pcall(function() + if isValidTransfer(source, target.name) then +--_debug('pull %s %s %d %d', source.name, target.name, slot, qty) + count = source.pullItems(target.name, slot, qty) + else +--_debug('push %s %s', target.name, source.name) + count = target.pushItems(source.name, slot, qty) + end + end) + if not s and m then + _debug(m) + end + + return count +end + +function Storage:import(source, slot, count, item) + if not source then error('Storage:import: source is required') end + if not slot then error('Storage:import: slot is required') end + + local total = 0 + local key = item.key or table.concat({ item.name, item.damage, item.nbtHash }, ':') + + if not self.cache then + self:listItems() + end + + local entry = itemDB:get(key) + if not entry then + if item.displayName then + -- this item already has metadata + entry = itemDB:add(item) + else + -- get the metadata from the device and add to db + entry = itemDB:add(source.adapter.getItemMeta(slot)) + end + itemDB:flush() + end + item = entry + + local function insert(adapter) + local amount = rawInsert(adapter, source.adapter, slot, count) + if amount > 0 then + + _G._debug('INS: %s(%d): %s[%d] -> %s', + item.displayName or item.name, amount, + self:_sn(source.name), slot, self:_sn(adapter.name)) + + self:updateCache(adapter, item, amount) + + -- record that we have imported this item into storage during this cycle + self.activity[key] = (self.activity[key] or 0) + amount + end + count = count - amount + total = total + amount + end + + -- find a chest locked with this item + for node in self:onlineAdapters() do + if node.lock and node.lock[key] then + insert(node.adapter, item) + if count > 0 and node.void then + total = total + self:trash(source, slot, count) + return total + end + --return total + end + if count <= 0 then + return total + end + end + + -- is this item in some chest + if self.cache[key] then + for node, adapter in self:onlineAdapters() do + if count <= 0 then + return total + end + if not node.lock and adapter.cache and adapter.cache[key] then + insert(adapter) + end + end + end + + -- high to low priority + for node in self:onlineAdapters() do + if count <= 0 then + break + end + if not node.lock then + insert(node.adapter) + end + end + + return total +end + +-- When importing items into a locked chest, trash any remaining items if full +function Storage:trash(source, slot, count) + local target = Util.find(self.nodes, 'mtype', 'trashcan') + local amount = 0 + if target and target.adapter and target.adapter.online then + local s, m = pcall(function() + _G._debug('TRA: %s[%d] (%d)', self:_sn(source.name), slot, count or 64) + --return trashcan.adapter.pullItems(source.name, slot, count) + if isValidTransfer(source.adapter, target.name) then + amount = source.adapter.pushItems(target.name, slot, count) + else + amount = target.adapter.pullItems(source.name, slot, count) + end + end) + if not s and m then + _G._debug(m) + end + end + return amount +end + +return Storage diff --git a/milo/apps/cobblegen.lua b/milo/apps/cobblegen.lua new file mode 100644 index 0000000..be7ae54 --- /dev/null +++ b/milo/apps/cobblegen.lua @@ -0,0 +1,30 @@ +_G.requireInjector(_ENV) + +local Util = require('util') + +local fs = _G.fs +local os = _G.os +local turtle = _G.turtle + +local STARTUP_FILE = 'usr/autorun/cobbleGen.lua' + +if not fs.exists(STARTUP_FILE) then + Util.writeFile(STARTUP_FILE, + [[os.sleep(1) +shell.openForegroundTab('packages/milo/apps/cobblegen')]]) +end + +os.queueEvent('turtle_inventory') +while true do + print('waiting') + os.pullEvent('turtle_inventory') + print('waiting for cobble') + for _ = 1, 20 do + if turtle.inspectDown() then + break + end + os.sleep(.1) + end + print('digging') + turtle.digDown() +end diff --git a/milo/apps/furni.lua b/milo/apps/furni.lua new file mode 100644 index 0000000..6969d3b --- /dev/null +++ b/milo/apps/furni.lua @@ -0,0 +1,166 @@ +--[[ +Use multiple furnaces at once to smelt items. + +SETUP: + Place an introspection module into the turtles inventory. + Connect turtle to milo with a wired modem. + Connect turtle to a second wired modem that is connected to furnaces ONLY. + Add as many furnaces as needed. + +CONFIGURATION: + Set turtle as a "Generic Inventory" + export coal to slot 2 + import from slot 3 + +Use this turtle for machine crafting. +--]] + +_G.requireInjector(_ENV) + +local Event = require('event') +local Peripheral = require('peripheral') +local Util = require('util') + +local device = _G.device +local fs = _G.fs +local os = _G.os +local turtle = _G.turtle + +local STARTUP_FILE = 'usr/autorun/miloFurni.lua' + +local function equip(side, item, rawName) + local equipped = Peripheral.lookup('side/' .. side) + + if equipped and equipped.type == item then + return true + end + + if not turtle.equip(side, rawName or item) then + if not turtle.selectSlotWithQuantity(0) then + error('No slots available') + end + turtle.equip(side) + if not turtle.equip(side, item) then + error('Unable to equip ' .. item) + end + end + + turtle.select(1) +end + +equip('left', 'plethora:introspection', 'plethora:module:0') +local intro = device['plethora:introspection'] +local inv = intro.getInventory() + +if not fs.exists(STARTUP_FILE) then + Util.writeFile(STARTUP_FILE, + [[os.sleep(1) +shell.openForegroundTab('packages/milo/apps/furni')]]) +end + +local furni +local localName + +print('detecting wired modem connected to furnaces...') +for _, dev in pairs(device) do + if dev.type == 'wired_modem' then + local list = dev.getNamesRemote() + furni = { } + localName = dev.getNameLocal() + for _, name in pairs(list) do + if device[name].type ~= 'minecraft:furnace' then + furni = nil + break + end + table.insert(furni, device[name]) + end + end + if furni then + print('Using wired modem: ' .. dev.name) + print('Furnaces: ' .. #furni) + break + end +end + +if not furni then + error('Turtle must be connected to a second wired_modem connected to furnaces only') +end + +_G.printError([[Program must be restarted if new furnaces are added.]]) + +-- slot 1: item to cook +-- slot 2: fuel +-- slot 3: return + +local active = false + +local function process(list) + active = false + + for _, furnace in ipairs(Util.shallowCopy(furni)) do + local f = furnace.list() + + -- items to cook + local item = list[1] + local cooking = f[1] + + if cooking or item then + active = true + end + + if item and item.count > 0 then + if not cooking or cooking.name == item.name then + local count = cooking and cooking.count or 0 + if count < 64 then + print('cooking : ' .. furnace.name) + count = furnace.pullItems(localName, 1, 8, 1) + item.count = item.count - count + Util.removeByValue(furni, furnace) + table.insert(furni, furnace) + end + end + end + + -- fuel + local fuel = f[2] or { count = 0 } + if fuel.count < 8 then + print('fueling ' ..furnace.name) + furnace.pullItems(localName, 2, 8 - fuel.count, 2) + end + + local result = f[3] + if result then + if not list[3] or result.name == list[3].name then + print('pulling from : ' .. furnace.name) + furnace.pushItems(localName, 3, result.count, 3) + list[3] = result + end + end + end + + return active +end + +Event.on('turtle_inventory', function() + print('processing') + while true do + -- furnace block updates can cause errors + local s, m = pcall(process, inv.list()) + if s and not active then + break + end + if s and not m then + _G.printError(m) + end + os.sleep(3) + end + print('idle') +end) + +Event.onInterval(5, function() + -- for some reason, it keeps stalling ... + os.queueEvent('turtle_inventory') +end) + +os.queueEvent('turtle_inventory') +Event.pullEvents() diff --git a/milo/apps/water.lua b/milo/apps/water.lua new file mode 100644 index 0000000..79f985d --- /dev/null +++ b/milo/apps/water.lua @@ -0,0 +1,29 @@ +_G.requireInjector(_ENV) + +local Util = require('util') + +local fs = _G.fs +local os = _G.os +local turtle = _G.turtle + +local STARTUP_FILE = 'usr/autorun/miloWater.lua' +if not fs.exists(STARTUP_FILE) then + Util.writeFile(STARTUP_FILE, + [[os.sleep(2) +shell.openForegroundTab('packages/milo/apps/water')]]) +end + +while true do + turtle.placeDown('minecraft:bucket:0') + turtle.placeDown('minecraft:glass_bottle:0') + for k,v in pairs(turtle.getInventory()) do + if v.name == 'minecraft:concrete_powder' then + turtle.select(k) + for _ = 1, v.count do + turtle.placeDown() + turtle.digDown() + end + end + end + os.pullEvent('turtle_inventory') +end diff --git a/milo/autorun/milo.lua b/milo/autorun/milo.lua new file mode 100644 index 0000000..91e9f22 --- /dev/null +++ b/milo/autorun/milo.lua @@ -0,0 +1,6 @@ +local device = _G.device +local shell = _ENV.shell + +if device.workbench then + shell.openForegroundTab('Milo') +end diff --git a/milo/core/learnWizard.lua b/milo/core/learnWizard.lua new file mode 100644 index 0000000..34e6633 --- /dev/null +++ b/milo/core/learnWizard.lua @@ -0,0 +1,93 @@ +local Milo = require('milo') +local UI = require('ui') + +local turtle = _G.turtle + +local learnPage = UI.Page { + titleBar = UI.TitleBar { title = 'Learn Recipe' }, + wizard = UI.Wizard { + y = 2, ey = -2, + pages = { + general = UI.Window { + index = 1, + grid = UI.ScrollingGrid { + x = 2, ex = -2, y = 2, ey = -2, + disableHeader = true, + columns = { + { heading = 'Name', key = 'name'}, + }, + sortColumn = 'name', + }, + accelerators = { + grid_select = 'nextView', + }, + }, + }, + }, + notification = UI.Notification { }, +} + +local general = learnPage.wizard.pages.general + +function general:validate() + Milo:setState('learnType', self.grid:getSelected().value) + return true +end + +function learnPage:enable() + local t = { } + + for _, page in pairs(self.wizard.pages) do + if page.validFor then + t[page.validFor] = { + name = page.validFor, + value = page.validFor, + } + end + end + general.grid:setValues(t) + general.grid:setSelected('name', Milo:getState('learnType') or '') + + Milo:pauseCrafting({ key = 'gridInUse', msg = 'Crafting paused' }) + + self:focusFirst() + UI.Page.enable(self) +end + +function learnPage:disable() + Milo:resumeCrafting({ key = 'gridInUse' }) + return UI.Page.disable(self) +end + +function learnPage.wizard:getPage(index) + local pages = { } + table.insert(pages, general) + local selected = general.grid:getSelected() + for _, page in pairs(self.pages) do + if page.validFor and (not selected or selected.value == page.validFor) then + table.insert(pages, page) + end + end + table.sort(pages, function(a, b) + return a.index < b.index + end) + + return pages[index] +end + +function learnPage:eventHandler(event) + if event.type == 'cancel' then + turtle.emptyInventory() + UI:setPreviousPage() + + elseif event.type == 'form_invalid' or event.type == 'general_error' then + self.notification:error(event.message) + self:setFocus(event.field) + + else + return UI.Page.eventHandler(self, event) + end + return true +end + +UI:addPage('learnWizard', learnPage) diff --git a/milo/core/listing.lua b/milo/core/listing.lua new file mode 100644 index 0000000..dc00592 --- /dev/null +++ b/milo/core/listing.lua @@ -0,0 +1,345 @@ +local Craft = require('craft2') +local Event = require('event') +local Milo = require('milo') +local Sound = require('sound') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors +local context = Milo:getContext() +local displayMode = Milo:getState('displayMode') or 0 +local string = _G.string + +local displayModes = { + [0] = { text = 'A', help = 'Showing all items' }, + [1] = { text = 'I', help = 'Showing inventory items' }, +} + +local page = UI.Page { + menuBar = UI.MenuBar { + buttons = { + { text = 'Learn', event = 'learn' }, + { text = 'Craft', event = 'craft' }, + { text = 'Edit', event = 'details' }, + { text = 'Refresh', event = 'refresh', x = -12 }, + { + text = '\206', + x = -3, + dropdown = { + { text = 'Setup', event = 'network' }, + UI.MenuBar.spacer, + { + text = 'Rescan storage', + event = 'rescan', + help = 'Rescan all inventories' + }, + }, + }, + }, + }, + grid = UI.Grid { + y = 2, ey = -2, + columns = { + { heading = ' Qty', key = 'count' , width = 4, justify = 'right' }, + { heading = 'Name', key = 'displayName' }, + { heading = 'Min', key = 'low' , width = 4 }, + { heading = 'Max', key = 'limit' , width = 4 }, + }, + sortColumn = Milo:getState('sortColumn') or 'count', + inverseSort = Milo:getState('inverseSort'), + }, + statusBar = UI.StatusBar { + filter = UI.TextEntry { + x = 1, ex = -17, + limit = 50, + shadowText = 'filter', + shadowTextColor = colors.gray, + backgroundColor = colors.cyan, + backgroundFocusColor = colors.cyan, + accelerators = { + [ 'enter' ] = 'eject', + }, + }, + storageStatus = UI.Text { + x = -16, ex = -9, + textColor = colors.lime, + backgroundColor = colors.cyan, + value = '', + }, + amount = UI.TextEntry { + x = -8, ex = -4, + limit = 3, + shadowText = '1', + shadowTextColor = colors.gray, + backgroundColor = colors.black, + backgroundFocusColor = colors.black, + accelerators = { + [ 'enter' ] = 'eject_specified', + }, + help = 'Specify an amount to send', + }, + display = UI.Button { + x = -3, + event = 'toggle_display', + value = 0, + text = displayModes[displayMode].text, + help = displayModes[displayMode].help, + }, + }, + notification = UI.Notification(), + throttle = UI.Throttle { + textColor = colors.yellow, + borderColor = colors.gray, + }, + accelerators = { + r = 'refresh', + [ 'control-r' ] = 'refresh', + + [ 'control-e' ] = 'eject', + [ 'control-s' ] = 'eject_stack', + [ 'control-a' ] = 'eject_all', + + [ 'control-m' ] = 'network', + + q = 'quit', + }, + allItems = { } +} + +function page.statusBar:draw() + return UI.Window.draw(self) +end + +function page.grid:getRowTextColor(row, selected) + if row.is_craftable then + return colors.yellow + end + if row.has_recipe then + return colors.cyan + end + return UI.Grid:getRowTextColor(row, selected) +end + +function page.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + row.count = row.count > 0 and Util.toBytes(row.count) + if row.low then + row.low = Util.toBytes(row.low) + end + if row.limit then + row.limit = Util.toBytes(row.limit) + end + return row +end + +function page.grid:sortCompare(a, b) + if self.sortColumn ~= 'displayName' then + if a[self.sortColumn] == b[self.sortColumn] then + if self.inverseSort then + return a.displayName > b.displayName + end + return a.displayName < b.displayName + end + if a[self.sortColumn] == 0 then + return self.inverseSort + end + if b[self.sortColumn] == 0 then + return not self.inverseSort + end + return a[self.sortColumn] < b[self.sortColumn] + end + return UI.Grid.sortCompare(self, a, b) +end + +function page.grid:eventHandler(event) + if event.type == 'grid_sort' then + Milo:setState('sortColumn', event.sortColumn) + Milo:setState('inverseSort', event.inverseSort) + end + return UI.Grid.eventHandler(self, event) +end + +function page:eject(amount) + local item = self.grid:getSelected() + if item and amount then + -- get most up-to-date item + if item then + if amount == 'stack' then + amount = item.maxCount or 64 + elseif amount == 'all' then + item = Milo:getItem(Milo:listItems(), item) + amount = item.count + end + + if amount > 0 then + item = Util.shallowCopy(item) + self.grid.values[self.grid.sorted[self.grid.index]] = item + local request = Milo:craftAndEject(item, amount) + item.count = request.current - request.count + if request.count + request.craft > 0 then + self.grid:draw() + return true + end + end + end + end + Sound.play('entity.villager.no') +end + +function page:eventHandler(event) + if event.type == 'quit' then + UI:exitPullEvents() + + elseif event.type == 'eject' or event.type == 'grid_select' then + self:eject(1) + + elseif event.type == 'eject_stack' then + self:eject('stack') + + elseif event.type == 'eject_all' then + self:eject('all') + + elseif event.type == 'eject_specified' then + if self:eject(tonumber(self.statusBar.amount.value)) then + self.statusBar.amount:reset() + self:setFocus(self.statusBar.filter) + end + + elseif event.type == 'network' then + UI:setPage('network') + + elseif event.type == 'details' or event.type == 'grid_select_right' then + local item = self.grid:getSelected() + if item then + UI:setPage('item', item) + end + + elseif event.type == 'refresh' then + self:refresh() + self.grid:draw() + self:setFocus(self.statusBar.filter) + + elseif event.type == 'rescan' then + self:refresh(true) + self.grid:draw() + self:setFocus(self.statusBar.filter) + + elseif event.type == 'toggle_display' then + displayMode = (displayMode + 1) % 2 + Util.merge(event.button, displayModes[displayMode]) + event.button:draw() + self:applyFilter() + self.grid:draw() + Milo:setState('displayMode', displayMode) + + elseif event.type == 'learn' then + UI:setPage('learnWizard') + + elseif event.type == 'craft' then + local item = self.grid:getSelected() + if item then + if Craft.findRecipe(item) then -- or item.is_craftable then + UI:setPage('craft', self.grid:getSelected()) + else + self.notification:error('No recipe defined') + end + end + + elseif event.type == 'text_change' and event.element == self.statusBar.filter then + self.filter = event.text + if #self.filter == 0 then + self.filter = nil + end + self:applyFilter() + self.grid:draw() + self.statusBar.filter:focus() + + else + UI.Page.eventHandler(self, event) + end + return true +end + +function page:enable(args) + local function updateStatus() + self.statusBar.storageStatus.value = + context.storage:isOnline() and '' or 'offline' + self.statusBar.storageStatus.textColor = + context.storage:isOnline() and colors.lime or colors.red + end + updateStatus() + + Event.onTimeout(0, function() + self:refresh() + self:draw() + self:sync() + + self.timer = Event.onInterval(3, function() + for _,v in pairs(self.grid.values) do + local c = context.storage.cache[v.key] + v.count = c and c.count or 0 + end + self.grid:draw() + self:sync() + end) + + self.handler = Event.on({ 'storage_offline', 'storage_online' }, function() + updateStatus() + self.statusBar.storageStatus:draw() + self:sync() + end) + end) + + if args and args.filter then + self.filter = args.filter + self.statusBar.filter.value = args.filter + end + + if args and args.message then + self.notification:success(args.message) + end + + self:setFocus(self.statusBar.filter) + UI.Page.enable(self) +end + +function page:disable() + Event.off(self.timer) + Event.off(self.handler) + UI.Page.disable(self) +end + +function page:refresh(force) + local throttle = function() self.throttle:update() end + + self.throttle:enable() + self.allItems = Milo:mergeResources(Milo:listItems(force, throttle)) + self:applyFilter() + self.throttle:disable() +end + +function page:applyFilter() + local function filterItems(t, filter) + if filter or displayMode > 0 then + local r = { } + if filter then + filter = filter:lower() + end + for _,v in pairs(t) do + if not filter or string.find(v.lname, filter, 1, true) then + if filter or --displayMode == 0 or + displayMode == 1 and v.count > 0 then + table.insert(r, v) + end + end + end + return r + end + return t + end + + local t = filterItems(self.allItems, self.filter) + self.grid:setValues(t) +end + +UI:addPage('listing', page) diff --git a/milo/core/machines.lua b/milo/core/machines.lua new file mode 100644 index 0000000..0cf90ec --- /dev/null +++ b/milo/core/machines.lua @@ -0,0 +1,533 @@ +local Event = require('event') +local itemDB = require('itemDB') +local Milo = require('milo') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors +local device = _G.device +local turtle = _G.turtle + +local context = Milo:getContext() + +local nodeWizard + +local networkPage = UI.Page { + titleBar = UI.TitleBar { + previousPage = true, + title = 'Network', + }, + filter = UI.TextEntry { + y = -2, x = 1, ex = -9, + limit = 50, + shadowText = 'filter', + backgroundColor = colors.cyan, + backgroundFocusColor = colors.cyan, + }, + grid = UI.ScrollingGrid { + y = 2, ey = -3, + values = context.storage.nodes, + columns = { + { key = 'suffix', width = 4, justify = 'right' }, + { heading = 'Name', key = 'displayName' }, + { heading = 'Type', key = 'mtype', width = 4 }, + { heading = 'Pri', key = 'priority', width = 3 }, + }, + sortColumn = 'displayName', + help = 'Select Node', + }, + remove = UI.Button { + y = -2, x = -4, + text = '-', event = 'remove_node', + help = 'Remove Node', + }, + statusBar = UI.StatusBar { + ex = -9, + backgroundColor = colors.lightGray, + }, + storageStatus = UI.Text { + x = -8, ex = -1, y = -1, + backgroundColor = colors.lightGray, + }, + notification = UI.Notification { }, + accelerators = { + delete = 'remove_node', + } +} + +function networkPage.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + local t = { row.name:match(':(.+)_(%d+)$') } + if #t ~= 2 then + t = { row.name:match('(.+)_(%d+)$') } + end + if t and #t == 2 then + row.name, row.suffix = table.unpack(t) + row.name = row.name .. '_' .. row.suffix + end + row.displayName = row.displayName or row.name + return row +end + +function networkPage.grid:getRowTextColor(row, selected) + if not device[row.name] then + return colors.red + end + if row.mtype == 'ignore' then + return colors.lightGray + end + return UI.Grid:getRowTextColor(row, selected) +end + +function networkPage.grid:sortCompare(a, b) + if self.sortColumn == 'displayName' then + local an = a.displayName or a.name + local bn = b.displayName or b.name + return an:lower() < bn:lower() + end + return UI.Grid.sortCompare(self, a, b) +end + +function networkPage:getList() + for _, v in pairs(device) do + if not context.storage.nodes[v.name] then + local node = { + name = v.name, + mtype = 'ignore', + category = 'ignore', + } + for _, page in pairs(nodeWizard.wizard.pages) do + if page.isValidType and page:isValidType(node) then + context.storage.nodes[v.name] = node + break + end + end + end + end +end + +function networkPage:enable() + local function updateStatus() + local isOnline = context.storage:isOnline() + self.storageStatus.value = isOnline and ' online' or 'offline' + self.storageStatus.textColor = isOnline and colors.lime or colors.red + self.storageStatus:draw() + end + + self.handler = Event.on({ 'device_attach', 'device_detach', 'storage_online', 'storage_offline' }, function() + self:getList() + self:applyFilter() + self.grid:draw() + updateStatus() + self:sync() + end) + + self:getList() + self:applyFilter() + self:setFocus(self.filter) + UI.Page.enable(self) + updateStatus() +end + +function networkPage:disable() + UI.Page.disable(self) + Event.off(self.handler) + + -- Since some storage may have been added/removed - force a full rescan + context.storage:setDirty() +end + +function networkPage:applyFilter() + local t = Util.filter(context.storage.nodes, function(v) + return v.mtype ~= 'hidden' + end) + + if #self.filter.value > 0 then + local filter = self.filter.value:lower() + t = Util.filter(t, function(v) + return v.displayName and + string.find(string.lower(v.displayName), filter, 1, true) or + string.find(string.lower(v.name), filter, 1, true) + end) + end + + self.grid:setValues(t) +end + +function networkPage:eventHandler(event) + if event.type == 'grid_select' then + if not device[event.selected.name] then + UI:setPage('machineMover', event.selected) + else + UI:setPage('nodeWizard', event.selected) + end + + elseif event.type == 'remove_node' then + local node = self.grid:getSelected() + if node then + context.storage.nodes[node.name] = nil + context.storage:saveConfiguration() + end + self:applyFilter() + self.grid:draw() + + elseif event.type == 'text_change' then + self:applyFilter() + self.grid:draw() + + elseif event.type == 'grid_focus_row' then + self.statusBar:setStatus(event.selected.name) + + elseif event.type == 'focus_change' then + self.statusBar:setStatus(event.focused.help) + + else + UI.Page.eventHandler(self, event) + end + return true +end + +nodeWizard = UI.Page { + titleBar = UI.TitleBar { title = 'Configure' }, + wizard = UI.Wizard { + y = 2, ey = -2, + pages = { + general = UI.Window { + index = 1, + backgroundColor = colors.cyan, + form = UI.Form { + x = 2, ex = -2, y = 1, ey = 3, + manualControls = true, + [1] = UI.TextEntry { + formLabel = 'Name', formKey = 'displayName', + help = 'Set a friendly name', + limit = 64, pruneEmpty = true, + }, + [2] = UI.Chooser { + width = 25, + formLabel = 'Type', formKey = 'mtype', + --nochoice = 'Storage', + help = 'Select type', + }, + }, + grid = UI.ScrollingGrid { + y = 5, ey = -2, x = 2, ex = -2, + columns = { + { heading = 'Slot', key = 'slot', width = 4 }, + { heading = 'Name', key = 'displayName', }, + { heading = 'Qty', key = 'count' , width = 3 }, + }, + sortColumn = 'slot', + help = 'Contents of inventory', + }, + }, + confirmation = UI.Window { + title = 'Confirm changes', + index = 2, + notice = UI.TextArea { + x = 2, ex = -2, y = 2, ey = -2, + value = +[[Press accept to save the changes. + +The settings will take effect immediately!]], + }, + }, + }, + }, + statusBar = UI.StatusBar { + backgroundColor = colors.cyan, + }, + notification = UI.Notification { }, + filter = UI.SlideOut { + backgroundColor = colors.cyan, + menuBar = UI.MenuBar { + buttons = { + { text = 'Save', event = 'save' }, + { text = 'Cancel', event = 'cancel' }, + }, + }, + grid = UI.ScrollingGrid { + x = 2, ex = -6, y = 2, ey = -6, + columns = { + { heading = 'Name', key = 'displayName' }, + }, + sortColumn = 'displayName', + accelerators = { + delete = 'remove_entry', + }, + }, + remove = UI.Button { + x = -4, y = 4, + text = '-', event = 'remove_entry', help = 'Remove', + }, + form = UI.Form { + x = 2, y = -4, height = 3, + margin = 1, + manualControls = true, + [1] = UI.Checkbox { + formLabel = 'Ignore Dmg', formKey = 'ignoreDamage', + help = 'Ignore damage of item', + }, + [2] = UI.Checkbox { + formLabel = 'Ignore NBT', formKey = 'ignoreNbtHash', + help = 'Ignore NBT of item', + }, + [3] = UI.Chooser { + width = 13, + formLabel = 'Mode', formKey = 'blacklist', + nochoice = 'whitelist', + choices = { + { name = 'whitelist', value = false }, + { name = 'blacklist', value = true }, + }, + help = 'Ignore damage of item' + }, + scan = UI.Button { + x = -11, y = 1, + text = 'Scan', event = 'scan_turtle', + help = 'Add items to turtle to add to filter', + }, + }, + statusBar = UI.StatusBar { + backgroundColor = colors.cyan, + }, + }, +} + +--[[ Filter slide out ]] -- +function nodeWizard.filter:show(entry, callback, whitelistOnly) + self.entry = entry + self.callback = callback + + if not self.entry.filter then + self.entry.filter = { } + end + + self.form:setValues(entry) + self:resetGrid() + + self.form[3].inactive = whitelistOnly + + UI.SlideOut.show(self) + self:setFocus(self.form.scan) + + Milo:pauseCrafting({ key = 'gridInUse', msg = 'Crafting paused' }) +end + +function nodeWizard.filter:hide() + UI.SlideOut.hide(self) + Milo:resumeCrafting({ key = 'gridInUse' }) +end + +function nodeWizard.filter:resetGrid() + local t = { } + for k in pairs(self.entry.filter) do + table.insert(t, itemDB:splitKey(k)) + end + self.grid:setValues(t) +end + +function nodeWizard.filter.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + row.displayName = itemDB:getName(row) + return row +end + +function nodeWizard.filter:eventHandler(event) + if event.type == 'focus_change' then + self.statusBar:setStatus(event.focused.help) + + elseif event.type == 'scan_turtle' then + local inventory = Milo:getTurtleInventory() + for _,item in pairs(inventory) do + self.entry.filter[Milo:uniqueKey(item)] = true + end + self:resetGrid() + self.grid:update() + self.grid:draw() + turtle.emptyInventory() + + elseif event.type == 'remove_entry' then + local row = self.grid:getSelected() + if row then + Util.removeByValue(self.grid.values, row) + self.grid:update() + self.grid:draw() + end + + elseif event.type == 'save' then + self.form:save() + self.entry.filter = { } + for _,v in pairs(self.grid.values) do + self.entry.filter[Milo:uniqueKey(v)] = true + end + self:hide() + self.callback() + + elseif event.type == 'cancel' then + self:hide() + + else + return UI.SlideOut.eventHandler(self, event) + end + return true +end + +--[[ General Page ]] -- +function nodeWizard.wizard.pages.general:enable() + UI.Window.enable(self) + self:focusFirst() +end + +function nodeWizard.wizard.pages.general:isValidFor() + return false +end + +function nodeWizard.wizard.pages.general:showInventory(node) + local inventory + + if device[node.name] and device[node.name].list then + pcall(function() + inventory = device[node.name].list() + for k,v in pairs(inventory) do + v.slot = k + end + end) + end + + self.grid:setValues(inventory or { }) +end + +function nodeWizard.wizard.pages.general.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + row.displayName = itemDB:getName(row) + return row +end + +function nodeWizard.wizard.pages.general:validate() + if self.form:save() then + nodeWizard.node.category = Util.find(nodeWizard.choices, 'value', nodeWizard.node.mtype).category + + nodeWizard.nodePages = { } + table.insert(nodeWizard.nodePages, nodeWizard.wizard.pages.general) + for _, page in pairs(nodeWizard.wizard.pages) do + if not page.isValidFor or page:isValidFor(nodeWizard.node) then + table.insert(nodeWizard.nodePages, page) + if page.setNode then + page:setNode(nodeWizard.node) + end + end + end + table.insert(nodeWizard.nodePages, nodeWizard.wizard.pages.confirmation) + return true + end +end + +--[[ Confirmation ]]-- +function nodeWizard.wizard.pages.confirmation:isValidFor() + return false +end + +--[[ Wizard ]] -- +function nodeWizard:enable(node) + local adapter = node.adapter + node.adapter = nil -- don't deep copy the adapter + self.node = Util.deepCopy(node) + self.node.adapter = adapter + node.adapter = adapter + + self.choices = { + { name = 'Ignore', value = 'ignore', category = 'ignore' }, + { name = 'Hidden', value = 'hidden', category = 'ignore', help = 'Do not show in list' }, + } + for _, page in pairs(self.wizard.pages) do + if page.isValidType then + local choice = page:isValidType(self.node) + if choice and not Util.find(self.choices, 'value', choice.value) then + table.insert(self.choices, 2, choice) + end + end + end + self.wizard.pages.general.form[1].shadowText = self.node.name + self.wizard.pages.general.form[2].choices = self.choices + self.wizard.pages.general.form:setValues(self.node) + + self.wizard.pages.general:showInventory(self.node) + + self.nodePages = { } + table.insert(self.nodePages, self.wizard.pages.general) + table.insert(self.nodePages, self.wizard.pages.confirmation) + + UI.Page.enable(self) +end + +function nodeWizard.wizard:getPage(index) + return nodeWizard.nodePages[index] +end + +function nodeWizard:eventHandler(event) + if event.type == 'cancel' then + UI:setPreviousPage() + + elseif event.type == 'accept' then + + local adapter = self.node.adapter + self.node.adapter = nil + + Util.prune(self.node, function(v) + if type(v) == 'boolean' then + return v + elseif type(v) == 'string' then + return #v > 0 + elseif type(v) == 'table' then + return not Util.empty(v) + end + return true + end) + + for _, page in pairs(self.nodePages) do + if page.saveNode then + page:saveNode(self.node) + end + end + + Util.clear(context.storage.nodes[self.node.name]) + Util.merge(context.storage.nodes[self.node.name], self.node) + context.storage.nodes[self.node.name].adapter = adapter + + context.storage:saveConfiguration() + + UI:setPreviousPage() + + elseif event.type == 'choice_change' then + local help + if event.choice and event.choice.help then + help = event.choice.help + else + help = '' + end + self.statusBar:setStatus(help) + + elseif event.type == 'edit_filter' then + self.filter:show(event.entry, event.callback, event.whitelistOnly) + + elseif event.type == 'enable_view' then + local current = event.next or event.prev + self.titleBar.title = current.title or 'Node' + self.titleBar:draw() + + elseif event.type == 'focus_change' then + self.statusBar:setStatus(event.focused.help) + + elseif event.type == 'form_invalid' or event.type == 'general_error' then + self.notification:error(event.message) + self:setFocus(event.field) + + else + return UI.Page.eventHandler(self, event) + end + return true +end + +UI:addPage('network', networkPage) +UI:addPage('nodeWizard', nodeWizard) diff --git a/milo/etc/apps/apps.db b/milo/etc/apps/apps.db new file mode 100644 index 0000000..a9a8fd5 --- /dev/null +++ b/milo/etc/apps/apps.db @@ -0,0 +1,19 @@ +{ + [ "9302912a2d9794a47241faefc475335b4e07a581" ] = { + title = "Remote", + category = "Apps", + run = "MiloRemote", + iconExt = "\0304\031f\135\129\0314\128\128\031f\130\030f\128\ +\031f\128\031c\159\149\0300\0317\143\0304\031c\149\030f\0314\133\ +\031f\128\030c\0310\142\030f\031c\149\030c\0310\139\030f\031c\149\031f\128", + }, + [ "eea426f9baef72a8fcefd091e0cec5ab94a76698" ] = { + title = "Milo", + category = "Apps", + run = "Milo", + requires = 'advancedTurtle', + iconExt = "\0304\031f\135\129\0314\128\128\031f\130\030f\128\ +\031f\128\031c\159\149\0300\0317\143\0304\031c\149\030f\0314\133\ +\031f\128\030c\0310\142\030f\031c\149\030c\0310\139\030f\031c\149\031f\128", + }, +} diff --git a/milo/plugins/activityView.lua b/milo/plugins/activityView.lua new file mode 100644 index 0000000..c2d5347 --- /dev/null +++ b/milo/plugins/activityView.lua @@ -0,0 +1,222 @@ +local Ansi = require('ansi') +local Event = require('event') +local Milo = require('milo') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors +local context = Milo:getContext() +local device = _G.device +local os = _G.os + +--[[ Configuration Page ]]-- +local template = +[[%sDisplays the amount of items entering or leaving storage.%s +Right-clicking on the activity monitor will reset the totals.]] + +local wizardPage = UI.Window { + title = 'Activity Monitor', + index = 2, + backgroundColor = colors.cyan, + [1] = UI.TextArea { + x = 2, ex = -2, y = 2, ey = 6, + marginRight = 0, + value = string.format(template, Ansi.yellow, Ansi.reset), + }, + form = UI.Form { + x = 2, ex = -2, y = 7, ey = -2, + manualControls = true, + [1] = UI.Chooser { + width = 9, + formLabel = 'Font Size', formKey = 'textScale', + nochoice = 'Small', + choices = { + { name = 'Small', value = .5 }, + { name = 'Large', value = 1 }, + }, + help = 'Adjust text scaling', + }, + }, +} + +function wizardPage:setNode(node) + self.form:setValues(node) +end + +function wizardPage:validate() + return self.form:save() +end + +function wizardPage:saveNode(node) + os.queueEvent('monitor_resize', node.name) +end + +function wizardPage:isValidType(node) + local m = device[node.name] + return m and m.type == 'monitor' and { + name = 'Activity Monitor', + value = 'activity', + category = 'display', + help = 'Display storage activity' + } +end + +function wizardPage:isValidFor(node) + return node.mtype == 'activity' +end + +UI:getPage('nodeWizard').wizard:add({ activity = wizardPage }) + +--[[ Display ]]-- +local function createPage(node) + local page = UI.Window { + parent = UI.Device { + device = node.adapter, + textScale = node.textScale or .5, + }, + grid = UI.Grid { + columns = { + { heading = 'Qty', key = 'count', width = 5 }, + { heading = 'Change', key = 'change', width = 5 }, + { heading = 'Rate', key = 'rate', width = 6 }, + { heading = 'Name', key = 'displayName' }, + }, + sortColumn = 'displayName', + }, + timestamp = os.clock(), + } + + function page.grid:getRowTextColor(row, selected) + if row.lastCount and row.lastCount ~= row.count then + return row.count > row.lastCount and colors.yellow or colors.lightGray + end + return UI.Grid:getRowTextColor(row, selected) + end + + function page.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + + local ind = '+' + if row.change < 0 then + ind = '' + end + + row.change = ind .. Util.toBytes(row.change) + row.count = Util.toBytes(row.count) + row.rate = Util.toBytes(row.rate) + + return row + end + + function page:reset() + self.lastItems = nil + self.grid:setValues({ }) + self.grid:draw() + end + + function page:refresh() + local t = context.storage.cache + + if not self.lastItems then + self.lastItems = { } + for k,v in pairs(t) do + self.lastItems[k] = { + displayName = v.displayName, + initialCount = v.count, + } + end + self.timestamp = os.clock() + self.grid:setValues({ }) + + else + for _,v in pairs(self.lastItems) do + v.lastCount = v.count + v.count = nil + end + + self.elapsed = os.clock() - self.timestamp + + for k,v in pairs(t) do + local v2 = self.lastItems[k] + if v2 then + v2.count = v.count + else + self.lastItems[k] = { + displayName = v.displayName, + count = v.count, + initialCount = 0, + } + end + end + + local changedItems = { } + for k,v in pairs(self.lastItems) do + if not v.count then + v.count = 0 + end + if v.count ~= v.initialCount then + v.change = v.count - v.initialCount + v.rate = Util.round(60 / self.elapsed * v.change, 1) + changedItems[k] = v + end + end + + self.grid:setValues(changedItems) + end + self.grid:draw() + end + + function page:update() + page:refresh() + page:sync() + end + + page:enable() + page:draw() + page:sync() + + return page +end + +local pages = { } + +Event.on('monitor_resize', function(_, side) + for node in context.storage:filterActive('activity') do + if node.name == side and pages[node.name] then + local p = pages[node.name] + p.parent:setTextScale(node.textScale or .5) + p.parent:resize() + p:resize() + p:draw() + p:sync() + break + end + end +end) + +Event.on('monitor_touch', function(_, side) + local function filter(node) + return node.adapter.name == side and pages[node.name] + end + for node in context.storage:filterActive('activity', filter) do + pages[node.name]:reset() + pages[node.name]:sync() + end +end) + +--[[ Task ]]-- +local ActivityTask = { + name = 'activity', + priority = 30, +} + +function ActivityTask:cycle() + for node in context.storage:filterActive('activity') do + if not pages[node.name] then + pages[node.name] = createPage(node) + end + pages[node.name]:update() + end +end + +Milo:registerTask(ActivityTask) diff --git a/milo/plugins/brewingStandView.lua b/milo/plugins/brewingStandView.lua new file mode 100644 index 0000000..1d11816 --- /dev/null +++ b/milo/plugins/brewingStandView.lua @@ -0,0 +1,42 @@ +local Ansi = require('ansi') +local UI = require('ui') + +local colors = _G.colors +local device = _G.device + +local template = +[[%sBrewing stands have the ability to automatically learn recipes.%s + +Simply craft potions in the brewing stand as normal except for these conditions. +1. Place item in top slot FIRST. +2. Place all 3 bottles. + +When finished brewing, the recipe will be available upon refreshing. + +Note that you do not need to import items from the brewing stand or export blaze powder, this will be done automatically.]] + +local brewingStandView = UI.Window { + title = 'Brewing Stand', + index = 2, + backgroundColor = colors.cyan, + [1] = UI.TextArea { + x = 2, ex = -2, y = 2, ey = -2, + value = string.format(template, Ansi.yellow, Ansi.reset), + }, +} + +function brewingStandView:isValidType(node) + local m = device[node.name] + return m and m.type == 'minecraft:brewing_stand'and { + name = 'Brewing Stand', + value = 'brewingStand', + category = 'machine', + help = 'Auto-learning brewing stand', + } +end + +function brewingStandView:isValidFor(node) + return node.mtype == 'brewingStand' +end + +UI:getPage('nodeWizard').wizard:add({ brewingStand = brewingStandView }) diff --git a/milo/plugins/craftTask.lua b/milo/plugins/craftTask.lua new file mode 100644 index 0000000..546027e --- /dev/null +++ b/milo/plugins/craftTask.lua @@ -0,0 +1,77 @@ +local Craft = require('craft2') +local Milo = require('milo') +local Util = require('util') + +local context = Milo:getContext() + +local craftTask = { + name = 'crafting', + priority = 70, +} + +function craftTask:craft(recipe, item) + if Milo:isCraftingPaused() then + return + end + + -- TODO: refactor into craft.lua + Craft.processPending(item, context.storage) + + -- create a mini-list of items that are required for this recipe + item.ingredients = Craft.getResourceList( + recipe, Milo:listItems(), item.requested - item.crafted, item.pending) + + for k, v in pairs(item.ingredients) do + v.crafted = v.used + v.count = v.used + v.key = k + if v.need > 0 then + v.status = 'No recipe' + v.statusCode = Craft.STATUS_ERROR + end + end + item.ingredients[recipe.result] = item + item.ingredients[recipe.result].total = item.count + item.ingredients[recipe.result].crafted = item.crafted + +--[[ +_G._p2 = item +if not item.history then + item.history = { } +end +local t = Util.shallowCopy(item) +t.history = { input = { }, output = { } } +for k,v in pairs(item.ingredients) do + t.history.input[k] = Util.shallowCopy(v) +end +table.insert(item.history, t) +]] + Craft.craftRecipe(recipe, item.requested - item.crafted, context.storage, item) + +--[[ +for k,v in pairs(item.ingredients) do + t.history.output[k] = Util.shallowCopy(v) +end +]] +end + +function craftTask:cycle() + for _,key in pairs(Util.keys(context.craftingQueue)) do + local item = context.craftingQueue[key] + if item.requested - item.crafted > 0 then + local recipe = Craft.findRecipe(key) + if recipe then + self:craft(recipe, item) + if item.callback and item.crafted >= item.requested then + item.callback(item) -- invoke callback + end + elseif not context.controllerAdapter then + item.status = '(no recipe)' + item.statusCode = Craft.STATUS_ERROR + item.crafted = 0 + end + end + end +end + +Milo:registerTask(craftTask) \ No newline at end of file diff --git a/milo/plugins/demandCraft.lua b/milo/plugins/demandCraft.lua new file mode 100644 index 0000000..c546f2d --- /dev/null +++ b/milo/plugins/demandCraft.lua @@ -0,0 +1,132 @@ +local Craft = require('craft2') +local itemDB = require('itemDB') +local Milo = require('milo') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors + +local craftPage = UI.Page { + titleBar = UI.TitleBar { }, + wizard = UI.Wizard { + y = 2, ey = -2, + pages = { + quantity = UI.Window { + index = 1, + text = UI.Text { + x = 6, y = 3, + value = 'Quantity', + }, + count = UI.TextEntry { + x = 15, y = 3, width = 10, + limit = 6, + value = 1, + }, + ejectText = UI.Text { + x = 6, y = 4, + value = 'Eject', + }, + eject = UI.Chooser { + x = 15, y = 4, width = 7, + value = true, + nochoice = 'No', + choices = { + { name = 'Yes', value = true }, + { name = 'No', value = false }, + }, + }, + }, + resources = UI.Window { + index = 2, + grid = UI.ScrollingGrid { + y = 2, ey = -2, + columns = { + { heading = 'Name', key = 'displayName' }, + { heading = 'Total', key = 'total' , width = 5 }, + { heading = 'Used', key = 'used' , width = 5 }, + { heading = 'Need', key = 'need' , width = 5 }, + }, + sortColumn = 'displayName', + }, + }, + }, + }, +} + +function craftPage:enable(item) + self.item = item + self:focusFirst() + self.titleBar.title = itemDB:getName(item) +-- self.wizard.pages.quantity.eject.value = true + UI.Page.enable(self) +end + +function craftPage.wizard.pages.resources.grid:getDisplayValues(row) + local function dv(v) + return v == 0 and '' or Util.toBytes(v) + end + row = Util.shallowCopy(row) + row.total = Util.toBytes(row.total) + row.used = dv(row.used) + row.need = dv(row.need) + return row +end + +function craftPage.wizard.pages.resources.grid:getRowTextColor(row, selected) + if row.need > 0 then + return colors.orange + end + return UI.Grid:getRowTextColor(row, selected) +end + +function craftPage.wizard:eventHandler(event) + if event.type == 'nextView' then + local count = tonumber(self.pages.quantity.count.value) + if not count or count <= 0 then + self.pages.quantity.count.backgroundColor = colors.red + self.pages.quantity.count:draw() + return false + end + self.pages.quantity.count.backgroundColor = colors.black + end + return UI.Wizard.eventHandler(self, event) +end + +function craftPage.wizard.pages.resources:enable() + local items = Milo:listItems() + local count = tonumber(self.parent.quantity.count.value) + local recipe = Craft.findRecipe(craftPage.item) + if recipe then + local ingredients = Craft.getResourceList4(recipe, items, count) + for _,v in pairs(ingredients) do + v.displayName = itemDB:getName(v) + end + self.grid:setValues(ingredients) + else + self.grid:setValues({ }) + end + return UI.Window.enable(self) +end + +function craftPage:eventHandler(event) + if event.type == 'cancel' then + UI:setPreviousPage() + + elseif event.type == 'accept' then + local item = Util.shallowCopy(self.item) + item.requested = tonumber(self.wizard.pages.quantity.count.value) + item.forceCrafting = true + if self.wizard.pages.quantity.eject.value then + item.callback = function(request) + Milo:eject(item, request.requested) + end + end + Milo:requestCrafting(item) + UI:setPreviousPage() + else + return UI.Page.eventHandler(self, event) + end + return true +end + +UI:addPage('craft', craftPage) diff --git a/milo/plugins/exportTask.lua b/milo/plugins/exportTask.lua new file mode 100644 index 0000000..c02ddf4 --- /dev/null +++ b/milo/plugins/exportTask.lua @@ -0,0 +1,89 @@ +local itemDB = require('itemDB') +local Milo = require('milo') + +local ExportTask = { + name = 'exporter', + priority = 40, +} + +local function filter(a) + return a.exports +end + +function ExportTask:cycle(context) + for node in context.storage:filterActive('machine', filter) do + local s, m = pcall(function() + for _, entry in pairs(node.exports) do + + if not entry.filter then + -- exports must have a filter + -- TODO: validate in exportView + break + end + + local function exportSingleSlot() + local slot = node.adapter.getItemMeta(entry.slot) + + if slot and slot.count == slot.maxCount then + return + end + + 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) + if (slot.name == filterItem.name and + entry.ignoreDamage or slot.damage == filterItem.damage and + entry.ignoreNbtHash or slot.nbtHash == filterItem.nbtHash) then + + local items = Milo:getMatches(filterItem, entry) + local _, item = next(items) + if item then + local count = math.min(item.count, slot.maxCount - slot.count) + context.storage:export(node, entry.slot, count, item) + end + break + end + end + return + end + + -- 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 _, item = next(items) + if item then + local count = math.min(item.count, itemDB:getMaxCount(item)) + context.storage:export(node, entry.slot, count, item) + break + end + end + end + + local function exportItems() + for key in pairs(entry.filter) do + local items = Milo:getMatches(itemDB:splitKey(key), entry) + for _,item in pairs(items) do + if context.storage:export(node, nil, item.count, item) == 0 then + -- TODO: really shouldn't break here as there may be room in other slots + -- leaving for now for performance reasons + break + end + end + end + end + if type(entry.slot) == 'number' then + exportSingleSlot() + else + exportItems() + end + end + end) + if not s and m then + _G._debug('Importer error') + _G._debug(m) + end + end +end + +Milo:registerTask(ExportTask) diff --git a/milo/plugins/exportView.lua b/milo/plugins/exportView.lua new file mode 100644 index 0000000..7b1aa53 --- /dev/null +++ b/milo/plugins/exportView.lua @@ -0,0 +1,119 @@ +local itemDB = require('itemDB') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors +local device = _G.device + +local exportView = UI.Window { + title = 'Export item into inventory', + index = 3, + grid = UI.ScrollingGrid { + x = 2, ex = -6, y = 2, ey = -4, + columns = { + { heading = 'Slot', key = 'slot', width = 4 }, + { heading = 'Filter', key = 'filter' }, + }, + sortColumn = 'slot', + help = 'Edit this entry', + accelerators = { + delete = 'remove_entry', + }, + }, + text = UI.Text { + x = 3, y = -2, + value = 'Slot', + textColor = colors.black, + }, + slots = UI.Chooser { + x = 8, y = -2, + width = 7, + nochoice = 'All', + help = 'Export to this slot', + }, + add = UI.Button { + x = 16, y = -2, + text = '+', event = 'add_entry', help = 'Add', + }, + remove = UI.Button { + x = -4, y = 4, + text = '-', event = 'remove_entry', help = 'Remove', + }, +} + +function exportView:isValidType(node) + local m = device[node.name] + return m and m.pullItems and { + name = 'Generic Inventory', + value = 'machine', + category = 'machine', + help = 'Chest, furnace... (has an inventory)' + } +end + +function exportView:isValidFor(node) + return node.mtype == 'machine' +end + +function exportView:setNode(node) + self.machine = node + if not self.machine.exports then + self.machine.exports = { } + end + self.grid:setValues(self.machine.exports) + + self.slots.choices = { + { name = 'All', value = '*' } + } + + local m = device[self.machine.name] + for k = 1, m.size() do + table.insert(self.slots.choices, { name = k, value = k }) + end +end + +function exportView.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + if not row.filter or Util.empty(row.filter) then + row.filter = 'none' + else + local t = { } + for key in pairs(row.filter) do + table.insert(t, itemDB:getName(key)) + end + row.filter = table.concat(t, ', ') + end + return row +end + +function exportView:eventHandler(event) + if event.type == 'grid_select' then + self:emit({ + type = 'edit_filter', + entry = self.grid:getSelected(), + whitelistOnly = true, + callback = function() + self.grid:update() + self.grid:draw() + end, + }) + + elseif event.type == 'add_entry' then + table.insert(self.machine.exports, { + slot = self.slots.value or '*', + filter = { }, + }) + self.grid:update() + self.grid:draw() + + elseif event.type == 'remove_entry' then + local row = self.grid:getSelected() + if row then + Util.removeByValue(self.grid.values, row) + self.grid:update() + self.grid:draw() + end + end +end + +UI:getPage('nodeWizard').wizard:add({ export = exportView }) diff --git a/milo/plugins/importTask.lua b/milo/plugins/importTask.lua new file mode 100644 index 0000000..4cf05ee --- /dev/null +++ b/milo/plugins/importTask.lua @@ -0,0 +1,68 @@ +local Milo = require('milo') + +local ImportTask = { + name = 'importer', + priority = 20, +} + +local function filter(a) + return a.imports +end + +function ImportTask:cycle(context) + for node in context.storage:filterActive('machine', filter) do + local s, m = pcall(function() + for _, entry in pairs(node.imports) do + + local function itemMatchesFilter(item) + if not entry.ignoreDamage and not entry.ignoreNbtHash then + local key = Milo:uniqueKey(item) + return entry.filter[key] + end + + for key in pairs(entry.filter) do + local v = Milo:splitKey(key) + if item.name == v.name and + (entry.ignoreDamage or item.damage == v.damage) and + (entry.ignoreNbtHash or item.nbtHash == v.nbtHash) then + return true + end + end + end + + local function matchesFilter(item) + if not entry.filter then + return true + end + + if entry.blacklist then + return not itemMatchesFilter(item) + end + + return itemMatchesFilter(item) + end + + local function importSlot(slotNo) + local item = node.adapter.getItemMeta(slotNo) + if item and matchesFilter(item) then + context.storage:import(node, slotNo, item.count, item) + end + end + + if type(entry.slot) == 'number' then + importSlot(entry.slot) + else + for i in pairs(node.adapter.list()) do + importSlot(i) + end + end + end + end) + if not s and m then + _G._debug('Importer error') + _G._debug(m) + end + end +end + +Milo:registerTask(ImportTask) diff --git a/milo/plugins/importView.lua b/milo/plugins/importView.lua new file mode 100644 index 0000000..671821b --- /dev/null +++ b/milo/plugins/importView.lua @@ -0,0 +1,118 @@ +local itemDB = require('itemDB') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors +local device = _G.device + +local importView = UI.Window { + title = 'Import item from inventory', + index = 4, + grid = UI.ScrollingGrid { + x = 2, ex = -6, y = 2, ey = -4, + columns = { + { heading = 'Slot', key = 'slot', width = 4 }, + { heading = 'Filter', key = 'filter' }, + }, + sortColumn = 'slot', + help = 'Edit this entry', + accelerators = { + delete = 'remove_entry', + }, + }, + text = UI.Text { + x = 3, y = -2, + value = 'Slot', + textColor = colors.black, + }, + slots = UI.Chooser { + x = 8, y = -2, + width = 7, + nochoice = 'All', + help = 'Import from this slot', + }, + add = UI.Button { + x = 16, y = -2, + text = '+', event = 'add_entry', help = 'Add', + }, + remove = UI.Button { + x = -4, y = 4, + text = '-', event = 'remove_entry', help = 'Remove', + }, +} + +function importView:isValidType(node) + local m = device[node.name] + return m and m.pullItems and { + name = 'Generic Inventory', + value = 'machine', + category = 'machine', + help = 'Chest, furnace... (has an inventory)', + } +end + +function importView:isValidFor(node) + return node.mtype == 'machine' +end + +function importView:setNode(node) + self.machine = node + if not self.machine.imports then + self.machine.imports = { } + end + self.grid:setValues(self.machine.imports) + + self.slots.choices = { + { name = 'All', value = '*' } + } + + local m = device[self.machine.name] + for k = 1, m.size() do + table.insert(self.slots.choices, { name = k, value = k }) + end +end + +function importView.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + if not row.filter or Util.empty(row.filter) then + row.filter = 'none' + else + local t = { } + for key in pairs(row.filter) do + table.insert(t, itemDB:getName(key)) + end + row.filter = table.concat(t, ', ') + end + return row +end + +function importView:eventHandler(event) + if event.type == 'grid_select' then + self:emit({ + type = 'edit_filter', + entry = self.grid:getSelected(), + callback = function() + self.grid:update() + self.grid:draw() + end, + }) + + elseif event.type == 'add_entry' then + table.insert(self.machine.imports, { + slot = self.slots.value or '*', + filter = { }, + }) + self.grid:update() + self.grid:draw() + + elseif event.type == 'remove_entry' then + local row = self.grid:getSelected() + if row then + Util.removeByValue(self.grid.values, row) + self.grid:update() + self.grid:draw() + end + end +end + +UI:getPage('nodeWizard').wizard:add({ import = importView }) diff --git a/milo/plugins/inputChestTask.lua b/milo/plugins/inputChestTask.lua new file mode 100644 index 0000000..85a20d2 --- /dev/null +++ b/milo/plugins/inputChestTask.lua @@ -0,0 +1,16 @@ +local Milo = require('milo') + +local InputChest = { + name = 'input', + priority = 10, +} + +function InputChest:cycle(context) + for node in context.storage:filterActive('input') do + for slot, item in pairs(node.adapter.list()) do + context.storage:import(node, slot, item.count, item) + end + end +end + +Milo:registerTask(InputChest) diff --git a/milo/plugins/inputChestView.lua b/milo/plugins/inputChestView.lua new file mode 100644 index 0000000..8a7d752 --- /dev/null +++ b/milo/plugins/inputChestView.lua @@ -0,0 +1,38 @@ +local Ansi = require('ansi') +local UI = require('ui') + +local colors = _G.colors +local device = _G.device + +--[[ Configuration Screen ]] +local template = +[[%sInput Chest%s + +Any items placed in this chest will be imported into storage. +]] + +local inputChestWizardPage = UI.Window { + title = 'Input Chest', + index = 2, + backgroundColor = colors.cyan, + [1] = UI.TextArea { + x = 2, ex = -2, y = 2, ey = -2, + value = string.format(template, Ansi.yellow, Ansi.reset), + }, +} + +function inputChestWizardPage:isValidType(node) + local m = device[node.name] + return m and m.pullItems and { + name = 'Input Chest', + value = 'input', + category = 'custom', + help = 'Sends all items to storage', + } +end + +function inputChestWizardPage:isValidFor(node) + return node.mtype == 'input' +end + +UI:getPage('nodeWizard').wizard:add({ inputChest = inputChestWizardPage }) diff --git a/milo/plugins/item.lua b/milo/plugins/item.lua new file mode 100644 index 0000000..e494f3a --- /dev/null +++ b/milo/plugins/item.lua @@ -0,0 +1,340 @@ +local Ansi = require('ansi') +local Craft = require('craft2') +local itemDB = require('itemDB') +local Milo = require('milo') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors +local device = _G.device + +local context = Milo:getContext() + +local page = UI.Page { + titleBar = UI.TitleBar { + title = 'Limit Resource', + previousPage = true, + event = 'form_cancel', + }, + form = UI.Form { + x = 1, y = 2, height = 10, ex = -1, + [1] = UI.TextEntry { + formLabel = 'Name', formKey = 'displayName', help = 'Override display name', + shadowText = 'Display name', + required = true, + limit = 120, + }, + [2] = UI.TextEntry { + width = 7, + formLabel = 'Min', formKey = 'low', help = 'Craft if below min', + validate = 'numeric', + }, + [3] = UI.TextEntry { + width = 7, + formLabel = 'Max', formKey = 'limit', help = 'Send to trash if above max', + validate = 'numeric', + }, + [4] = UI.Checkbox { + formLabel = 'Ignore Dmg', formKey = 'ignoreDamage', + help = 'Ignore damage of item', + }, + [5] = UI.Checkbox { + formLabel = 'Ignore NBT', formKey = 'ignoreNbtHash', + help = 'Ignore NBT of item', + }, + machineButton = UI.Button { + x = 2, y = -2, width = 10, + formLabel = 'Machine', + event = 'select_machine', + text = 'Assign', + }, + infoButton = UI.Button { + x = 2, y = -2, + event = 'show_info', + text = 'Info', + }, + resetButton = UI.Button { + x = 9, y = -2, + event = 'reset', + text = 'Reset', + help = 'Clear recipe and all settings', + }, + }, + rsControl = UI.SlideOut { + backgroundColor = colors.cyan, + titleBar = UI.TitleBar { + title = "Redstone Control", + }, + form = UI.Form { + y = 2, + [1] = UI.Chooser { + width = 7, + formLabel = 'RS Control', formKey = 'rsControl', + nochoice = 'No', + choices = { + { name = 'Yes', value = true }, + { name = 'No', value = false }, + }, + help = 'Control via redstone' + }, + [2] = UI.Chooser { + width = 25, + formLabel = 'RS Device', formKey = 'rsDevice', + --choices = devices, + help = 'Redstone Device' + }, + [3] = UI.Chooser { + width = 10, + formLabel = 'RS Side', formKey = 'rsSide', + --nochoice = 'No', + choices = { + { name = 'up', value = 'up' }, + { name = 'down', value = 'down' }, + { name = 'east', value = 'east' }, + { name = 'north', value = 'north' }, + { name = 'west', value = 'west' }, + { name = 'south', value = 'south' }, + }, + help = 'Output side' + }, + }, + }, + machines = UI.SlideOut { + backgroundColor = colors.cyan, + titleBar = UI.TitleBar { + title = 'Select Machine', + event = 'cancel_machine', + }, + grid = UI.ScrollingGrid { + y = 2, ey = -5, + disableHeader = true, + columns = { + { heading = 'Name', key = 'displayName'}, + }, + sortColumn = 'displayName', + }, + button1 = UI.Button { + x = -14, y = -3, + text = 'Ok', event = 'set_machine', + }, + button2 = UI.Button { + x = -9, y = -3, + text = 'Cancel', event = 'cancel_machine', + }, + statusBar = UI.StatusBar { values = 'Enter or double click to select' }, + }, + info = UI.SlideOut { + titleBar = UI.TitleBar { + title = "Information", + }, + textArea = UI.TextArea { + x = 2, ex = -2, y = 3, ey = -4, + backgroundColor = colors.black, + }, + cancel = UI.Button { + ex = -2, y = -2, width = 6, + text = 'Okay', + event = 'hide_info', + }, + }, + statusBar = UI.StatusBar { }, + notification = UI.Notification { }, +} + +function page:enable(item) + self.origItem = item + self.item = Util.shallowCopy(item) + self.res = item.resource or { } + self.res.displayName = self.item.displayName + self.form:setValues(self.res) + self.titleBar.title = item.displayName or item.name + + local machine = Craft.machineLookup[self.item.key] + self.form.machineButton.inactive = not machine + if machine then + self:filterMachines(machine) + end + + UI.Page.enable(self) + self:focusFirst() +end + +function page:filterMachines(machine) + local t = Util.filter(context.storage.nodes, function(node) + if node.category == 'machine' then + return node.adapter and node.adapter.online and node.adapter.pushItems + end + end) + self.machines.grid:setValues(t) + self.machines.grid:setSelected('name', machine) +end + +function page.machines.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + row.displayName = row.displayName or row.name + return row +end + +function page.machines.grid:getRowTextColor(row, selected) + if row.name == Craft.machineLookup[page.item.key] then + return colors.yellow + end + return UI.Grid:getRowTextColor(row, selected) +end + +function page.rsControl:enable() + local devices = self.form[2].choices + Util.clear(devices) + for _,dev in pairs(device) do + if dev.setOutput then + table.insert(devices, { name = dev.name, value = dev.name }) + end + end + + if Util.size(devices) == 0 then + table.insert(devices, { name = 'None found', values = '' }) + end + + UI.SlideOut.enable(self) +end + +function page.rsControl:eventHandler(event) + if event.type == 'form_cancel' then + self:hide() + elseif event.type == 'form_complete' then + self:hide() + else + return UI.SlideOut.eventHandler(self, event) + end + return true +end + +function page:eventHandler(event) + if event.type == 'form_cancel' then + UI:setPreviousPage() + + elseif event.type == 'show_rs' then + self.rsControl:show() + + elseif event.type == 'select_machine' then + self.machines:show() + + elseif event.type == 'reset' then + if context.userRecipes[self.item.key] then + context.userRecipes[self.item.key] = nil + Util.writeTable(Craft.USER_RECIPES, context.userRecipes) + Craft.loadRecipes() + end + + if context.resources[self.item.key] then + context.resources[self.item.key] = nil + Milo:saveResources() + end + + if Craft.machineLookup[self.item.key] then + Craft.machineLookup[self.item.key] = nil + Util.writeTable(Craft.MACHINE_LOOKUP, Craft.machineLookup) + end + + UI:setPreviousPage() + + elseif event.type == 'grid_select' then + Craft.machineLookup[self.item.key] = event.selected.name + self.machines.grid:draw() + + elseif event.type == 'set_machine' then + local machine = self.machines.grid:getSelected() + if machine then + Util.writeTable(Craft.MACHINE_LOOKUP, Craft.machineLookup) + end + self.machines:hide() + + elseif event.type == 'cancel_machine' then + self.machines:hide() + + elseif event.type == 'show_info' then + local value = + string.format('%s%s%s\n%s\n', + Ansi.orange, self.item.displayName, Ansi.reset, + self.item.name) + + if self.item.nbtHash then + value = value .. self.item.nbtHash .. '\n' + end + + value = value .. string.format('\n%sDamage:%s %s', + Ansi.yellow, Ansi.reset, self.item.damage) + + if self.item.maxDamage and self.item.maxDamage > 0 then + value = value .. string.format(' (max: %s)', self.item.maxDamage) + end + + if self.item.maxCount then + value = value .. string.format('\n%sStack Size: %s%s', + Ansi.yellow, Ansi.reset, self.item.maxCount) + end + + self.info.textArea.value = value + self.info:show() + + elseif event.type == 'hide_info' then + self.info:hide() + + elseif event.type == 'form_invalid' then + self.notification:error(event.message) + + elseif event.type == 'focus_change' then + self.statusBar:setStatus(event.focused.help) + self.statusBar:draw() + + 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) + itemDB:flush() + + -- TODO: ugh + if context.storage.cache[self.origItem.key] then + context.storage.cache[self.origItem.key].displayName = self.res.displayName + end + end + self.res.displayName = nil + Util.prune(self.res, function(v) + if type(v) == 'boolean' then + return v + elseif type(v) == 'string' then + return #v > 0 + end + return true + 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, + } + + for k,v in pairs(context.resources) do + if v == self.res then + context.resources[k] = nil + break + end + end + + if not Util.empty(self.res) then + context.resources[Milo:uniqueKey(newKey)] = self.res + end + + Milo:saveResources() + UI:setPreviousPage() + end + else + return UI.Page.eventHandler(self, event) + end + return true +end + +UI:addPage('item', page) diff --git a/milo/plugins/jobMonitor.lua b/milo/plugins/jobMonitor.lua new file mode 100644 index 0000000..0d34a47 --- /dev/null +++ b/milo/plugins/jobMonitor.lua @@ -0,0 +1,193 @@ +local Craft = require('craft2') +local Event = require('event') +local itemDB = require('itemDB') +local Milo = require('milo') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors +local context = Milo:getContext() +local device = _G.device +local os = _G.os + +--[[ Configuration Screen ]] +local wizardPage = UI.Window { + title = 'Crafting Monitor', + index = 2, + backgroundColor = colors.cyan, + [1] = UI.TextArea { + x = 2, ex = -2, y = 2, ey = 3, + marginRight = 0, + textColor = colors.yellow, + value = 'Displays the crafting progress.' + }, + form = UI.Form { + x = 2, ex = -2, y = 4, ey = -2, + manualControls = true, + [1] = UI.Chooser { + width = 9, + formLabel = 'Font Size', formKey = 'textScale', + nochoice = 'Small', + choices = { + { name = 'Small', value = .5 }, + { name = 'Large', value = 1 }, + }, + help = 'Adjust text scaling', + }, + }, +} + +function wizardPage:setNode(node) + self.form:setValues(node) +end + +function wizardPage:saveNode(node) + os.queueEvent('monitor_resize', node.name) +end + +function wizardPage:validate() + return self.form:save() +end + +function wizardPage:isValidType(node) + local m = device[node.name] + return m and m.type == 'monitor' and { + name = 'Crafting Monitor', + value = 'jobs', + category = 'display', + help = 'Display crafting progress / jobs' + } +end + +function wizardPage:isValidFor(node) + return node.mtype == 'jobs' +end + +UI:getPage('nodeWizard').wizard:add({ jobs = wizardPage }) + +--[[ Display ]] +-- TODO: some way to cancel a job +local function createPage(node) + local page = UI.Page { + parent = UI.Device { + device = node.adapter, + textScale = node.textScale or .5, + }, + grid = UI.Grid { + sortColumn = 'index', + columns = { + { heading = 'Qty', key = 'remaining', width = 4 }, + { heading = 'Crafting', key = 'displayName', }, + { heading = 'Status', key = 'status', }, + { heading = 'need', key = 'need', width = 4 }, + -- { heading = 'total', key = 'total', width = 4 }, + -- { heading = 'used', key = 'used', width = 4 }, + -- { heading = 'count', key = 'count', width = 4 }, + { heading = 'crafted', key = 'crafted', width = 5 }, + -- { heading = 'Progress', key = 'progress', width = 8 }, + }, + }, + } + + function page:updateList(craftList) + if not Milo:isCraftingPaused() then + local t = { } + for _,v in pairs(craftList) do + table.insert(t, v) + v.index = #t + for k2,v2 in pairs(v.ingredients or { }) do + if v2.key ~= v.key --[[and v2.statusCode ]] then + table.insert(t, v2) + if not v2.displayName then + v2.displayName = itemDB:getName(k2) + end + v2.index = #t + end + end + end + self.grid:setValues(t) + self.grid:update() + self:draw() + self:sync() + end + end + + function page.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + if not row.displayName then + row.displayName = itemDB:getName(row) + end + if row.requested then + row.remaining = math.max(0, row.requested - row.crafted) + else + row.displayName = ' ' .. row.displayName + end + --row.progress = string.format('%d/%d', row.crafted, row.count) + return row + end + + function page.grid:getRowTextColor(row, selected) + local statusColor = { + [ Craft.STATUS_ERROR ] = colors.red, + [ Craft.STATUS_WARNING ] = colors.orange, + [ Craft.STATUS_INFO ] = colors.yellow, + [ Craft.STATUS_SUCCESS ] = colors.green, + } + return row.statusCode and statusColor[row.statusCode] or + UI.Grid:getRowTextColor(row, selected) + end + + page:enable() + page:draw() + page:sync() + + return page +end + +local pages = { } + +Event.on('monitor_resize', function(_, side) + for node in context.storage:filterActive('jobs') do + if node.name == side and pages[node.name] then + local p = pages[node.name] + p.parent:setTextScale(node.textScale or .5) + p.parent:resize() + p:resize() + p:draw() + p:sync() + break + end + end +end) + +Event.on({ 'milo_resume', 'milo_pause' }, function(_, reason) + for node in context.storage:filterActive('jobs') do + local page = pages[node.name] + if page then + if reason then + page.grid:clear() + page.grid:centeredWrite(math.ceil(page.grid.height / 2), reason.msg) + else + page.grid:draw() + end + page:sync() + end + end +end) + +--[[ Task ]] +local task = { + name = 'job status', + priority = 80, +} + +function task:cycle() + for node in context.storage:filterActive('jobs') do + if not pages[node.name] then + pages[node.name] = createPage(node) + end + pages[node.name]:updateList(context.craftingQueue) + end +end + +Milo:registerTask(task) diff --git a/milo/plugins/learn.lua b/milo/plugins/learn.lua new file mode 100644 index 0000000..bc9235c --- /dev/null +++ b/milo/plugins/learn.lua @@ -0,0 +1 @@ +-- moved \ No newline at end of file diff --git a/milo/plugins/limitTask.lua b/milo/plugins/limitTask.lua new file mode 100644 index 0000000..1de414e --- /dev/null +++ b/milo/plugins/limitTask.lua @@ -0,0 +1,33 @@ +local Milo = require('milo') + +local LimitTask = { + name = 'limiter', + priority = 50, +} + +function LimitTask:cycle(context) + local trashcan = context.storage:filterActive('trashcan')() + + if trashcan then + for key,res in pairs(context.resources) do + if res.limit then + local items, count = Milo:getMatches(Milo:splitKey(key), res) + if count > res.limit then + local amount = count - res.limit + for _, item in pairs(items) do + amount = amount - context.storage:export( + trashcan, + nil, + math.min(amount, item.count), + item) + if amount <= 0 then + break + end + end + end + end + end + end +end + +Milo:registerTask(LimitTask) diff --git a/milo/plugins/listing.lua b/milo/plugins/listing.lua new file mode 100644 index 0000000..8a44d2f --- /dev/null +++ b/milo/plugins/listing.lua @@ -0,0 +1 @@ +--moved file diff --git a/milo/plugins/machineLearn.lua b/milo/plugins/machineLearn.lua new file mode 100644 index 0000000..b94914f --- /dev/null +++ b/milo/plugins/machineLearn.lua @@ -0,0 +1,129 @@ +local itemDB = require('itemDB') +local Milo = require('milo') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors +local device = _G.device +local turtle = _G.turtle + +local context = Milo:getContext() +local machine + +local pages = { + machines = UI.Window { + index = 2, + validFor = 'Machine Processing', + grid = UI.ScrollingGrid { + y = 2, ey = -2, + columns = { + { heading = 'Name', key = 'displayName' }, + }, + sortColumn = 'displayName', + }, + }, + confirmation = UI.Window { + index = 3, + validFor = 'Machine Processing', + notice = UI.TextArea { + x = 2, ex = -2, y = 2, ey = -2, + backgroundColor = colors.black, + value = +[[Place items in slots according to the machine's inventory. + +Place the result in the last slot of the turtle. + +Example: Slot 1 is the top slot in a furnace.]], + }, + }, +} + +function pages.machines.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + row.displayName = row.displayName or row.name + return row +end + +function pages.machines:enable() + local t = Util.filter(context.storage.nodes, function(node) + if node.category == 'machine' then + return node.adapter and node.adapter.online and node.adapter.pushItems + end + end) + self.grid:setValues(t) + UI.Window.enable(self) +end + +function pages.machines:validate() + local selected = self.grid:getSelected() + if not selected then + self:emit({ type = 'general_error', message = 'No machines configured' }) + return + end + + machine = device[selected.name] + if not machine then + self:emit({type = 'general_error', message = 'Machine not found' }) + return + end + + if not machine.size then + self:emit({ type = 'general_error', message = 'Invalid machine' }) + return + end + + return true +end + +function pages.confirmation:validate() + local inventory = Milo:getTurtleInventory() + local result = inventory[16] + local slotCount = machine.size() + + inventory[16] = nil + + if not result then + self:emit({ type = 'general_error', message = 'Result must be placed in last slot' }) + return + end + + if Util.empty(inventory) then + self:emit({ type = 'general_error', message = 'Ingredients not present' }) + return + end + + for k in pairs(inventory) do + if k > slotCount then + self:emit({ + type = 'general_error', + message = 'Slot ' .. k .. ' is not valid\nThe valid slots are 1 - ' .. machine.size() + }) + return + end + end + + -- TODO: maxCount needs to be entered by user ? ie. brewing station can only do 1 at a time + + local recipe = { + count = result.count, + ingredients = { }, + maxCount = result.maxCount ~= 64 and result.maxCount or nil, + } + + for k,v in pairs(inventory) do + recipe.ingredients[k] = Milo:uniqueKey(v) + end + + Milo:saveMachineRecipe(recipe, result, machine.name) + turtle.emptyInventory() + + local displayName = itemDB:getName(result) + UI:setPage('listing', { + filter = displayName, + message = 'Learned: ' .. displayName, + }) + + return true +end + +UI:getPage('learnWizard').wizard:add(pages) diff --git a/milo/plugins/machineMover.lua b/milo/plugins/machineMover.lua new file mode 100644 index 0000000..a2d91be --- /dev/null +++ b/milo/plugins/machineMover.lua @@ -0,0 +1,107 @@ +local Craft = require('craft2') +local Milo = require('milo') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors +local context = Milo:getContext() +local device = _G.device + +local page = UI.Page { + titleBar = UI.TitleBar { title = 'Reassign Machine' }, + grid = UI.ScrollingGrid { + y = 2, ey = -4, + values = context.storage.nodes, + columns = { + { key = 'suffix', width = 4, justify = 'right' }, + { heading = 'Name', key = 'displayName' }, + { heading = 'Type', key = 'mtype', width = 4 }, + { heading = 'Pri', key = 'priority', width = 3 }, + }, + sortColumn = 'displayName', + help = 'Select Node', + }, + accept = UI.Button { + x = -9, y = -2, + event = 'grid_select', + text = 'Accept', + }, + cancel = UI.Button { + x = -18, y = -2, + event = 'cancel', + text = 'Cancel', + }, + accelerators = { + grid_select = 'nextView', + }, + notification = UI.Notification { }, +} + +function page.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + local t = { row.name:match(':(.+)_(%d+)$') } + if #t ~= 2 then + t = { row.name:match('(.+)_(%d+)$') } + end + if t and #t == 2 then + row.name, row.suffix = table.unpack(t) + row.name = row.name .. '_' .. row.suffix + end + row.displayName = row.displayName or row.name + return row +end + +function page.grid:getRowTextColor(row, selected) + if row.mtype == 'ignore' then + return colors.lightGray + end + return UI.Grid:getRowTextColor(row, selected) +end + +function page:applyFilter() + local t = Util.filter(context.storage.nodes, function(v) + return v.mtype ~= 'hidden' and device[v.name] + end) + + self.grid:setValues(t) +end + +function page:enable(machine) + self.machine = machine + self:applyFilter() + + UI.Page.enable(self) +end + +function page:eventHandler(event) + if event.type == 'grid_select' then + local target = self.grid:getSelected() + if target then + local adapter = target.adapter + local name = target.name + Util.merge(target, self.machine) + target.adapter = adapter + target.name = name + + context.storage.nodes[self.machine.name] = nil + context.storage:saveConfiguration() + + for k,v in pairs(Craft.machineLookup) do + if v == self.machine.name then + Craft.machineLookup[k] = name + end + Util.writeTable(Craft.MACHINE_LOOKUP, Craft.machineLookup) + end + + UI:setPreviousPage() + end + + elseif event.type == 'cancel' then + UI:setPreviousPage() + + else + return UI.Page.eventHandler(self, event) + end +end + +UI:addPage('machineMover', page) diff --git a/milo/plugins/manipulatorView.lua b/milo/plugins/manipulatorView.lua new file mode 100644 index 0000000..87bd2b2 --- /dev/null +++ b/milo/plugins/manipulatorView.lua @@ -0,0 +1,78 @@ +local Ansi = require('ansi') +local Milo = require('milo') +local UI = require('ui') + +local colors = _G.colors +local device = _G.device + +--[[ Configuration Screen ]]-- +local wizardPage = UI.Window { + title = 'Manipulator', + index = 2, + backgroundColor = colors.cyan, + form = UI.Form { + x = 2, ex = -2, y = 3, ey = -2, + manualControls = true, + [1] = UI.Checkbox { + formLabel = 'Import', formKey = 'importEnder', + help = 'Locks chest to a single item type', + pruneEmpty = true, + }, + [2] = UI.TextArea { + x = 13, ex = -2, y = 2, + value = 'Automatically import the user\'s ender chest contents', + }, + }, + userInfo = UI.TextArea { + x = 3, ex = -2, y = 2, height = 2, + }, +} + +function wizardPage:isValidType(node) + local m = device[node.name] + return m and + m.type == 'manipulator' and + m.getEnder and + { + name = 'Manipulator', + value = 'manipulator', + category = 'custom', + help = 'Manipulator w/bound introspection mod' + } +end + +function wizardPage:isValidFor(node) + return node.mtype == 'manipulator' +end + +function wizardPage:setNode(node) + self.form:setValues(node) + self.userInfo.value = string.format('%sBound to: %s%s', + Ansi.black, Ansi.yellow, node.adapter.getName()) +end + +function wizardPage:validate() + return self.form:save() +end + +--UI:getPage('nodeWizard').wizard:add({ manipulator = wizardPage }) + +--[[ Task ]]-- +local task = { + name = 'manipulator', + priority = 15, +} + +function task:cycle(context) + local function filter(v) + return v.adapter.getEnder and v.importEnder + end + + for manipulator in context.storage:filterActive('manipulator', filter) do + for slot, item in pairs(manipulator.adapter.getEnder().list()) do + context.storage:import('joebodo:enderChest', slot, item.count, item) + end + end +end + +--Milo:registerTask(task) diff --git a/milo/plugins/potionImportTask.lua b/milo/plugins/potionImportTask.lua new file mode 100644 index 0000000..7f3a34a --- /dev/null +++ b/milo/plugins/potionImportTask.lua @@ -0,0 +1,86 @@ +local Craft = require('craft2') +local itemDB = require('itemDB') +local Milo = require('milo') +local Util = require('util') + +local BLAZE_POWDER = "minecraft:blaze_powder:0" + +local PotionImportTask = { + name = 'potions', + priority = 30, + brewQueue = { }, +} + +local function filter(a) + return a.adapter.type == 'minecraft:brewing_stand' +end + +function PotionImportTask:cycle(context) + for bs in context.storage:filterActive('brewingStand', filter) do + if bs.adapter.getBrewTime() == 0 then + local list = bs.adapter.list() + + -- refill blaze powder + if not list[5] then + local blazePowder = context.storage.cache[BLAZE_POWDER] + if blazePowder then + context.storage:export(bs, 5, 1, blazePowder) + else + local item = itemDB:get(BLAZE_POWDER) + if item then + item = Util.shallowCopy(item) + else + item = Milo:splitKey(BLAZE_POWDER) + end + item.requested = 1 + Milo:requestCrafting(item) + end + end + + if list[1] and not list[4] then + -- brewing has completd + + if self.brewQueue[bs.name] and list[1] then + local key = Milo:uniqueKey(list[1]) + if not Craft.findRecipe(key) then + Milo:saveMachineRecipe(self.brewQueue[bs.name], list[1], bs.name) + end + end + + for slot = 1, 3 do + if list[slot] then + context.storage:import(bs, slot, 1, list[slot]) + end + end + end + self.brewQueue[bs.name] = nil + + elseif not self.brewQueue[bs.name] then + local recipe = { + count = 3, + ingredients = { }, + maxCount = 1, + } + local list = bs.adapter.list() + + local function valid() + for i = 1, 4 do + if not list[i] then + return false + end + end + return true + end + + if valid() then + for i = 1, 4 do + recipe.ingredients[i] = Milo:uniqueKey(list[i]) + end + + self.brewQueue[bs.name] = recipe + end + end + end +end + +Milo:registerTask(PotionImportTask) diff --git a/milo/plugins/refreshTask.lua b/milo/plugins/refreshTask.lua new file mode 100644 index 0000000..3d87078 --- /dev/null +++ b/milo/plugins/refreshTask.lua @@ -0,0 +1,23 @@ +local Milo = require('milo') + +local RefreshTask = { + name = 'refresher', + priority = 0, +} + +function RefreshTask:cycle(context) + local now = os.clock() + + for node, adapter in context.storage:onlineAdapters() do + if node.refreshInterval then + if not adapter.lastRefresh or adapter.lastRefresh + node.refreshInterval < now then + _G._debug('REFRESHER: ' .. adapter.name) + context.storage.dirty = true + adapter.dirty = true + adapter.lastRefresh = now + end + end + end +end + +Milo:registerTask(RefreshTask) diff --git a/milo/plugins/remote.lua b/milo/plugins/remote.lua new file mode 100644 index 0000000..e0398e2 --- /dev/null +++ b/milo/plugins/remote.lua @@ -0,0 +1,201 @@ +local Event = require('event') +local itemDB = require('itemDB') +local Milo = require('milo') +local Socket = require('socket') + +local device = _G.device + +local SHIELD_SLOT = 2 + +local context = Milo:getContext() + +local function getNameSafe(v) + local name + local s, m = pcall(function() + name = v.getName() + end) + if not s then + _G._debug(m) + end + return name +end + +local function getManipulatorForUser(user) + for _,v in pairs(device) do + if v.type == 'manipulator' and v.getName and getNameSafe(v) == user then + return v + end + end +end + +local function compactList(list) + local c = { } + for k,v in pairs(list) do + c[k]= table.concat({ v.has_recipe and 1 or 0, v.count, v.displayName }, ':') + end + return c +end + +local function client(socket) + _G._debug('REMOTE: connection from ' .. socket.dhost) + + local user = socket:read(2) + if not user then + return + end + + local manipulator = getManipulatorForUser(user) + if not manipulator then + _G._debug('REMOTE: Manipulator with introspection module bound with user not found. Closing connection.') + socket:write({ + msg = 'Manipulator not found' + }) + socket:close() + return + end + + _G._debug('REMOTE: all good') + socket:write({ + data = 'ok', + }) + + local function makeNode(devType) + local devName = user .. ':' .. devType + local adapter = device[devName] + if adapter then + return { + adapter = adapter, + name = devName, + } + end + end + + repeat + local data = socket:read() + if not data then + break + end +--_G._debug(data) + socket.co = coroutine.running() + + if data.request == 'scan' then -- full scan of all inventories + local items = Milo:mergeResources(Milo:listItems(true)) + socket:write({ + type = 'list', + list = compactList(items), + }) + + elseif data.request == 'list' then + local items = Milo:mergeResources(Milo:listItems()) + socket:write({ + type = 'list', + list = compactList(items), + }) + + elseif data.request == 'deposit' then + local function deposit() + local devType = 'inventory' + local slotNo = data.slot + if data.slot == 'shield' then + slotNo = SHIELD_SLOT + devType = 'equipment' + end + + local node = makeNode(devType) + if node then + 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) + if item then + socket:write({ + type = 'received', + key = item.key, + count = item.count, + }) + end + end + end + end + end + + Milo:queueRequest({ }, deposit) + + elseif data.request == 'transfer' then + local count = data.count + + if count == 'stack' then + count = itemDB:getMaxCount(data.item) + elseif count == 'all' then + local item = Milo:getItem(Milo:listItems(), data.item) + count = item and item.count or 0 + end + + local function transfer(request) + local target = makeNode('inventory') + if target then + local amount = context.storage:export( + target, + nil, + request.requested, + data.item) + + local item = Milo:listItems()[request.key] + socket:write({ + type = 'transfer', + key = request.key, + requested = request.requested, + current = item and item.count or 0, + count = amount, + craft = request.craft, + }) + end + end + + local request = Milo:makeRequest(data.item, count, transfer) + if (request.craft + request.count == 0) or + (request.craft > 0 and request.count == 0) then + socket:write({ + type = 'transfer', + key = request.key, + requested = request.requested, + count = request.current, + craft = request.craft, + }) + end + end + until not socket.connected + + _G._debug('REMOTE: disconnected from ' .. socket.dhost) +end + +local handler + +local function listen() + if device.wireless_modem then + handler = Event.addRoutine(function() + _G._debug('REMOTE: listening on port 4242') + while true do + local socket = Socket.server(4242) + Event.addRoutine(function() + client(socket) + socket:close() + end) + end + end) + end +end + +Event.on({ 'device_attach', 'device_detach' }, function(_, name) + if name == 'wireless_modem' then + if handler then + handler:terminate() + handler = nil + _debug('REMOTE: wireless modem disconnected') + else + listen() + end + end +end) + +listen() diff --git a/milo/plugins/replenishTask.lua b/milo/plugins/replenishTask.lua new file mode 100644 index 0000000..ec8b5a2 --- /dev/null +++ b/milo/plugins/replenishTask.lua @@ -0,0 +1,39 @@ +local Milo = require('milo') + +local ReplenishTask = { + name = 'replenish', + priority = 60, +} + +function ReplenishTask:cycle(context) + for k,res in pairs(context.resources) do + if res.low then + local item = Milo:splitKey(k) + item.key = k + + local _, count = Milo:getMatches(item, res) + + if count < res.low then + local nbtHash + if not res.ignoreNbtHash then + nbtHash = item.nbtHash + end + Milo:requestCrafting({ + name = item.name, + damage = res.ignoreDamage and 0 or item.damage, + nbtHash = nbtHash, + requested = res.low - count, + count = count, + replenish = true, + }) + else + local request = context.craftingQueue[Milo:uniqueKey(item)] + if request and request.replenish then + --request.count = request.crafted + end + end + end + end +end + +Milo:registerTask(ReplenishTask) diff --git a/milo/plugins/speakerView.lua b/milo/plugins/speakerView.lua new file mode 100644 index 0000000..da063bd --- /dev/null +++ b/milo/plugins/speakerView.lua @@ -0,0 +1,73 @@ +local Milo = require('milo') +local Sound = require('sound') +local UI = require('ui') + +local colors = _G.colors +local context = Milo:getContext() +local device = _G.device + +local speakerNode = context.storage:getSingleNode('speaker') +if speakerNode then + Sound.setVolume(speakerNode.volume) +end + +local wizardPage = UI.Window { + title = 'Speaker', + index = 2, + backgroundColor = colors.cyan, + [1] = UI.Text { + x = 2, y = 2, + textColor = colors.yellow, + value = 'Set the volume for sound effects', + }, + form = UI.Form { + x = 2, ex = -2, y = 3, ey = -2, + manualControls = true, + volume = UI.TextEntry { + formLabel = 'Volume', formKey = 'volume', + width = 5, limit = 3, + validate = 'numeric', + help = 'A value from 0 (mute) to 1 (loud)', + }, + testSound = UI.Button { + x = 15, y = 2, + text = 'Test', event = 'test_sound', + help = 'Test sound volume', + }, + }, +} + +function wizardPage:setNode(node) + self.form:setValues(node) +end + +function wizardPage:saveNode(node) + Sound.setVolume(node.volume) +end + +function wizardPage:validate() + return self.form:save() +end + +function wizardPage:isValidType(node) + local m = device[node.name] + return m and m.type == 'speaker' and { + name = 'Speaker', + value = 'speaker', + category = 'custom', + help = 'Sound effects', + } +end + +function wizardPage:isValidFor(node) + return node.mtype == 'speaker' +end + +function wizardPage:eventHandler(event) + if event.type == 'test_sound' then + local vol = tonumber(self.form.volume.value) + Sound.play('entity.item.pickup', vol) + end +end + +UI:getPage('nodeWizard').wizard:add({ speaker = wizardPage }) diff --git a/milo/plugins/storageView.lua b/milo/plugins/storageView.lua new file mode 100644 index 0000000..bbb7c9a --- /dev/null +++ b/milo/plugins/storageView.lua @@ -0,0 +1,160 @@ +local itemDB = require('itemDB') +local UI = require('ui') + +local colors = _G.colors +local device = _G.device + +local storageView = UI.Window { + title = 'Storage Options - General', + index = 2, + backgroundColor = colors.cyan, + form = UI.Form { + x = 2, ex = -2, y = 1, ey = -2, + manualControls = true, + [1] = UI.TextEntry { + formLabel = 'Priority', formKey = 'priority', + help = 'Larger values get precedence', + limit = 4, + validate = 'numeric', + shadowText = 'Numeric priority', + }, + [2] = UI.TextEntry { + formLabel = 'Refresh', formKey = 'refreshInterval', + shadowText = 'seconds between refresh', + limit = 4, + validate = 'numeric', + help = 'Refresh periodically', + }, + [3] = UI.TextArea { + x = 12, ex = -2, y = 4, + textColor = colors.yellow, + marginRight = 0, + value = 'Only specify if you are manually taking items out of this inventory. Value should be > 10', + }, + }, +} + +function storageView:enable() + UI.Window.enable(self) + self:focusFirst() +end + +function storageView:validate() + return self.form:save() +end + +function storageView:isValidType(node) + local m = device[node.name] + return m and m.pullItems and { + name = 'Storage', + value = 'storage', + category = 'storage', + help = 'Use for item storage', + } +end + +function storageView:isValidFor(node) + return node.mtype == 'storage' +end + +function storageView:setNode(node) + self.form:setValues(node) +end + +UI:getPage('nodeWizard').wizard:add({ storageGeneral = storageView }) + +--[[ Locking Page ]]-- +local lockView = UI.Window { + title = 'Storage Options - Locking', + index = 3, + backgroundColor = colors.cyan, + form = UI.Form { + x = 2, ex = -2, y = 1, ey = 3, + manualControls = true, + [1] = UI.Checkbox { + formLabel = 'Locked', formKey = 'lockWith', + help = 'Locks chest to current item types', + }, + [2] = UI.Checkbox { + formLabel = 'Void', formKey = 'void', + help = 'Void items if locked chest is full', + }, + }, + grid = UI.ScrollingGrid { + x = 2, ex = -2, y = 5, ey = -2, + columns = { + { heading = 'Name', key = 'displayName' }, + }, + sortColumn = 'displayName', + disableHeader = true, + }, +} + +function lockView:showLockTypes() + self.grid.values = { } + if self.node.lock then + for key in pairs(self.node.lock) do + table.insert(self.grid.values, { + displayName = itemDB:getName(key), + key = key, + }) + end + end + self.grid:update() + self.grid:draw() +end + +function lockView:enable() + UI.Window.enable(self) + self:focusFirst() +end + +function lockView:validate() + return self.form:save() +end + +function lockView:isValidType(node) + local m = device[node.name] + return m and m.pullItems and { + name = 'Storage', + value = 'storage', + category = 'storage', + help = 'Use for item storage', + } +end + +function lockView:isValidFor(node) + return node.mtype == 'storage' +end + +function lockView:setNode(node) + self.node = node + self.form:setValues(node) + self:showLockTypes() +end + +function lockView:eventHandler(event) + if event.type == 'checkbox_change' and event.element.formKey == 'lockWith' then + if event.checked then + if device[self.node.name] and device[self.node.name].list then + local list = device[self.node.name].list() + if not next(list) then + self:emit({ + type = 'general_error', + field = event.element, + message = 'The chest must contain the item(s) to lock' }) + else + self.node.lock = { } + for _, slot in pairs(list) do + self.node.lock[itemDB:makeKey(slot)] = true + end + end + end + else + self.node.lock = nil + end + self:showLockTypes() + end +end + +UI:getPage('nodeWizard').wizard:add({ storageLock = lockView }) diff --git a/milo/plugins/trashcanView.lua b/milo/plugins/trashcanView.lua new file mode 100644 index 0000000..a5df858 --- /dev/null +++ b/milo/plugins/trashcanView.lua @@ -0,0 +1,91 @@ +local Milo = require('milo') +local UI = require('ui') + +local colors = _G.colors +local device = _G.device + +--[[ Configuration Screen ]] +local wizardPage = UI.Window { + title = 'Trashcan', + index = 2, + backgroundColor = colors.cyan, + info = UI.TextArea { + x = 1, ex = -1, y = 2, ey = 4, + textColor = colors.yellow, + marginLeft = 1, + marginRight = 1, + value = [[ Items can be automatically dropped from this storage.]], + }, + form = UI.Form { + x = 2, ex = -2, y = 4, ey = -2, + manualControls = true, + [1] = UI.Checkbox { + formLabel = 'Drop', formKey = 'drop', + help = 'Drop the items out of this inventory', + }, + [2] = UI.Chooser { + width = 9, + formLabel = 'Direction', formKey = 'dropDirection', + nochoice = 'Down', + choices = { + { name = 'Down', value = 'down' }, + { name = 'Up', value = 'up' }, + { name = 'North', value = 'north' }, + { name = 'South', value = 'south' }, + { name = 'East', value = 'east' }, + { name = 'West', value = 'west' }, + }, + help = 'Drop in a specified direction' + }, + }, +} + +function wizardPage:enable() + UI.Window.enable(self) + self:focusFirst() +end + +function wizardPage:validate() + return self.form:save() +end + +function wizardPage:setNode(node) + self.form:setValues(node) +end + +function wizardPage:isValidType(node) + local m = device[node.name] + return m and m.pullItems and { + name = 'Trashcan', + value = 'trashcan', + category = 'custom', + help = 'An inventory to send unwanted items', + } +end + +function wizardPage:isValidFor(node) + return node.mtype == 'trashcan' +end + +UI:getPage('nodeWizard').wizard:add({ trashcan = wizardPage }) + +--[[ TASK ]]-- +local task = { + name = 'trashcan', + priority = 90, +} + +local function filter(a) + return a.drop +end + +function task:cycle(context) + for node in context.storage:filterActive('trashcan', filter) do + for k in pairs(node.adapter.list()) do + local direction = node.dropDirection or 'down' + node.adapter.drop(k, 64, direction) + end + end +end + +Milo:registerTask(task) diff --git a/milo/plugins/turtleLearn.lua b/milo/plugins/turtleLearn.lua new file mode 100644 index 0000000..e5096ae --- /dev/null +++ b/milo/plugins/turtleLearn.lua @@ -0,0 +1,135 @@ +local Craft = require('craft2') +local itemDB = require('itemDB') +local Milo = require('milo') +local UI = require('ui') +local Util = require('util') + +local turtle = _G.turtle + +local context = Milo:getContext() + +local function learnRecipe() + local ingredients = Milo:getTurtleInventory() + + if not ingredients then + return false, 'No recipe defined' + end + + turtle.select(1) + if not turtle.craft() then + return false, 'Failed to craft' + end + + local results = Milo:getTurtleInventory() + if not results or not results[1] then + return false, 'Failed to craft' + end + + local maxCount + local newRecipe = { + ingredients = ingredients, + } + + local numResults = 0 + for _,v in pairs(results) do + if v.count > 0 then + numResults = numResults + 1 + end + end + if numResults > 1 then + for _,v1 in pairs(results) do + for _,v2 in pairs(ingredients) do + if v1.name == v2.name and + v1.nbtHash == v2.nbtHash and + (v1.damage == v2.damage or + (v1.maxDamage > 0 and v2.maxDamage > 0 and + v1.damage ~= v2.damage)) then + if not newRecipe.crafingTools then + newRecipe.craftingTools = { } + end + local tool = Util.shallowCopy(v2) + if tool.maxDamage > 0 then + tool.damage = '*' + end + + --[[ + Turtles can only craft one item at a time using a tool :( + ]]-- + maxCount = 1 + + newRecipe.craftingTools[Milo:uniqueKey(tool)] = true + v1.craftingTool = true + break + end + end + end + end + + local recipe + for _,v in pairs(results) do + if not v.craftingTool then + recipe = v + if maxCount then + recipe.maxCount = maxCount + end + break + end + end + + if not recipe then + _debug(results) + _debug(newRecipe) + error('Failed - view system log') + end + + newRecipe.count = recipe.count + + local key = Milo:uniqueKey(recipe) + if recipe.maxCount ~= 64 then + newRecipe.maxCount = recipe.maxCount + end + for k,ingredient in pairs(Util.shallowCopy(ingredients)) do + if ingredient.maxDamage > 0 then + -- ingredient.damage = '*' -- I don't think this is right + end + ingredients[k] = Milo:uniqueKey(ingredient) + end + + context.userRecipes[key] = newRecipe + Util.writeTable(Craft.USER_RECIPES, context.userRecipes) + Craft.loadRecipes() + + turtle.emptyInventory() + + return recipe +end + +local pages = { + turtleCraft = UI.Window { + index = 2, + validFor = 'Turtle Crafting', + notice = UI.TextArea { + x = 2, ex = -2, y = 2, ey = -2, + value = +[[Place recipe in turtle!]], + }, + }, +} + +function pages.turtleCraft:validate() + local recipe, msg = learnRecipe(self) + + if recipe then + local displayName = itemDB:getName(recipe) + + UI:setPage('listing', { + filter = displayName, + message = 'Learned: ' .. displayName, + }) + return true + else + self:emit({ type = 'general_error', message = msg }) + end +end + +UI:getPage('learnWizard').wizard:add(pages) diff --git a/miners/.package b/miners/.package new file mode 100644 index 0000000..dca161c --- /dev/null +++ b/miners/.package @@ -0,0 +1,8 @@ +{ + title = 'Turtle mining programs', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/miners', + description = [[Provides two types of automated mining: + * Scanning Miner: Uses a block scanner to very efficiently mine areas + * Simple Miner: A single or multi-turtle miner]], + licence = 'MIT', +} diff --git a/miners/etc/apps/apps.db b/miners/etc/apps/apps.db new file mode 100644 index 0000000..a9ea75e --- /dev/null +++ b/miners/etc/apps/apps.db @@ -0,0 +1,11 @@ +{ + [ "4486006f811b88cacd5f211fd579717e29b600cd" ] = { + title = "Miner", + category = "Apps", + icon = " \0315\\\030 \031 \ + \0304\031f _ \030 \031c/\0315\\\ + \0304 ", + run = "simpleMiner.lua", + requires = 'turtle', + }, +} diff --git a/miners/scanningMiner.lua b/miners/scanningMiner.lua new file mode 100644 index 0000000..fcffcb1 --- /dev/null +++ b/miners/scanningMiner.lua @@ -0,0 +1,632 @@ +--[[ + Efficient miner + + GPS is required. + + Miner Requires: + Diamond pick + Ender Modem + Plethora scanner + Bucket +--]] +_G.requireInjector(_ENV) + +local Event = require('event') +local GPS = require('gps') +local Point = require('point') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors +local fs = _G.fs +local os = _G.os +local peripheral = _G.peripheral +local read = _G.read +local turtle = _G.turtle + +UI:configure('scanningMiner', ...) + +local args = { ... } +local options = { + chunks = { arg = 'c', type = 'number', value = -1, + desc = 'Number of chunks to mine' }, + setTrash = { arg = 's', type = 'flag', value = false, + desc = 'Set trash items' }, + help = { arg = 'h', type = 'flag', value = false, + desc = 'Displays the options' }, +} + +local MIN_FUEL = 7500 +local LOW_FUEL = 1500 +local MAX_FUEL = turtle.getFuelLimit() + +local DICTIONARY_FILE = 'usr/config/mining.dictionary' +local PROGRESS_FILE = 'usr/config/scanning_mining.progress' +local STARTUP_FILE = 'usr/autorun/scanningMiner.lua' + +local mining +local ignores = { + ignore = true, + retain = true, +} + +local dictionary = { + data = Util.readTable(DICTIONARY_FILE) or { + [ 'minecraft:chest' ] = 'suck', + [ 'minecraft:lava' ] = 'liquid_fuel', + [ 'minecraft:flowing_lava' ] = 'liquid_fuel', + [ 'minecraft:bedrock' ] = 'ignore', + [ 'minecraft:flowing_water' ] = 'ignore', + [ 'minecraft:water' ] = 'ignore', + [ 'minecraft:air' ] = 'ignore', + [ 'minecraft:bucket' ] = 'retain', + [ 'computercraft:advanced_modem' ] = 'retain', + [ 'minecraft:diamond_pickaxe' ] = 'retain', + [ 'plethora:module' ] = 'retain', + }, +} + +function dictionary:write() + Util.writeTable(DICTIONARY_FILE, self.data) +end +function dictionary:mineable(name, damage) + self.data[name .. ':' .. damage] = nil +end +function dictionary:ignore(name, damage) + if damage then + self.data[name .. ':' .. damage] = 'ignore' + else + self.data[name] = 'ignore' + end +end +function dictionary:get(name, damage) + return self.data[name] or self.data[name .. ':' .. damage] +end +function dictionary:isTrash(name, damage) + return self:get(name, damage) == 'ignore' +end + +local page = UI.Page { + menuBar = UI.MenuBar { + buttons = { + --{ text = 'Mine', event = 'mine' }, + { text = 'Ignore', event = 'ignore' }, + { text = 'Ignore All', event = 'ignore_all' }, + }, + }, + grid = UI.Grid { + y = 2, ey = -2, + sortColumn = 'name', + columns = { + { heading = 'Count', key = 'count', width = 5 }, + { heading = 'Resource', key = 'displayName' }, + }, + }, + statusBar = UI.StatusBar { + columns = { + { key = 'status' }, + { key = 'fuel', width = 6 }, + }, + }, + accelerators = { + q = 'cancel', + } +} + +function page:eventHandler(event) + local t = self.grid:getSelected() + if t then + if event.type == 'mine' then + dictionary:mineable(t.name, t.damage) + dictionary:write() + + elseif event.type == 'ignore' then + dictionary:ignore(t.name, t.damage) + dictionary:write() + self.grid:draw() + + elseif event.type == 'ignore_all' then + dictionary:ignore(t.name) + dictionary:write() + self.grid:draw() + end + end + if event.type == 'quit' then + turtle.abort(true) + end + UI.Page.eventHandler(self, event) +end + +function page.grid:getRowTextColor(row, selected) + if dictionary:get(row.name, row.damage) == 'ignore' then + return colors.lightGray + end + if row.displayName == self.nextBlock then + return colors.yellow + end + return UI.Grid.getRowTextColor(self, row, selected) +end + +local function getChunkCoordinates(diameter, index, x, z) + local dirs = { -- circumference of grid + { xd = 0, zd = 1, heading = 1 }, -- south + { xd = -1, zd = 0, heading = 2 }, + { xd = 0, zd = -1, heading = 3 }, + { xd = 1, zd = 0, heading = 0 } -- east + } + -- always move east when entering the next diameter + if index == 0 then + dirs[4].x = x + 16 + dirs[4].z = z + return dirs[4] + end + local dir = dirs[math.floor(index / (diameter - 1)) + 1] + dir.x = x + dir.xd * 16 + dir.z = z + dir.zd * 16 + return dir +end + +local function getCornerOf(c) + return math.floor(c.x / 16) * 16, math.floor(c.z / 16) * 16 +end + +local function isFinished() + if mining.chunks ~= -1 then + local chunks = math.pow(mining.diameter-2, 2) + mining.chunkIndex + if chunks >= mining.chunks then + return true + end + end +end + +local function nextChunk() + local x, z = getCornerOf({ x = mining.x, z = mining.z }) + local points = math.pow(mining.diameter, 2) - math.pow(mining.diameter-2, 2) + mining.chunkIndex = mining.chunkIndex + 1 + + if mining.chunkIndex >= points then + mining.diameter = mining.diameter + 2 + mining.chunkIndex = 0 + end + + local nc = getChunkCoordinates(mining.diameter, mining.chunkIndex, x, z) + + -- enter next chunk + mining.x = nc.x + mining.z = nc.z + + Util.writeTable(PROGRESS_FILE, mining) + + return not isFinished() +end + +local function status(newStatus) + turtle.setStatus(newStatus) + page.statusBar:setValue('status', newStatus) + page.statusBar:draw() + page:sync() +end + +local function refuel() + if turtle.getFuelLevel() < MIN_FUEL then + local oldStatus = turtle.getStatus() + status('refueling') + + turtle.refuel('minecraft:coal:0', 32) + if turtle.getFuelLevel() < MIN_FUEL then + turtle.eachFilledSlot(function(slot) + if turtle.getFuelLevel() < MIN_FUEL then + turtle.select(slot.index) + turtle.refuel(64) + end + end) + end + status(oldStatus) + end + + turtle.select(1) +end + +local function safeGoto(x, z, y, h) + local oldStatus = turtle.getStatus() + + while not turtle._goto({ x = x, z = z, y = y or turtle.point.y, heading = h }) do + status('stuck') + if turtle.isAborted() then + return false + end + os.sleep(3) + end + turtle.setStatus(oldStatus) + return true +end + +local function safeGotoY(y) + local oldStatus = turtle.getStatus() + while not turtle.gotoY(y) do + status('stuck') + if turtle.isAborted() then + return false + end + os.sleep(1) + end + turtle.setStatus(oldStatus) + return true +end + +local function unload() + local oldStatus = turtle.getStatus() + status('unloading') + local pt = Util.shallowCopy(turtle.point) + safeGotoY(0) + + safeGoto(0, 0, 0) + if not turtle.detectUp() then + error('no chest') + end + local slots = turtle.getFilledSlots() + for _,slot in pairs(slots) do + local action = dictionary:get(slot.name, slot.damage) + if not ignores[action] then + turtle.select(slot.index) + turtle.dropUp(64) + end + end + turtle.condense() + turtle.select(1) + safeGoto(pt.x, pt.z, 0, pt.heading) + + safeGotoY(pt.y) + status(oldStatus) +end + +local function ejectTrash() + turtle.eachFilledSlot(function(slot) + if dictionary:isTrash(slot.name, slot.damage) then + turtle.select(slot.index) + turtle.dropDown(64) + end + end) +end + +local function checkSpace() + if turtle.getItemCount(15) > 0 then + refuel() + local oldStatus = turtle.getStatus() + status('condensing') + ejectTrash() + turtle.condense() + if turtle.getItemCount(15) > 0 then + unload() + end + status(oldStatus) + turtle.select(1) + end +end + +local function collectDrops(suckAction) + for _ = 1, 50 do + checkSpace() + if not suckAction() then + break + end + end +end + +local function equip(side, item) + if not turtle.equip(side, item) then + if not turtle.selectSlotWithQuantity(0) then + ejectTrash() + end + if not turtle.selectSlotWithQuantity(0) then + turtle.select(16) + turtle.drop() + end + turtle.equip(side) + if not turtle.equip(side, item) then + error('Unable to equip ' .. item) + end + end +end + +local function scan() + equip('left', 'plethora:module') + local blocks = peripheral.call('left', 'scan') + equip('left', 'minecraft:diamond_pickaxe') + + local bedrock = -256 + local counts = { } + + for _, b in pairs(blocks) do + if b.x == 0 and b.y == 0 and b.z == 0 then + b.name = 'minecraft:air' + end + b.x = b.x + turtle.point.x + b.y = b.y + turtle.point.y + b.z = b.z + turtle.point.z + + if b.name == 'minecraft:bedrock' then + if b.y > bedrock then + bedrock = b.y + end + end + end + + Util.filterInplace(blocks, function(b) + if b.y >= 0 or + (b.action == 'liquid_fuel' and b.y <= bedrock) then + return false + + elseif b.action == 'liquid_fuel' and b.damage > 0 then + return false + + elseif b.y >= bedrock then + b.action = dictionary:get(b.name, b.metadata) or 'mine' + + if ignores[b.action] then + return false + end + + local key = b.name .. ':' .. b.metadata + if not counts[key] then + counts[key] = { + displayName = key, + name = b.name, + damage = b.metadata, + count = 1 + } + else + counts[key].count = counts[key].count + 1 + end + return true + end + end) + + turtle.select(1) + + local dirty = true + + local function display() + if dirty then + page.grid:draw() + page:sync() + end + dirty = false + end + + page.grid:setValues(counts) + page.grid:draw() + display() + + status('mining') + + local i = #blocks + Point.eachClosest(turtle.point, blocks, function(b) + if turtle.isAborted() then + error('aborted') + end + + page.grid.nextBlock = b.name .. ':' .. b.metadata + + -- Get the action again in case the user has ignored via UI + b.action = dictionary:get(b.name, b.metadata) or 'mine' + if b.action == 'suck' or b.action == 'mine' then + if b.action == 'suck' then + local pt = turtle.moveAgainst(b) + collectDrops(turtle.getAction(pt.direction).suck) + end + checkSpace() + local s, m + if b.y == bedrock then + s, m = turtle.digDownAt(b) + else + s, m = turtle.digAt(b) + end + if not s then + page.statusBar:setValue('status', b.name .. ' ' .. m) + page.statusBar:draw() + page:sync() + else + page.statusBar:setValue('mining', m) + end + dirty = true + elseif b.action == 'liquid_fuel' then + if turtle.getFuelLevel() < (MAX_FUEL - 1000) then + if turtle.placeAt(b, 'minecraft:bucket:0') then + turtle.refuel() + turtle.select(1) + dirty = true + end + end + end + local key = b.name .. ':' .. b.metadata + counts[key].count = counts[key].count - 1 + i = i - 1 + display() + end) +end + +local function mineChunk() + local pts = { } + + for i = 1, math.ceil(mining.home.y / 16) do + pts[i] = { x = mining.x + 8, z = mining.z + 8, y = (i - 1) * 16 + 8 } + if pts[i].y > mining.home.y - 8 then + pts[i].y = mining.home.y - 8 + end + pts[i].y = pts[i].y - mining.home.y -- abs to rel + end + + Point.eachClosest(turtle.point, pts, function(pt) + if turtle.isAborted() then + error('aborted') + end + local chunks = math.pow(mining.diameter-2, 2) + mining.chunkIndex + status(string.format('scanning %d %d-%d', + chunks, + pt.y + mining.home.y - 8, + pt.y + mining.home.y + 8)) + + turtle.select(1) + safeGoto(pt.x, pt.z, pt.y) + scan() + + if turtle.getFuelLevel() < LOW_FUEL then + refuel() + local veryMinFuel = Point.turtleDistance(turtle.point, { x = 0, y = 0, z = 0 }) + 512 + if turtle.getFuelLevel() < veryMinFuel then + error('Not enough fuel to continue') + end + end + end) +end + +local function addTrash() + local slots = turtle.getFilledSlots() + + for _,slot in pairs(slots) do + local e = dictionary:get(slot.name, slot.damage) + if not e or e ~= 'retain' then + dictionary:ignore(slot.name, slot.damage) + end + end + + dictionary:write() +end + +-- Startup logic +if not Util.getOptions(options, args) then + return +end + +-- in plethora code, we can override initialize with a scanner version +turtle.initialize = function() + if turtle.isEquipped('modem') ~= 'right' then + equip('right', 'computercraft:advanced_modem') + end + + equip('left', 'minecraft:diamond_pickaxe') + + local function verify(item) + if not turtle.has(item) then + error('Missing: ' .. item) + end + end + + local items = { 'minecraft:bucket', 'plethora:module' } + for _,v in pairs(items) do + verify(v) + end + + --os.sleep(5) + local pt = GPS.getPoint(2) or error('GPS not found') + equip('left', 'plethora:module') + local facing = peripheral.call('left', 'getBlockMeta', 0, 0, 0).state.facing + pt.heading = Point.facings[facing].heading + turtle.setPoint(pt, true) + equip('left', 'minecraft:diamond_pickaxe') +end + +local function main() + repeat + mineChunk() + until not nextChunk() +end + +local success, msg + +if not fs.exists(DICTIONARY_FILE) or options.setTrash.value then + print('Place blocks into the turtles inventory to ignore, such as cobble, stone, gravel, etc.') + print('\nPress enter when ready') + read() + addTrash() +end + +if not fs.exists(STARTUP_FILE) then + Util.writeFile(STARTUP_FILE, + [[os.sleep(1) +shell.openForegroundTab('scanningMiner.lua')]]) + print('Autorun program created: ' .. STARTUP_FILE) +end + +Event.addRoutine(function() + turtle.reset() + + ejectTrash() + + turtle.initialize { + right = 'computercraft:advanced_modem', + left = 'minecraft:diamond_pickaxe', + required = { + 'minecraft:bucket', + 'plethora:module', + }, + GPS = true, + minFuel = 100, + -- searchFor = 'ironchest:iron_shulker_box_white' + } + + turtle.setMoveCallback(function() + page.statusBar:setValue('fuel', Util.toBytes(turtle.getFuelLevel())) + page.statusBar:draw() + page:sync() + end) + + mining = Util.readTable(PROGRESS_FILE) or { + diameter = 1, + chunkIndex = 0, + x = 0, z = 0, + chunks = options.chunks.value, + home = Point.copy(turtle.point), + heading = turtle.point.heading, -- always using east for now + } + + if options.chunks.value ~= -1 then + mining.chunks = options.chunks.value + end + + -- use coordinates relative to initial starting point + turtle.setPoint({ + x = turtle.point.x - mining.home.x, + y = turtle.point.y - mining.home.y, + z = turtle.point.z - mining.home.z, + }) + + if not fs.exists(PROGRESS_FILE) then + Util.writeTable(PROGRESS_FILE, mining) + end + + turtle.setPolicy(turtle.policies.digAttack) + turtle.setDigPolicy(turtle.digPolicies.turtleSafe) + turtle.setMovementStrategy('goto') + status('mining') + + if isFinished() then + success = false + msg = 'Mining complete' + else + success, msg = pcall(main) + end + + status(success and 'finished' or turtle.isAborted() and 'aborting' or 'error') + if turtle._goto({ x = 0, y = 0, z = 0 }) then + unload() + end + turtle.reset() + + Event.exitPullEvents() +end) + +Event.onTerminate(function() + turtle.abort(true) +end) + +UI:setPage(page) +UI:pullEvents() +UI.term:reset() + +turtle.reset() + +if not success and msg then + _G.printError(msg) +end diff --git a/apps/simpleMiner.lua b/miners/simpleMiner.lua similarity index 98% rename from apps/simpleMiner.lua rename to miners/simpleMiner.lua index 8c726b2..c4c8d6e 100644 --- a/apps/simpleMiner.lua +++ b/miners/simpleMiner.lua @@ -219,7 +219,16 @@ local function safeGoto(x, z, y, h) local oldStatus = turtle.getStatus() -- only pathfind above or around other turtles (never down) - Pathing.setBox({ x = turtle.point.x, y = turtle.point.y, z = turtle.point.z, ex = x, ey = y, ez = z }) + local box = Point.normalizeBox({ x = turtle.point.x, y = turtle.point.y, z = turtle.point.z, + ex = x, ey = y, ez = z }) + box.x = box.x - 1 + box.z = box.z - 1 + box.ex = box.ex + 1 + box.ey = box.ey + 1 + box.ez = box.ez + 1 + + Pathing.setBox(box) + while not turtle.pathfind({ x = x, z = z, y = y or turtle.point.y, heading = h }) do --status('stuck') if turtle.isAborted() then diff --git a/neural/.package b/neural/.package new file mode 100644 index 0000000..da1f204 --- /dev/null +++ b/neural/.package @@ -0,0 +1,6 @@ +{ + title = 'Programs for the neural interface', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/neural', + description = [[ WIP ]], + licence = 'MIT', +} diff --git a/neural/apis/neural/angle.lua b/neural/apis/neural/angle.lua new file mode 100644 index 0000000..f378a31 --- /dev/null +++ b/neural/apis/neural/angle.lua @@ -0,0 +1,11 @@ +local Angle = { } + +function Angle.towards(x, y, z) + return math.deg(math.atan2(-x, z)), 0 +end + +function Angle.away(x, y, z) + return math.deg(math.atan2(x, -z)), 0 +end + +return Angle diff --git a/neural/apis/neural/interface.lua b/neural/apis/neural/interface.lua new file mode 100644 index 0000000..ea36793 --- /dev/null +++ b/neural/apis/neural/interface.lua @@ -0,0 +1,139 @@ +local Interface = { } + +local Angle = require('neural.angle') +local Util = require('util') + +local device = _G.device +local os = _G.os + +local ni = device.neuralInterface or { } +for k,v in pairs(ni) do + Interface[k] = v +end + +local function yap(pt) + local x, y, z = pt.x, pt.y + 1, pt.z + local pitch = -math.atan2(y, math.atan2(-(x - .5), z - .5)) + local yaw = math.deg(math.atan2(-(x - .5), z - .5)) + + return math.deg(yaw), math.deg(pitch) +end + +function Interface.launchTo(pt, strength) + local yaw = math.deg(math.atan2(pt.x, -pt.z)) + if not strength then + local dist = math.sqrt( + math.pow(pt.x, 2) + + math.pow(pt.z, 2)) + strength = math.sqrt(math.max(32, dist) / 3) + debug(strength) + end + Interface.launch(yaw, 225, strength or 1) +end + +function Interface.dropArmor() + for i = 3, 5 do + Interface.unequip(i) + end +end + +function Interface.walkTo(pt) + local s, m = ni.walk(pt.x, pt.y, pt.z) + if not s then + _G.printError(m) + end + os.sleep(.05) + while ni.isWalking() do + os.sleep(0) + end +end + +-- flatten equipment functions +function Interface.getEquipmentList() + local l = Interface.getEquipment and Interface.getEquipment().list() or { } + + for k, v in pairs(l) do + v.slot = k + end + + return l +end + +function Interface.equip(slot) + return Interface.getEquipment and Interface.getEquipment().suck(slot) or 0 +end + +function Interface.unequip(slot) + return Interface.getEquipment and Interface.getEquipment().drop(slot) +end + +function Interface.getUniqueNames() + local t = { } + for _,v in pairs(Interface.sense()) do + t[v.name] = v.name + end + return Util.transpose(t) +end + +function Interface.lookAt(pt) + local yaw, pitch = Angle.towards(pt.x - .5, pt.y + 1, pt.z - .5) + return Interface.look(yaw, pitch) +end + +function Interface.shootAt(entity, strength) + Interface.lookAt(entity) + return Interface.shoot(strength or 1) +end + +function Interface.shootAt2(entity, strength) + local x, z = entity.x - .5, entity.z - .5 + + local function quad(a, b, c) + if math.abs(a) < 1e-6 then + if math.abs(b) < 1e-6 then + return math.abs(c) < 1e-6 and 0, 0 + else + return -c/b, -c/b + end + else + local disc = b*b - 4*a*c + if disc >= 0 then + disc = math.sqrt(disc) + a = 2*a + return (-b-disc)/a, (-b+disc)/a + end + end + end + + local v = .025 -- velocity of arrow + + local tvx = entity.motionX + local tvz = entity.motionZ + local a = tvx*tvx + tvz*tvz - v*v + local b = 2 * (tvx * x + tvz * z) + local c = x * x + z * z + local t0, t1 = quad(a, b, c) + if t0 then + local t = math.min(t0, t1) + if t < 0 then + t = math.max(t0, t1) + end + if t > 0 then + --Util.print({ x, t, tvx, x + tvx * t }) + x = x + tvx * t + z = z + tvz * t + end + end + + local yaw = math.deg(math.atan2(-(x - .5), z - .5)) + local pitch = -math.deg(math.atan2(entity.y, math.sqrt(x * x + z * z))) + + Interface.look(yaw, pitch) -- pitch is broken + return Interface.shoot(strength or 1) +end + +function Interface.setStatus(s) + ni.status = s +end + +return Interface diff --git a/neural/apis/neural/mobs.lua b/neural/apis/neural/mobs.lua new file mode 100644 index 0000000..88b6cf8 --- /dev/null +++ b/neural/apis/neural/mobs.lua @@ -0,0 +1,22 @@ +local Mobs = { } + +local hostiles = { + ancient_golem = true, + BabySkeleton = true, + BabyZombie = true, + Bat = true, + Creeper = true, + Husk = true, + Skeleton = true, + Slime = true, + Spider = true, + Witch = true, + Zombie = true, + ZombieVillager = true, +} + +function Mobs.getNames() + return hostiles +end + +return Mobs diff --git a/neural/autorun/interface.lua b/neural/autorun/interface.lua new file mode 100644 index 0000000..ab63796 --- /dev/null +++ b/neural/autorun/interface.lua @@ -0,0 +1,24 @@ +_G.requireInjector(_ENV) + +local GPS = require('gps') + +local device = _G.device + +if device.neuralInterface and device.wireless_modem then + device.neuralInterface.goTo = function(x, _, z) + local pt = GPS.locate(2) + if pt then + return pcall(function() + local gpt = { + x = x - pt.x, + y = 0, + z = z - pt.z, + } + gpt.x = math.min(math.max(gpt.x, -15), 15) + gpt.z = math.min(math.max(gpt.z, -15), 15) + return device.neuralInterface.walk(gpt.x, gpt.y, gpt.z) + end) + end + return false, 'No GPS' + end +end diff --git a/neural/etc/apps/neural-apps.db b/neural/etc/apps/neural-apps.db new file mode 100644 index 0000000..3004314 --- /dev/null +++ b/neural/etc/apps/neural-apps.db @@ -0,0 +1,22 @@ +{ + [ "shootingGallery" ] = { + title = "Gallery", + category = "Neural", + run = "shootingGallery.lua", + }, + [ "neuralFight" ] = { + title = "Fight", + category = "Neural", + run = "neuralFight.lua", + }, + [ "neuralFly" ] = { + title = "Fly", + category = "Neural", + run = "neuralFly.lua", + }, + [ "neuralRemote" ] = { + title = "Remote", + category = "Neural", + run = "neuralRemote.lua", + }, +} diff --git a/neural/mobfollow.lua b/neural/mobfollow.lua new file mode 100644 index 0000000..5c819fc --- /dev/null +++ b/neural/mobfollow.lua @@ -0,0 +1,49 @@ +_G.requireInjector(_ENV) + +local GPS = require('gps') +local Util = require('util') +local Peripheral = require('peripheral') +local Point = require('point') + +local os = _G.os + +local args = { ... } +local remoteId = args[1] or error('mobFollow ') +local ni = Peripheral.lookup(remoteId .. '://name/neuralInterface') + +if not ni then + error('failed to connect') +end + +local lpt = nil + +while true do + local pt = GPS.locate(2) + + if not pt then + print('No GPS') + else + local gpt = Util.shallowCopy(pt) + if pt and lpt and Point.same(pt, lpt) then + -- havent moved + print('no move') + else + if not lpt then + gpt.x = gpt.x - 2 + else + local dx = lpt.x - pt.x + local dz = lpt.z - pt.z + local angle = math.atan2(dx, dz) + gpt.x = pt.x + 2.5 * math.sin(angle) + gpt.z = pt.z + 2.5 * math.cos(angle) + end + lpt = pt + local s, m = ni.goTo(gpt.x, gpt.y + 1, gpt.z) + if not s then + print(m) + end + end + end + + os.sleep(.5) +end \ No newline at end of file diff --git a/neural/neuralFight.lua b/neural/neuralFight.lua new file mode 100644 index 0000000..0fb4d77 --- /dev/null +++ b/neural/neuralFight.lua @@ -0,0 +1,98 @@ +_G.requireInjector() + +local Angle = require('neural.angle') +local GPS = require('gps') +local Mobs = require('neural.mobs') +local ni = require('neural.interface') +local Point = require('point') +local Util = require('util') + +local os = _G.os + +local RADIUS = 13 +local ROTATION = math.pi / 16 + +local uid = ni.getID and ni.getID() or error('Introspection module is required') +local pos = { x = 0, y = 0, z = 0 } + +local function findTargets() + local l = ni.sense() + table.sort(l, function(e1, e2) + return Point.distance(e1, pos) < Point.distance(e2, pos) + end) + + local targets = { } + for _,v in ipairs(l) do + if v.id ~= uid and Mobs.getNames()[v.name] then + if math.abs(v.y) < 2 and Point.distance(v, pos) < 16 then -- pitch is broken + table.insert(targets, v) + end + end + end + return #targets > 0 and targets +end + +local function shootAt(targets) + for _,target in ipairs(targets) do + target = ni.getMetaByID(target.id) + if target and target.isAlive and Point.distance(target, pos) < 14 then + ni.shootAt(target) + end + end +end + +local potions = Util.filter( + ni.getEquipmentList(), + function(a) + return a.name == 'minecraft:splash_potion' + end) + +local function heal(target) + local hands = { 'main', 'off' } + + if #potions > 0 and ni.getMetaOwner().health < 10 then + local yaw, pitch = Angle.away(target.x - .5, 0, target.z - .5) + ni.look(yaw, pitch) + ni.use(.01, hands[potions[1].slot]) + ni.launch(yaw, pitch, 1) + table.remove(potions, 1) + end +end + +local pt = GPS.locate() + +while true do + local targets = findTargets() + if not targets then + local cpt = GPS.locate() + if Point.distance(pt, cpt) > 2 then + print('walking to starting point') + local s, m = ni.goTo(pt.x, pt.y, pt.z) + Util.print({ s, m }) + os.sleep(.05) + while ni.isWalking() do + os.sleep(0) + end + Util.print('done walking') + end + os.sleep(1) + else + local target = targets[1] + local angle = math.atan2(-target.x, -target.z) + ROTATION + + ni.launchTo({ + x = target.x + RADIUS * math.sin(angle), + y = 0, + z = target.z + RADIUS * math.cos(angle) + }, 1) + os.sleep(.2) + + shootAt(targets) + + heal(target) + + if math.random(1, 3) == 3 then + ROTATION = -ROTATION + end + end +end diff --git a/neural/neuralFly.lua b/neural/neuralFly.lua new file mode 100644 index 0000000..6978178 --- /dev/null +++ b/neural/neuralFly.lua @@ -0,0 +1,97 @@ +_G.requireInjector(_ENV) + +local Config = require('config') +local GPS = require('gps') +local ni = _G.device.neuralInterface + +local os = _G.os +local parallel = _G.parallel + +local id = ni.getID() +local config = Config.load('flight', { }) + +local args = { ... } +if args[1] == 'wp' then + local pt = GPS.locate() + config[args[2]] = pt + Config.update('flight', config) + return +end + +local wp = config[args[1]] +if not wp then + error('invalid wp') +end + +local pt = GPS.locate() + +local function descend() + print('descending to ' .. wp.y) + repeat + local meta = ni.getMetaByID(id) + if meta.motionY < 0 then + ni.launch(0, -90, math.min(4, meta.motionY / -0.5)) + end + print(math.abs(wp.y - pt.y)) + until math.abs(wp.y - pt.y) < 1 +end + +local function gps() + while true do + local lpt = GPS.locate() + if lpt then + pt = lpt + end + os.sleep(.1) + end +end + +local function yap(x, y, z) + local pitch = -math.atan2(y, math.sqrt(x * x + z * z)) + local yaw = math.atan2(-(x - .5), z - .5) + + return math.deg(yaw), math.deg(pitch) +end + +local function distance(a, b) + return math.sqrt( + math.pow(a.x - b.x, 2) + + math.pow(a.z - b.z, 2)) +end + +local function hover() + repeat + local meta = ni.getMetaByID(id) + local pitch = 295 + local yaw = yap(wp.x - pt.x, wp.y, wp.z - pt.z) + + if pt.y < wp.y + 16 and meta.motionY < 0 then + ni.launch(yaw, pitch, math.min(4, math.min(4, -meta.motionY * math.abs(pt.y - (wp.y + 16)) / 2))) + end + + until distance(wp, pt) < 2 +end + +local function launch() + ni.launch(0, 270, 3) + + repeat + local meta = ni.getMetaByID(id) + until meta.motionY < 0 + + hover() + + descend() +end + +local s, m = pcall(parallel.waitForAny, launch, gps) + +if not s then + _G.printError(m) +end + +--s, m = pcall(parallel.waitForAny, descend, gps) + +if not s then + error(m) +end diff --git a/neural/neuralRecorder.lua b/neural/neuralRecorder.lua new file mode 100644 index 0000000..eac4da5 --- /dev/null +++ b/neural/neuralRecorder.lua @@ -0,0 +1,88 @@ +_G.requireInjector(_ENV) + +local GPS = require('gps') +local Point = require('point') +local Util = require('util') + +local os = _G.os +local parallel = _G.parallel + +local t = { } +local ni = _G.device.neuralInterface or error('Neural Interface not found') + +if not ni.getID then + error('Missing Introspection Module') +end + +local uid = ni.getID() +local c = os.clock() + +local pt = GPS.locate(3) or error('GPS failed') +local lpt +local me = Util.find(ni.sense(), 'id', uid) + +local function gps() + while true do + me = Util.find(ni.sense(), 'id', uid) + pt = GPS.locate(3) or error('GPS failed') + os.sleep(.3) + print('got gps') + end +end + +local function record() + local timerId = os.startTimer(.1) + repeat + local event, ch = os.pullEvent() + local v + local delay = os.clock() - c + c = os.clock() + --print(event .. ' ' .. tostring(ch)) + if event == 'char' then + print('char ' .. ch) + if ch == ' ' then + v = { + action = 'walk', + x = pt.x, + y = pt.y, + z = pt.z, + pitch = me.pitch, + yaw = me.yaw, + delay = delay, + } + elseif ch == 'u' then + v = { + action = 'use', + x = pt.x, + y = pt.y, + z = pt.z, + pitch = me.pitch, + yaw = me.yaw, + delay = delay, + } + end + elseif event == 'timer' and ch == timerId then + if not lpt or not Point.same(pt, lpt) then + v = { + action = 'walk', + x = pt.x, + y = pt.y, + z = pt.z, + pitch = me.pitch, + yaw = me.yaw, + delay = delay, + } + lpt = pt + end + timerId = os.startTimer(.2) + end + + if v then + Util.print(v) + table.insert(t, v) + end + until event == 'char' and ch == 'q' +end + +parallel.waitForAny(gps, record) +Util.writeTable('neural.tbl', t) diff --git a/neural/neuralRemote.lua b/neural/neuralRemote.lua new file mode 100644 index 0000000..2833681 --- /dev/null +++ b/neural/neuralRemote.lua @@ -0,0 +1,56 @@ +rednet.open("right") +local sensor = peripheral.wrap("back") +local modules = peripheral.wrap("back") +local Ka = peripheral.find("neuralInterface") +local function fire(entity) + local x, y, z = entity.x, entity.y, entity.z + local pitch = -math.atan2(y, math.sqrt(x * x + z * z)) + local yaw = math.atan2(-x, z) + + Ka.look(math.deg(yaw), math.deg(pitch), 5) + Ka.shoot(1) + sleep(0.2) +end +local mobNames = {"Skeleton"} +local mobLookup = {} +for i = 1, #mobNames do + mobLookup[mobNames[i]] = true +end + +function SkeletonShoot() + local mobs = sensor.sense() + local candidates = {} + for i = 1, #mobs do + local mob = mobs[i] + if mobLookup[mob.name] then + candidates[#candidates + 1] = mob + end + end + if #candidates > 0 then + local mob = candidates[math.random(1, #candidates)] + fire(mob) + else + sleep(.1) + end +end + + while true do + local id,message = rednet.receive() + print(tostring(id)..message) + if id == 582 then + if message == "forward" then --W + Ka.walk(1,0,0) + elseif message == "back" then --S + Ka.walk(-1,0,0) + elseif message == "turnLeft" then--A + Ka.walk(0,0,-1) + elseif message == "turnRight" then--D + Ka.walk(0,0,1) + elseif message == "shoot" then--Starts fell program + SkeletonShoot() + end + else + write(" Denied!") + end + end + diff --git a/neural/neuralReplay.lua b/neural/neuralReplay.lua new file mode 100644 index 0000000..b2264ca --- /dev/null +++ b/neural/neuralReplay.lua @@ -0,0 +1,52 @@ +_G.requireInjector(_ENV) + +local GPS = require('gps') +local Util = require('util') + +local os = _G.os +local shell = _ENV.shell + +local args = { ... } +local fileName = args[1] or 'neural.tbl' + +local t = Util.readTable(shell.resolve(fileName)) or error('Unable to read ' .. fileName) +local ni = _G.device.neuralInterface + +local function walkTo(x, y, z) + local pt = GPS.locate(2) + if pt then + local s, m, m2 = pcall(function() + local gpt = { + x = x - pt.x, + y = math.floor(y) - math.floor(pt.y), + z = z - pt.z, + } + gpt.x = math.min(math.max(gpt.x, -30), 30) + gpt.z = math.min(math.max(gpt.z, -30), 30) + return ni.walk(gpt.x, gpt.y, gpt.z) + end) + if not s or not m then + _G.printError(m2 or m) + end + end + os.sleep(.5) + while ni.isWalking() do + os.sleep(0) + end +end + +for _,v in pairs(t) do + Util.print(v) + + --if v.action == 'walk' then + walkTo(v.x, v.y, v.z) + --end + ni.look(v.yaw, v.pitch) + if v.action == 'use' then + ni.use() + os.sleep(2) + end +-- os.sleep(v.delay) + -- os.sleep(2) + -- read() +end diff --git a/neural/neuralSword.lua b/neural/neuralSword.lua new file mode 100644 index 0000000..c269c62 --- /dev/null +++ b/neural/neuralSword.lua @@ -0,0 +1,28 @@ +_G.requireInjector(_ENV) + +local ni = require('neural.interface') +local Util = require('util') + +local os = _G.os + +while true do + local target = Util.find(ni.sense(), 'name', 'joebodo') + if target then + if math.abs(target.x) < 2 and + math.abs(target.z) < 2 then + ni.lookAt(target) + ni.swing() + os.sleep(.5) + else + local angle = math.atan2(-(target.x - .5), target.z - .5) + ni.walkTo({ + x = target.x + 1.5 * math.sin(angle), + y = 0, + z = target.z - 1.5 * math.cos(angle) + }, 1) + end + else + print('no target') + os.sleep(1) + end +end \ No newline at end of file diff --git a/neural/robotWars.lua b/neural/robotWars.lua new file mode 100644 index 0000000..4dc4604 --- /dev/null +++ b/neural/robotWars.lua @@ -0,0 +1,83 @@ +_G.requireInjector() + +local Angle = require('neural.angle') +local ni = require('neural.interface') +local Util = require('util') + +local os = _G.os + +local RADIUS = 13 +local ROTATION = math.pi / 16 + +local args = { ... } +local TARGET = args[1] or error('Syntax: robotWars ') +local uid = ni.getID and ni.getID() or error('Introspection module is required') + +local function findTarget(name) + for _, v in pairs(ni.sense()) do + if v.name == name and v.id ~= uid then + return v + end + end +end + +local function shootAt(entity) + local target = ni.getMetaByID(entity.id) + if target then + ni.shootAt(target) + end +end + +local enemy = findTarget(TARGET) +local potions = Util.filter( + ni.getEquipmentList(), + function(a) + return a.name == 'minecraft:splash_potion' + end) + +if not enemy then + print('Current enemies:') + for _,v in pairs(ni.getUniqueNames()) do + print(v) + end + print() + error('Invalid enemy') +end + +local function heal(target) + local hands = { 'main', 'off' } + + if #potions > 0 and ni.getMetaOwner().health < 10 then + local yaw, pitch = Angle.away({ x = target.x, y = 0, z = target.z }) + ni.look(yaw, pitch) + ni.use(.01, hands[potions[1].slot]) + ni.launch(yaw, pitch, 1) + table.remove(potions, 1) + end +end + +repeat + local target = ni.getMetaByID(enemy.id) + if not target then + print('lost target') + break + end + local angle = math.atan2(-target.x, -target.z) + ROTATION + + ni.launchTo({ + x = target.x + RADIUS * math.sin(angle), + y = 0, + z = target.z + RADIUS * math.cos(angle) + }, 1) + os.sleep(.2) + + shootAt(enemy) + + heal(enemy) + + if math.random(1, 3) == 3 then + ROTATION = -ROTATION + end +until not target.isAlive + +print('Won !') \ No newline at end of file diff --git a/neural/shootMob.lua b/neural/shootMob.lua new file mode 100644 index 0000000..02b4815 --- /dev/null +++ b/neural/shootMob.lua @@ -0,0 +1,31 @@ +_G.requireInjector(_ENV) + +local ni = require('neural.interface') +local uid = ni.getID and ni.getID() or error('Introspection module is required') + +local os = _G.os + +local args = { ... } + +local function findEntity(name) + for _,v in pairs(ni.sense()) do + if v.id ~= uid and v.name == name then + return v + end + end +end + +print('Targets:') +for _,v in pairs(ni.sense()) do + print(v.name) +end + +local target = args[1] or error('specify target name') + +repeat + local entity = findEntity(target) + if entity then + ni.shootAt(entity, 1) + end + os.sleep(.5) +until not entity diff --git a/neural/shootingGallery.lua b/neural/shootingGallery.lua new file mode 100644 index 0000000..6344508 --- /dev/null +++ b/neural/shootingGallery.lua @@ -0,0 +1,48 @@ +_G.requireInjector(_ENV) + +local Mobs = require('neural.mobs') +local ni = require('neural.interface') +local Point = require('point') +local Util = require('util') + +local os = _G.os + +if not ni.look then + error('neuralInterface required') +end + +local uid = ni.getID and ni.getID() or error('Introspection module is required') + +local function findTargets() + local pos = { x = 0, y = 0, z = 0 } + local l = ni.sense() + table.sort(l, function(e1, e2) + return Point.distance(e1, pos) < Point.distance(e2, pos) + end) + + local targets = { } + for _,v in ipairs(l) do + if v.id ~= uid and Mobs.getNames()[v.name] then + if math.abs(v.y) < 2 then -- pitch is broken + table.insert(targets, v) + end + end + end + return #targets > 0 and targets +end + +print('Targets:') +for _,v in pairs(ni.sense()) do + print(v.name) +end + +while true do + local targets = findTargets() + if targets then + for _, entity in ipairs(targets) do +Util.print(entity) + ni.shootAt(entity, 1) + end + end + os.sleep(.5) +end diff --git a/pickup/.package b/pickup/.package new file mode 100644 index 0000000..ea5dd64 --- /dev/null +++ b/pickup/.package @@ -0,0 +1,9 @@ +{ + required = { + 'core', + }, + title = 'Move resources around with turtles', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/pickup', + description = [[ WiP ]], + licence = 'MIT', +} diff --git a/apps/pickup.lua b/pickup/pickup.lua similarity index 95% rename from apps/pickup.lua rename to pickup/pickup.lua index f0aa9bb..140c326 100644 --- a/apps/pickup.lua +++ b/pickup/pickup.lua @@ -12,10 +12,6 @@ local peripheral = _G.peripheral local printError = _G.printError local turtle = _G.turtle -if not device.wireless_modem then - error('Modem is required') -end - if not turtle then error('Can only be run on a turtle') end @@ -236,16 +232,18 @@ local function pickupHost(socket) end end -Event.addRoutine(function() - while true do - print('waiting for connection on port 5222') - local socket = Socket.server(5222) +if device.wireless_modem then + Event.addRoutine(function() + while true do + print('waiting for connection on port 5222') + local socket = Socket.server(5222) - print('pickup: connection from ' .. socket.dhost) + print('pickup: connection from ' .. socket.dhost) - Event.addRoutine(function() pickupHost(socket) end) - end -end) + Event.addRoutine(function() pickupHost(socket) end) + end + end) +end local function eachEntry(t, fn) diff --git a/apps/pickupRemote.lua b/pickup/pickupRemote.lua similarity index 94% rename from apps/pickupRemote.lua rename to pickup/pickupRemote.lua index 3e99ec5..e6b3320 100644 --- a/apps/pickupRemote.lua +++ b/pickup/pickupRemote.lua @@ -1,9 +1,12 @@ +_G.requireInjector(_ENV) + +local device = _G.device +local multishell = _G.multishell + if not device.wireless_modem then error('Wireless modem is required') end -requireInjector(getfenv(1)) - local Event = require('event') local GPS = require('gps') local Socket = require('socket') @@ -20,11 +23,10 @@ local mainPage = UI.Page({ y = 2, height = 8, menuItems = { + { prompt = 'Add', event = 'add_location', help = 'Add a new location' }, { prompt = 'Pickup', event = 'pickup', help = 'Pickup items from this location' }, - { prompt = 'Charge cell', event = 'charge', help = 'Recharge this cell' }, { prompt = 'Refill', event = 'refill', help = 'Recharge this cell' }, { prompt = 'Set drop off location', event = 'setPickup', help = 'Recharge this cell' }, - { prompt = 'Set recharge location', event = 'setRecharge', help = 'Recharge this cell' }, { prompt = 'Clear', event = 'clear', help = 'Remove this location' }, }, }), @@ -94,6 +96,7 @@ local function getPoint() if not gpt then mainPage.statusBar:timedStatus('Unable to get location', 3) end + gpt.y = gpt.y - 1 return gpt end @@ -114,7 +117,7 @@ function refillPage:eventHandler(event) text = UI.Text({ x = 3, y = 3, value = 'Quantity' }), textEntry = UI.TextEntry({ x = 14, y = 3 }) }) - + dialog.eventHandler = function(self, event) if event.type == 'accept' then local l = tonumber(self.textEntry.value) @@ -128,10 +131,10 @@ function refillPage:eventHandler(event) end return true end - + return UI.Dialog.eventHandler(self, event) end - + dialog.titleBar.title = item.name dialog:setFocus(dialog.textEntry) UI:setPage(dialog) @@ -201,8 +204,7 @@ function mainPage:eventHandler(event) UI:setPage(refillPage) end - elseif event.type == 'pickup' or event.type == 'setPickup' or - event.type == 'setRecharge' or event.type == 'charge' or + elseif event.type == 'pickup' or event.type == 'setPickup' or event.type == 'clear' then local pt = getPoint() if pt then diff --git a/recipeBook/.package b/recipeBook/.package new file mode 100644 index 0000000..0662897 --- /dev/null +++ b/recipeBook/.package @@ -0,0 +1,6 @@ +{ + title = 'Recipe books for crafting programs', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/recipeBook', + description = [[ WIP ]], + licence = 'MIT', +} diff --git a/etc/names/appliedenergistics2.json b/recipeBook/etc/names/appliedenergistics2.json similarity index 100% rename from etc/names/appliedenergistics2.json rename to recipeBook/etc/names/appliedenergistics2.json diff --git a/etc/names/computercraft.json b/recipeBook/etc/names/computercraft.json similarity index 100% rename from etc/names/computercraft.json rename to recipeBook/etc/names/computercraft.json diff --git a/etc/names/enderio.json b/recipeBook/etc/names/enderio.json similarity index 100% rename from etc/names/enderio.json rename to recipeBook/etc/names/enderio.json diff --git a/etc/names/exnihiloadscensio.json b/recipeBook/etc/names/exnihiloadscensio.json similarity index 100% rename from etc/names/exnihiloadscensio.json rename to recipeBook/etc/names/exnihiloadscensio.json diff --git a/etc/names/extrautils2.json b/recipeBook/etc/names/extrautils2.json similarity index 100% rename from etc/names/extrautils2.json rename to recipeBook/etc/names/extrautils2.json diff --git a/etc/names/ironchest.json b/recipeBook/etc/names/ironchest.json similarity index 100% rename from etc/names/ironchest.json rename to recipeBook/etc/names/ironchest.json diff --git a/etc/names/rftools.json b/recipeBook/etc/names/rftools.json similarity index 100% rename from etc/names/rftools.json rename to recipeBook/etc/names/rftools.json diff --git a/etc/names/storagedrawers.json b/recipeBook/etc/names/storagedrawers.json similarity index 100% rename from etc/names/storagedrawers.json rename to recipeBook/etc/names/storagedrawers.json diff --git a/etc/names/tconstruct.json b/recipeBook/etc/names/tconstruct.json similarity index 100% rename from etc/names/tconstruct.json rename to recipeBook/etc/names/tconstruct.json diff --git a/etc/recipes/appliedenergistics2.db b/recipeBook/etc/recipes/appliedenergistics2.db similarity index 100% rename from etc/recipes/appliedenergistics2.db rename to recipeBook/etc/recipes/appliedenergistics2.db diff --git a/etc/recipes/botania.db b/recipeBook/etc/recipes/botania.db similarity index 100% rename from etc/recipes/botania.db rename to recipeBook/etc/recipes/botania.db diff --git a/etc/recipes/computercraft.db b/recipeBook/etc/recipes/computercraft.db similarity index 100% rename from etc/recipes/computercraft.db rename to recipeBook/etc/recipes/computercraft.db diff --git a/etc/recipes/enderio.db b/recipeBook/etc/recipes/enderio.db similarity index 100% rename from etc/recipes/enderio.db rename to recipeBook/etc/recipes/enderio.db diff --git a/etc/recipes/exnihilo.db b/recipeBook/etc/recipes/exnihilo.db similarity index 100% rename from etc/recipes/exnihilo.db rename to recipeBook/etc/recipes/exnihilo.db diff --git a/etc/recipes/extrautils2.db b/recipeBook/etc/recipes/extrautils2.db similarity index 100% rename from etc/recipes/extrautils2.db rename to recipeBook/etc/recipes/extrautils2.db diff --git a/etc/recipes/ironchest.db b/recipeBook/etc/recipes/ironchest.db similarity index 100% rename from etc/recipes/ironchest.db rename to recipeBook/etc/recipes/ironchest.db diff --git a/etc/recipes/mekanism.db b/recipeBook/etc/recipes/mekanism.db similarity index 100% rename from etc/recipes/mekanism.db rename to recipeBook/etc/recipes/mekanism.db diff --git a/etc/recipes/mysticalagriculture.db b/recipeBook/etc/recipes/mysticalagriculture.db similarity index 100% rename from etc/recipes/mysticalagriculture.db rename to recipeBook/etc/recipes/mysticalagriculture.db diff --git a/etc/recipes/rftools.db b/recipeBook/etc/recipes/rftools.db similarity index 100% rename from etc/recipes/rftools.db rename to recipeBook/etc/recipes/rftools.db diff --git a/etc/recipes/skyblockinfinity.db b/recipeBook/etc/recipes/skyblockinfinity.db similarity index 100% rename from etc/recipes/skyblockinfinity.db rename to recipeBook/etc/recipes/skyblockinfinity.db diff --git a/etc/recipes/storagedrawers.db b/recipeBook/etc/recipes/storagedrawers.db similarity index 100% rename from etc/recipes/storagedrawers.db rename to recipeBook/etc/recipes/storagedrawers.db diff --git a/etc/recipes/tconstruct.db b/recipeBook/etc/recipes/tconstruct.db similarity index 100% rename from etc/recipes/tconstruct.db rename to recipeBook/etc/recipes/tconstruct.db diff --git a/apps/recipeBook.lua b/recipeBook/recipeBook.lua similarity index 100% rename from apps/recipeBook.lua rename to recipeBook/recipeBook.lua diff --git a/storage/.package b/storage/.package new file mode 100644 index 0000000..1327c20 --- /dev/null +++ b/storage/.package @@ -0,0 +1,9 @@ +{ + required = { + 'core', + }, + title = 'Manage inventory', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/storage', + description = [[Storage manager for Minecraft 1.7 (Use Milo for new versions)]], + licence = 'MIT', +} diff --git a/apps/Crafter.lua b/storage/Crafter.lua similarity index 100% rename from apps/Crafter.lua rename to storage/Crafter.lua diff --git a/apps/chestManager.lua b/storage/chestManager.lua similarity index 99% rename from apps/chestManager.lua rename to storage/chestManager.lua index a220956..384a4e9 100644 --- a/apps/chestManager.lua +++ b/storage/chestManager.lua @@ -1210,8 +1210,8 @@ local function learnRecipe(page) end if not recipe then - debug(results) - debug(newRecipe) + _debug(results) + _debug(newRecipe) error('Failed - view system log') end diff --git a/storage/etc/apps/apps.db b/storage/etc/apps/apps.db new file mode 100644 index 0000000..603a26b --- /dev/null +++ b/storage/etc/apps/apps.db @@ -0,0 +1,19 @@ +{ + [ "81c0d915fa6d82fd30661c5e66e204cea52bb2b5" ] = { + title = "Activity", + category = "Apps", + icon = "\0318/\030f\031 \030 \0318\\\ +\030f \0308\0319o\030f\031 \ +\0318\\\030f\031 \030 \0318/", + run = "storageActivity.lua", + }, + [ "9e092dda4f0e27d0c7686ddd00272079e678b6e6" ] = { + title = "Storage", + category = "Apps", + icon = "\0307 \ +\0307 \0308\0311 \0305 \0308\031 \0307 \0308 \0301 \ +\0307 ", + run = "chestManager.lua", + requires = 'turtle', + }, +} diff --git a/apps/levelEmitter.lua b/storage/levelEmitter.lua similarity index 100% rename from apps/levelEmitter.lua rename to storage/levelEmitter.lua diff --git a/apps/storageActivity.lua b/storage/storageActivity.lua similarity index 100% rename from apps/storageActivity.lua rename to storage/storageActivity.lua