new release
This commit is contained in:
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
9
builder/.package
Normal file
9
builder/.package
Normal file
@@ -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',
|
||||
}
|
||||
@@ -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')
|
||||
@@ -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 = { '-', '/', '|', '\\' }
|
||||
6
core/.package
Normal file
6
core/.package
Normal file
@@ -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',
|
||||
}
|
||||
@@ -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', ...)
|
||||
|
||||
@@ -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
|
||||
@@ -110,7 +110,7 @@ function ChestAdapter:listItems(throttle)
|
||||
return items
|
||||
end
|
||||
else
|
||||
debug(m)
|
||||
_debug(m)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -70,7 +70,7 @@ function RefinedAdapter:listItems(throttle)
|
||||
end)
|
||||
|
||||
if not s and m then
|
||||
debug(m)
|
||||
_debug(m)
|
||||
end
|
||||
|
||||
itemDB:flush()
|
||||
@@ -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 = { }
|
||||
33
core/debugMonitor.lua
Normal file
33
core/debugMonitor.lua
Normal file
@@ -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 <monitor>')
|
||||
|
||||
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
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
@@ -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 = {
|
||||
58
core/lavaRefuel.lua
Normal file
58
core/lavaRefuel.lua
Normal file
@@ -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
|
||||
52
core/mirror.lua
Normal file
52
core/mirror.lua
Normal file
@@ -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
|
||||
@@ -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()
|
||||
@@ -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 = { }
|
||||
13
farms/.package
Normal file
13
farms/.package
Normal file
@@ -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',
|
||||
}
|
||||
175
farms/attack.lua
Normal file
175
farms/attack.lua
Normal file
@@ -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 <mob names>')
|
||||
|
||||
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
|
||||
23
farms/etc/apps/apps.db
Normal file
23
farms/etc/apps/apps.db
Normal file
@@ -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",
|
||||
},
|
||||
}
|
||||
226
farms/farmer.lua
Normal file
226
farms/farmer.lua
Normal file
@@ -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
|
||||
190
farms/rancher.lua
Normal file
190
farms/rancher.lua
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
6
forestry/.package
Normal file
6
forestry/.package
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
title = 'Forestry mod applications',
|
||||
repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/forestry',
|
||||
description = [[WIP]],
|
||||
licence = 'MIT',
|
||||
}
|
||||
106
forestry/alveary.lua
Normal file
106
forestry/alveary.lua
Normal file
@@ -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()
|
||||
|
||||
176
forestry/beeInfo.lua
Normal file
176
forestry/beeInfo.lua
Normal file
@@ -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()
|
||||
|
||||
34
forestry/filing.lua
Normal file
34
forestry/filing.lua
Normal file
@@ -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()
|
||||
|
||||
34
forestry/serums.lua
Normal file
34
forestry/serums.lua
Normal file
@@ -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()
|
||||
6
glasses/.package
Normal file
6
glasses/.package
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
title = 'Plethora overlay glasses support',
|
||||
repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/glasses',
|
||||
description = [[WIP]],
|
||||
licence = 'MIT',
|
||||
}
|
||||
481
glasses/apis/shatter.lua
Normal file
481
glasses/apis/shatter.lua
Normal file
@@ -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
|
||||
31
glasses/glassesDriver.lua
Normal file
31
glasses/glassesDriver.lua
Normal file
@@ -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)
|
||||
11
milo/.package
Normal file
11
milo/.package
Normal file
@@ -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',
|
||||
}
|
||||
229
milo/Milo.lua
Normal file
229
milo/Milo.lua
Normal file
@@ -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()
|
||||
550
milo/MiloRemote.lua
Normal file
550
milo/MiloRemote.lua
Normal file
@@ -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
|
||||
413
milo/apis/craft2.lua
Normal file
413
milo/apis/craft2.lua
Normal file
@@ -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
|
||||
312
milo/apis/milo.lua
Normal file
312
milo/apis/milo.lua
Normal file
@@ -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
|
||||
56
milo/apis/miniAdapter.lua
Normal file
56
milo/apis/miniAdapter.lua
Normal file
@@ -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
|
||||
505
milo/apis/storage.lua
Normal file
505
milo/apis/storage.lua
Normal file
@@ -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
|
||||
30
milo/apps/cobblegen.lua
Normal file
30
milo/apps/cobblegen.lua
Normal file
@@ -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
|
||||
166
milo/apps/furni.lua
Normal file
166
milo/apps/furni.lua
Normal file
@@ -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()
|
||||
29
milo/apps/water.lua
Normal file
29
milo/apps/water.lua
Normal file
@@ -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
|
||||
6
milo/autorun/milo.lua
Normal file
6
milo/autorun/milo.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
local device = _G.device
|
||||
local shell = _ENV.shell
|
||||
|
||||
if device.workbench then
|
||||
shell.openForegroundTab('Milo')
|
||||
end
|
||||
93
milo/core/learnWizard.lua
Normal file
93
milo/core/learnWizard.lua
Normal file
@@ -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)
|
||||
345
milo/core/listing.lua
Normal file
345
milo/core/listing.lua
Normal file
@@ -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)
|
||||
533
milo/core/machines.lua
Normal file
533
milo/core/machines.lua
Normal file
@@ -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)
|
||||
19
milo/etc/apps/apps.db
Normal file
19
milo/etc/apps/apps.db
Normal file
@@ -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",
|
||||
},
|
||||
}
|
||||
222
milo/plugins/activityView.lua
Normal file
222
milo/plugins/activityView.lua
Normal file
@@ -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)
|
||||
42
milo/plugins/brewingStandView.lua
Normal file
42
milo/plugins/brewingStandView.lua
Normal file
@@ -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 })
|
||||
77
milo/plugins/craftTask.lua
Normal file
77
milo/plugins/craftTask.lua
Normal file
@@ -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)
|
||||
132
milo/plugins/demandCraft.lua
Normal file
132
milo/plugins/demandCraft.lua
Normal file
@@ -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)
|
||||
89
milo/plugins/exportTask.lua
Normal file
89
milo/plugins/exportTask.lua
Normal file
@@ -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)
|
||||
119
milo/plugins/exportView.lua
Normal file
119
milo/plugins/exportView.lua
Normal file
@@ -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 })
|
||||
68
milo/plugins/importTask.lua
Normal file
68
milo/plugins/importTask.lua
Normal file
@@ -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)
|
||||
118
milo/plugins/importView.lua
Normal file
118
milo/plugins/importView.lua
Normal file
@@ -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 })
|
||||
16
milo/plugins/inputChestTask.lua
Normal file
16
milo/plugins/inputChestTask.lua
Normal file
@@ -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)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user