new release

This commit is contained in:
kepler155c@gmail.com
2018-12-08 13:29:26 -05:00
170 changed files with 9379 additions and 417 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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
View 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',
}

View File

@@ -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')

View File

@@ -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
View 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',
}

View File

@@ -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', ...)

View File

@@ -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

View File

@@ -110,7 +110,7 @@ function ChestAdapter:listItems(throttle)
return items
end
else
debug(m)
_debug(m)
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -70,7 +70,7 @@ function RefinedAdapter:listItems(throttle)
end)
if not s and m then
debug(m)
_debug(m)
end
itemDB:flush()

View File

@@ -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
View 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

View File

@@ -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",
},
}

View File

@@ -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
View 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
View 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

View File

@@ -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()

View File

@@ -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
View 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
View 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
View 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
View 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
View 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

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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",
},
}

View 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)

View 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 })

View 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)

View 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)

View 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
View 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 })

View 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
View 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 })

View 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