diff --git a/old_encryption/aead_chacha_poly.lua b/old_encryption/aead_chacha_poly.lua deleted file mode 100644 index 101d926..0000000 --- a/old_encryption/aead_chacha_poly.lua +++ /dev/null @@ -1,97 +0,0 @@ --- Copyright (c) 2015 Phil Leblanc -- see LICENSE file ------------------------------------------------------------- ---[[ - -aead_chacha_poly - -Authenticated Encryption with Associated Data (AEAD) [1], based -on Chacha20 stream encryption and Poly1305 MAC, as defined -in RFC 7539 [2]. - -[1] https://en.wikipedia.org/wiki/Authenticated_encryption -[2] http://www.rfc-editor.org/rfc/rfc7539.txt - -This file uses chacha20.lua and poly1305 for the encryption -and MAC primitives. - -]] - -local chacha20 = require "lib.plc.chacha20" -local poly1305 = require "lib.plc.poly1305" - ------------------------------------------------------------- --- poly1305 key generation - -local poly_keygen = function(key, nonce) - local counter = 0 - local m = string.rep('\0', 64) - local e = chacha20.encrypt(key, counter, nonce, m) - -- keep only first the 256 bits (32 bytes) - return e:sub(1, 32) -end - -local pad16 = function(s) - -- return null bytes to add to s so that #s is a multiple of 16 - return (#s % 16 == 0) and "" or ('\0'):rep(16 - (#s % 16)) -end - -local app = table.insert - -local encrypt = function(aad, key, iv, constant, plain) - -- aad: additional authenticated data - arbitrary length - -- key: 32-byte string - -- iv, constant: concatenated to form the nonce (12 bytes) - -- (why not one 12-byte param? --maybe because IPsec uses - -- an 8-byte nonce) - -- implementation: RFC 7539 sect 2.8.1 - -- (memory inefficient - encr text is copied in mac_data) - local mt = {} -- mac_data table - local nonce = constant .. iv - local otk = poly_keygen(key, nonce) - local encr = chacha20.encrypt(key, 1, nonce, plain) - app(mt, aad) - app(mt, pad16(aad)) - app(mt, encr) - app(mt, pad16(encr)) - -- aad and encrypted text length must be encoded as - -- little endian _u64_ (and not u32) -- see errata at - -- https://www.rfc-editor.org/errata_search.php?rfc=7539 - app(mt, string.pack(' what could be factored?) - local mt = {} -- mac_data table - local nonce = constant .. iv - local otk = poly_keygen(key, nonce) - app(mt, aad) - app(mt, pad16(aad)) - app(mt, encr) - app(mt, pad16(encr)) - app(mt, string.pack(' block then key = (sha256(key):gsub('..', function(h) return string.char(tonumber(h, 16)) end)) end - if #key < block then key = key .. string.rep("\0", block - #key) end - local o_key_pad = key:gsub('.', function(c) return string.char(bit32.bxor(string.byte(c), 0x5c)) end) - local i_key_pad = key:gsub('.', function(c) return string.char(bit32.bxor(string.byte(c), 0x36)) end) - local inner = sha256(i_key_pad .. msg) - inner = inner:gsub('..', function(h) return string.char(tonumber(h, 16)) end) - local mac = sha256(o_key_pad .. inner) - return mac:gsub('..', function(h) return string.char(tonumber(h, 16)) end) -end - -local function int_be(n) -- 32-bit BE - return string.char(bit32.band(bit32.rshift(n, 24), 255), bit32.band(bit32.rshift(n, 16), 255), - bit32.band(bit32.rshift(n, 8), 255), bit32.band(n, 255)) -end - -local function pbkdf2_sha256(password, salt, iterations, dkLen) - local hLen = 32 - local l = math.ceil(dkLen / hLen) - local r = dkLen - (l - 1) * hLen - local dk = {} - for i = 1, l do - local U = hmac_sha256(password, salt .. int_be(i)) - local T = { U:byte(1, hLen) } - for itr = 2, iterations do - U = hmac_sha256(password, U) - local Ub = { U:byte(1, hLen) } - for j = 1, hLen do T[j] = bit32.bxor(T[j], Ub[j]) end - if itr % 500 == 0 then sleep(0) end - end - if i < l then - dk[#dk + 1] = string.char(table.unpack(T)) - else - dk[#dk + 1] = string.char(table.unpack(T, 1, r)) - end - end - return table.concat(dk) -end - --- HMAC-DRBG -local SEED_PATH = "siss_drg_seed.bin" -local DRBG = { K = string.rep("\0", 32), V = string.rep("\1", 32), inited = false } - -local function drbg_update(provided_data) - DRBG.K = hmac_sha256(DRBG.K, DRBG.V .. "\0" .. (provided_data or "")) - DRBG.V = hmac_sha256(DRBG.K, DRBG.V) - if provided_data and #provided_data > 0 then - DRBG.K = hmac_sha256(DRBG.K, DRBG.V .. "\1" .. provided_data) - DRBG.V = hmac_sha256(DRBG.K, DRBG.V) - end -end - -local function drbg_init(seed) - DRBG.K = string.rep("\0", 32) - DRBG.V = string.rep("\1", 32) - drbg_update(seed) - DRBG.inited = true -end - -local function drbg_bytes(n) - if not DRBG.inited then error("DRBG not seeded. Call seed_entropy() once.") end - local out = {} - while #table.concat(out) < n do - DRBG.V = hmac_sha256(DRBG.K, DRBG.V) - out[#out + 1] = DRBG.V - end - drbg_update("") - local buf = table.concat(out) - return buf:sub(1, n) -end - -local function seed_entropy(extra_bytes) - local accum = {} - if fs.exists(SEED_PATH) then - local f = fs.open(SEED_PATH, "rb"); accum[#accum + 1] = f.readAll(); f.close() - local seed = sha256(table.concat(accum)):gsub('..', function(h) return string.char(tonumber(h, 16)) end) - drbg_init(seed) - else - term.write("Move around / mash keys then press Enter 4 times to seed…\n") - for i = 1, 4 do - local t1 = os.clock(); read(); local t2 = os.clock() - accum[#accum + 1] = int_be(math.floor((t2 - t1) * 1e9)) .. tostring({}):sub(8) - end - if extra_bytes and #extra_bytes > 0 then accum[#accum + 1] = extra_bytes end - local seed = sha256(table.concat(accum)):gsub('..', function(h) return string.char(tonumber(h, 16)) end) - drbg_init(seed) - local f = fs.open(SEED_PATH, "wb") - f.write(drbg_bytes(48)) - f.close() - end -end - -local VERSION = "\1" -local KDF_ID = "\1" -local AAD = "SiSS RA rev1|chacha20poly1305" - -local function u32be(n) - return string.char(bit32.band(bit32.rshift(n, 24), 255), bit32.band(bit32.rshift(n, 16), 255), - bit32.band(bit32.rshift(n, 8), 255), bit32.band(n, 255)) -end -local function u16be(n) return string.char(bit32.band(bit32.rshift(n, 8), 255), bit32.band(n, 255)) end -local function pack_params(iter) - return u32be(iter) .. u16be(32) -end -local function unpack_params(s) - local i1, i2, i3, i4, d1, d2 = s:byte(1, 6) - local iters = (bit32.bor(bit32.lshift(i1, 24), bit32.lshift(i2, 16), bit32.lshift(i3, 8), i4)) - local dklen = (bit32.bor(bit32.lshift(d1, 8), d2)) - return iters, dklen -end - -local function random_bytes(n) return drbg_bytes(n) end - -local function split_nonce12(nonce12) - assert(#nonce12 == 12, "nonce must be 12 bytes") - local iv = nonce12:sub(1, 6) - local constant = nonce12:sub(7, 12) - return iv, constant -end - -local function encrypt_with_password(password, plaintext) - local salt = random_bytes(16) - local iters = 1000 - local key = pbkdf2_sha256(password, salt, iters, 32) - - local nonce12 = random_bytes(12) - local iv, constant = split_nonce12(nonce12) - - local ciphertext, tag = aead.encrypt(AAD, key, iv, constant, plaintext) - - local params = pack_params(iters) - local blob = table.concat { VERSION, KDF_ID, params, salt, nonce12, ciphertext, tag } - return b64u_encode(blob) -end - -local function decrypt_with_password(password, blob) - local bin = b64u_decode(blob) - if #bin < 1 + 1 + 6 + 16 + 12 + 16 then return nil, "ciphertext too short" end - local pos = 1 - local ver = bin:sub(pos, pos); pos = pos + 1 - if ver ~= VERSION then return nil, "version mismatch" end - local kdfid = bin:sub(pos, pos); pos = pos + 1 - if kdfid ~= KDF_ID then return nil, "kdf mismatch" end - local params = bin:sub(pos, pos + 5); pos = pos + 6 - local iters, dklen = unpack_params(params); if dklen ~= 32 then return nil, "bad dkLen" end - local salt = bin:sub(pos, pos + 15); pos = pos + 16 - - -- read the same 12-byte nonce and split the same way - local nonce12 = bin:sub(pos, pos + 11); pos = pos + 12 - local iv, constant = split_nonce12(nonce12) - - local tag_len = 16 - local tag = bin:sub(#bin - tag_len + 1) - local ciphertext = bin:sub(pos, #bin - tag_len) - - local key = pbkdf2_sha256(password, salt, iters, 32) - - local pt, err = aead.decrypt(AAD, key, iv, constant, ciphertext, tag) - if not pt then return nil, err or "auth failed" end - return pt -end - -return { - decrypt_with_password = decrypt_with_password, - encrypt_with_password = encrypt_with_password, - seed_entropy = seed_entropy -} diff --git a/old_encryption/poly1305.lua b/old_encryption/poly1305.lua deleted file mode 100644 index db871f1..0000000 --- a/old_encryption/poly1305.lua +++ /dev/null @@ -1,204 +0,0 @@ --- Copyright (c) 2015 Phil Leblanc -- see LICENSE file ------------------------------------------------------------- ---[[ - -Poly1305 message authentication (MAC) created by Dan Bernstein -... -]] - ------------------------------------------------------------ --- poly1305 - -local sunp = string.unpack -local bit32 = bit32 -- alias - -local function poly_init(k) - -- k: 32-byte key as a string - -- initialize internal state - local st = { - r = { - bit32.band(sunp('= 16 do -- 16 = poly1305_block_size - -- h += m[i] (in rfc: a += n with 0x01 byte) - h0 = h0 + bit32.band(sunp('= 16 then - poly_blocks(st, m) - end - --handle remaining bytes - if st.bytes == 0 then -- no bytes left - -- nothing to do? no add 0x01? - apparently not. - else - local buffer = string.sub(m, st.midx) - .. '\x01' .. string.rep('\0', 16 - st.bytes - 1) - assert(#buffer == 16) - st.final = true -- this is the last block - --~ p16(buffer) - poly_blocks(st, buffer) - end - -- - return st -end --poly_update - -local function poly_finish(st) - -- - local c, mask --u32 - local f --u64 - -- fully carry h - local h0 = st.h[1] - local h1 = st.h[2] - local h2 = st.h[3] - local h3 = st.h[4] - local h4 = st.h[5] - -- - c = bit32.rshift(h1, 26); h1 = bit32.band(h1, 0x3ffffff) - h2 = h2 + c; c = bit32.rshift(h2, 26); h2 = bit32.band(h2, 0x3ffffff) - h3 = h3 + c; c = bit32.rshift(h3, 26); h3 = bit32.band(h3, 0x3ffffff) - h4 = h4 + c; c = bit32.rshift(h4, 26); h4 = bit32.band(h4, 0x3ffffff) - h0 = h0 + (c * 5); c = bit32.rshift(h0, 26); h0 = bit32.band(h0, 0x3ffffff) - h1 = h1 + c - -- - --compute h + -p - local g0 = (h0 + 5); c = bit32.rshift(g0, 26); g0 = bit32.band(g0, 0x3ffffff) - local g1 = (h1 + c); c = bit32.rshift(g1, 26); g1 = bit32.band(g1, 0x3ffffff) - local g2 = (h2 + c); c = bit32.rshift(g2, 26); g2 = bit32.band(g2, 0x3ffffff) - local g3 = (h3 + c); c = bit32.rshift(g3, 26); g3 = bit32.band(g3, 0x3ffffff) - local g4 = bit32.band(h4 + c - 0x4000000, 0xffffffff) -- (1 << 26) - -- - -- select h if h < p, or h + -p if h >= p - mask = bit32.band(bit32.rshift(g4, 31) - 1, 0xffffffff) - -- - g0 = bit32.band(g0, mask) - g1 = bit32.band(g1, mask) - g2 = bit32.band(g2, mask) - g3 = bit32.band(g3, mask) - g4 = bit32.band(g4, mask) - -- - mask = bit32.band(bit32.bnot(mask), 0xffffffff) - h0 = bit32.bor(bit32.band(h0, mask), g0) - h1 = bit32.bor(bit32.band(h1, mask), g1) - h2 = bit32.bor(bit32.band(h2, mask), g2) - h3 = bit32.bor(bit32.band(h3, mask), g3) - h4 = bit32.bor(bit32.band(h4, mask), g4) - -- - --h = h % (2^128) - h0 = bit32.band(bit32.bor((h0), bit32.lshift(h1, 26)), 0xffffffff) - h1 = bit32.band(bit32.bor(bit32.rshift(h1, 6), bit32.lshift(h2, 20)), 0xffffffff) - h2 = bit32.band(bit32.bor(bit32.rshift(h2, 12), bit32.lshift(h3, 14)), 0xffffffff) - h3 = bit32.band(bit32.bor(bit32.rshift(h3, 18), bit32.lshift(h4, 8)), 0xffffffff) - -- - -- mac = (h + pad) % (2^128) - f = h0 + st.pad[1]; h0 = bit32.band(f, 0xffffffff) - f = h1 + st.pad[2] + bit32.rshift(f, 32); h1 = bit32.band(f, 0xffffffff) - f = h2 + st.pad[3] + bit32.rshift(f, 32); h2 = bit32.band(f, 0xffffffff) - f = h3 + st.pad[4] + bit32.rshift(f, 32); h3 = bit32.band(f, 0xffffffff) - -- - local mac = string.pack('self transfer +function Myself:transfer(from, to, count) + if not turtle then + error("BUG DETECTED: attempted self->self transfer on a non-turtle") + end + if not self.in_transfer_session then + error("BUG DETECTED: attempted self->self transfer without beginning a transfer session") + end + return self:with_mutex(function() + self:select(from) + -- this doesn't return how many items were moved + turtle.transferTo(to, count) + -- so we'll just trust that the math we used to get `count` is correct + return count + end) +end + +function Myself:destructor() + self:end_transfer_session() +end + +return Myself +]==], 'Myself.lua') or Myself +TaskManager = using([==[-- simple task manager +-- a wrapper over parallel.waitForAll +-- that allows for rate limiting +-- and easy results collection +local TaskManager = {} +function TaskManager:new(max_active_threads) + local new_manager = { + max_active_threads = max_active_threads or 8, + active_threads = 1, + } + self.__index = self + setmetatable(new_manager, self) + return new_manager +end + +-- accepts a list of tasks to run (which can themselves spawn more tasks) +-- returns the result of each (in a list ordered the same way, packed) +function TaskManager:await(l) + local results = {} + local threads = {} + for i = 1,#l do + table.insert(threads, function() + while self.active_threads >= self.max_active_threads do coroutine.yield() end + self.active_threads = self.active_threads+1 + results[i] = l[i]() + self.active_threads = self.active_threads-1 + end) + end + self.active_threads = self.active_threads-1 + parallel.waitForAll(table.unpack(threads)) + self.active_threads = self.active_threads+1 + return results +end + +return TaskManager +]==], 'TaskManager.lua') or TaskManager +debugging = using([==[local pretty_print +function pprint(...) + pretty_print = pretty_print or require("cc.pretty").pretty_print + return pretty_print(...) +end +]==], 'debugging.lua') or debugging +display = using([==[local function halt() + while true do + os.pullEvent("free_lunch") + -- nom nom nom + end +end + +local cursor_x, cursor_y = 1, 1 +local function save_cursor() + cursor_x, cursor_y = term.getCursorPos() + local sizex, sizey = term.getSize() + local margin = 2 -- space to leave at the bottom of the screen + cursor_y = math.min(cursor_y, sizey-margin) +end +local function clear_below() + local _, y = term.getCursorPos() + local _, sizey = term.getSize() + while y < sizey do + y = y+1 + term.setCursorPos(1, y) + term.clearLine() + end +end +local function go_back() + term.setCursorPos(cursor_x, cursor_y) +end + +-- mbs messes with the term api +-- so for correct output we have to tell it when we start and stop messing with it +local term_current = term.current() +local function mbs_start() + (term_current.beginPrivateMode or function() end)() +end +local function mbs_end() + (term_current.endPrivateMode or function() end)() +end + +local function format_time(time) + if time < 60*60 then -- less than an hour => format as minutes and seconds + local seconds = math.floor(time) + local minutes = math.floor(seconds/60) + seconds = seconds-60*minutes + return minutes.."m "..seconds.."s" + else -- format as hours and minutes + local minutes = math.floor(time/60) + local hours = math.floor(minutes/60) + minutes = minutes-60*hours + return hours.."h "..minutes.."m" + end +end + + + +function display_exit(args_string) + local start_time = PROVISIONS.start_time + if PROVISIONS.global_options.quiet then + return + end + local total_transferred = PROVISIONS.report_transfer(0) + local elapsed_time = 0 + if start_time then + elapsed_time = os.clock()-start_time + end + local ips = (total_transferred/elapsed_time) + if ips ~= ips then + ips = 0 + end + go_back() + if PROVISIONS.global_options.debug then + print(" ") + end + mbs_end() + print("total uptime: "..format_time(elapsed_time)) + print("transferred total: "..format_number(total_transferred).." ("..format_number(ips, 2).." i/s) ") +end + +-- FIXME: MAKE BETTER ERROR PROPAGATION THAN SETTING A GLOBAL +-- local latest_error = nil + +function display_loop(args_string) + if PROVISIONS.global_options.quiet then + halt() + end + local start_time = PROVISIONS.start_time + mbs_start() + term.clear() + go_back() + print("hopper.lua "..version) + args_string = args_string:gsub("%s+/%s+", "\n/ ") + print("$ hopper "..args_string) + print("") + save_cursor() + + local time_to_wake = start_time + while true do + local total_transferred = PROVISIONS.report_transfer(0) + local elapsed_time = os.clock()-start_time + local ips = (total_transferred/elapsed_time) + if ips ~= ips then + ips = 0 + end + go_back() + if PROVISIONS.global_options.debug then + print((PROVISIONS.hoppering_stage or "nilstate").." ") + end + print("uptime: "..format_time(elapsed_time).." ") + if latest_error then + term.clearLine() + print("") + print(latest_error) + else + term.write("transferred so far: "..format_number(total_transferred).." ("..format_number(ips, 2).." i/s) ") + clear_below() + end + if PROVISIONS.global_options.debug then + sleep(0) + else + local current_time = os.clock() + time_to_wake = time_to_wake+1 + sleep(time_to_wake-current_time) + end + end +end +]==], 'display.lua') or display +glob = using([==[local aliases = {} +function register_alias(alias) + table.insert(aliases, alias) +end + +local function glob(ps, s) + if not ps then + error("glob: first arg is nil", 2) + end + if not s then + error("glob: second arg is nil", 2) + end + + -- special case for when you don't want a pattern to match anything + if ps == "" then return false end + + ps = "|"..ps.."|" + local i = #aliases + while i >= 1 do + ps = string.gsub(ps, "(|+)"..aliases[i].name.."(|+)", "%1"..aliases[i].pattern.."%2") + i = i-1 + end + + i = 0 + for p in string.gmatch(ps, "[^|]+") do + i = i+1 + p = string.gsub(p, "*", ".*") + p = string.gsub(p, "-", "%%-") + p = "^"..p.."$" + local res = string.find(s, p) + if res ~= nil then + return i + end + end + return false +end + +return glob +]==], 'glob.lua') or glob +main = using([==[sides = {"top", "front", "bottom", "back", "right", "left"} + +-- rarely used since it's slow on big objects +local function deepcopy(o) + if type(o) == "table" then + local n = {} + for k,v in pairs(o) do + n[k] = deepcopy(v) + end + return n + else + return o + end +end + +local function exitOnTerminate(f) + local status, err = pcall(f) + if status then + return + end + if err == "Terminated" then + return err + end + return error(err, 0) +end + +-- call f a few times until it returns non-nil +-- this is meant to be used with inventory operations +-- TODO: replace with a watchdog thread that monitors reconnect/disconnect events +local function stubbornly(f, ...) + for i = 1,5 do + local res = {f(...)} + if res[1] ~= nil then + return table.unpack(res) + end + end +end + +-- list of chests to not rescan +dont_rescan_patterns = {} + +local function should_rescan(chest) + for _,p in ipairs(dont_rescan_patterns) do + if glob(p, chest) then + return false + end + end + return true +end + +-- slot data structure: +-- chest_name: name of container holding that slot +-- chest_size: size of the container. might be nil, and might be less than the total number of slots +-- slot_number: the index of that slot in the chest. defaults to 0 +-- name: name of item held in slot, nil if empty +-- nbt: nbt hash of item, nil if none +-- count: how much is there of this item, 0 if none +-- type: whether it's an item or fluid. nil for item, "f" for fluid +-- limit: how many items the slot can store, if nil then 64 +-- limit_is_constant: if true then the slot can take the same amount of items regardless of that item type's stack size +-- duplicate: on an empty slot means to make a copy of it after it gets filled up (aka. it represents many empty slots) +-- is_source: whether this slot matches source slot critera +-- is_dest: whether this slot matches dest slot criteria +-- cannot_wrap: the chest this slot is in cannot be wrapped +-- must_wrap: the chest this slot is in must be wrapped +-- dest_after_action: a function to call after the slot receives items +-- - accepts dest slot, source slot, amount transferred +-- voided: how many of the items are physically there but are pretending to be missing +-- from_priority/to_priority: how early in the pattern match the chest appeared, lower number means higher priority + +local function hardcoded_limit_overrides(c) + local ok, types = pcall(function() return {peripheral.getType(c)} end) + if not ok then return nil end + for _,t in ipairs(types) do + if t == "spectrum:bottomless_bundle" then + return 1/0 + end + if t == "slate_works:storage_loci" then + return 1/0 + end + if t == "minecraft:chiseled_bookshelf" then + return 1, true + end + if t == "powah:energizing_orb" then + return 1, true + end + end + return nil +end + +local function isVanilla(c) + local ok, types = pcall(function() return {peripheral.getType(c)} end) + if not ok then return false end + for _,t in ipairs(types) do + if string.find(t, "minecraft:.*") then + return true + end + end + return false +end + +local function isStorageDrawer(c) + local ok, types = pcall(function() return {peripheral.getType(c)} end) + if not ok then return false end + for _,t in ipairs(types) do + if string.find(t, "storagedrawers:.*") then + return true + end + end + return false +end + +local function isCreateProcessor(c) + local ok, types = pcall(function() return {peripheral.getType(c)} end) + if not ok then return false end + for _,t in ipairs(types) do + if t == "create:crushing_wheel_controller" + or t == "create:depot" then + return 1 + end + if t == "basin" then + return 9 + end + end + return nil +end + +local function isPowahOrb(c) + local ok, types = pcall(function() return {peripheral.getType(c)} end) + if not ok then return false end + for _,t in ipairs(types) do + if t == "powah:energizing_orb" then + return true + end + end + return false +end + +local function isStorageController(c) + local ok, types = pcall(function() return {peripheral.getType(c)} end) + if not ok then return false end + for _,t in ipairs(types) do + if t == "functionalstorage:storage_controller" + or string.find(t, "toms_storage:ts%.inventory_connector.*") then + return true + end + end + return false +end + +local function isApotheosisLibrary(c) + local ok, types = pcall(function() return {peripheral.getType(c)} end) + if not ok then return false end + for _,t in ipairs(types) do + if t == "apotheosis:library" + or t == "apotheosis:ender_library" then + return true + end + end + return false +end + +local function isBonaFideFluidStorage(c) + local ok, types = pcall(function() return {peripheral.getType(c)} end) + if not ok then return false end + for _,t in ipairs(types) do + if t == "fluid_storage" then + return true + end + end + return false +end + +local upw_max_item_transfer = 128 -- default value, we dynamically discover the exact value later +local upw_max_fluid_transfer = 65500 -- defaults vary but 65500 seems to be the smallest +local upw_max_energy_transfer = 1 -- not even remotely true but the real limit varies per peripheral +local upw_item_storage_api_version = {1, 1} -- default value is 1.1, we dynamically discover it + +-- returns if container is an UnlimitedPeripheralWorks container +local function isUPW(c) + if type(c) == "string" then + c = peripheral.wrap(c) + end + if not c then + -- anything else not wrappable is also probably some exception + return false + end + if c.isUPW or c.items then + return true + else + return false + end +end + +local function isMEBridge(c) + if type(c) == "string" then + c = peripheral.wrap(c) + end + if not c then + -- anything else not wrappable is also probably some exception + return false + end + if c.isMEBridge or c.importFluidFromPeripheral then + return true + else + return false + end +end + +local function isAE2(c) + if type(c) == "string" then + c = peripheral.wrap(c) + end + if not c then + -- anything else not wrappable is also probably some exception + return false + end + if c.isAE2 or c.getCraftingCPUs then + return true + else + return false + end +end + +local function is_sided(chest) + for _,dir in pairs(sides) do + if chest == dir then + return true + end + end + return false +end + +local is_inventory_cache = {} +-- if this return false it's definitely not an inventory +-- if this returns true it *might* be an inventory +local function is_inventory(chest, recursed) + if not recursed then + if is_inventory_cache[chest] == nil then + is_inventory_cache[chest] = is_inventory(chest, true) + end + return is_inventory_cache[chest] + end + if is_sided(chest) then + return true -- it might change later so we just have to assume it's an inventory + end + if chest == "void" then + return true + end + if chest == "self" then + return true + end + local types = {peripheral.getType(chest)} + local is_turtle = false + + local known_types = { + inventory = true, + item_storage = true, + fluid_storage = true, + drive = true, + manipulator = true, + meBridge = true, + propulsion_thruster = true, + + modem = false, + peripheral_hub = false, + } + + for _,type in pairs(types) do + if type == "turtle" then + is_turtle = true + end + if PROVISIONS.options.energy then + if type == "energy_storage" then + return true + end + else + if known_types[type] ~= nil then + return known_types[type] + end + end + end + if is_turtle then + -- trying to wrap a "turtle" without "inventory" is a common mistake + -- hence the custom error message + error("Without the UnlimitedPeripheralWorks mod, turtles can only be transferred to/from using `self`") + end + + -- fail open to at least attempt to handle whatever else exists out there + return true +end + +-- item name -> maxCount +local stack_sizes_cache = {} +-- item name ; item nbt -> displayName +local display_name_cache = {} +-- item name -> tags +local tags_cache = {} +setmetatable(display_name_cache, { + __index = function(t, k) + if not PROVISIONS.logging.transferred then + -- the transferred hook is the only place where we use display names + -- so if it's not present just don't bother fetching them + return "PLACEHOLDER" + end + end, +}) + +local no_c = { + list = function() return {} end, +} + +-- chest_name -> {.size, .limits = {slot number -> limit}} +local slot_limits_cache = {} + +local function chest_wrap(chest, recursed) + -- for every possible chest must return an object with .list + -- as well as possibly custom transfer methods + local meta = { + cannot_wrap = false, + must_wrap = false, + chest_name = chest, + slot_number = 0, + transfer_strikes = 0, + } + meta.__index = meta + + if not is_inventory(chest) then + return no_c + end + + if not recursed then + local chest_wrap_cache = PROVISIONS.chest_wrap_cache + if not chest_wrap_cache[chest] then + chest_wrap_cache[chest] = {chest_wrap(chest, true)} + end + return table.unpack(chest_wrap_cache[chest]) + end + + local options = PROVISIONS.options + + if chest == "void" then + meta.dest_after_action = function(d, s, transferred) + s.count = s.count+transferred + s.voided = (s.voided or 0)+transferred + end + local c = { + list = function() + local l = { + {count = 0, limit = 1/0, duplicate = true}, + {count = 0, limit = 1/0, duplicate = true, type = "f"}, + {count = 0, limit = 1/0, duplicate = true, type = "e"}, + } + for _,s in ipairs(l) do + setmetatable(s, meta) + end + return l + end, + } + return c + end + if chest == "self" then + meta.cannot_wrap = true + local c = {} + if options.energy then + c.list = function() + local fuel_level = turtle.getFuelLevel() + local fuel_limit = turtle.getFuelLimit() + local s = {name = "turtleFuel", count = fuel_level, limit = fuel_limit, type = "e"} + setmetatable(s, meta) + return {s} + end + else + c.list = function() + local l = {} + for i = 1,16 do + l[i] = turtle.getItemDetail(i, false) + if l[i] then + if stack_sizes_cache[l[i].name] == nil + or display_name_cache[l[i].name..";"..(l[i].nbt or "")] == nil then + local details = turtle.getItemDetail(i, true) + l[i] = details + if details ~= nil then + stack_sizes_cache[details.name] = details.maxCount + display_name_cache[details.name..";"..(details.nbt or "")] = details.displayName + tags_cache[details.name] = details.tags + end + end + else + l[i] = {count = 0} -- empty slot + end + l[i].slot_number = i + setmetatable(l[i], meta) + end + return l + end + end + return c + end + local c = peripheral.wrap(chest) + if not c then + -- error("failed to wrap "..chest_name) + return no_c + end + if c.ejectDisk then + -- this a disk drive + if options.energy then return no_c end + c.ejectDisk() + meta.cannot_wrap = true + meta.dest_after_action = function(d, s, transferred) + c.ejectDisk() + d.count = 0 + d.name = nil + d.nbt = "" + end + c.list = function() + local slot = {count = 0, slot_number = 1} + setmetatable(slot, meta) + local l = {slot} + return l + end + return c + end + if c.getInventory and not c.list then + -- this is a bound introspection module + meta.must_wrap = true + if options.energy then return no_c end + local success + if options.ender then + success, c = pcall(c.getEnder) + else + success, c = pcall(c.getInventory) + end + if not success then + return no_c + end + end + if c.getPatternsFor and not c.items then + -- incorrectly wrapped AE2 system, UPW bug (computer needs to be placed last) + error("Cannot wrap AE2 system correctly! Break and place this computer and try again.") + end + if isMEBridge(c) then + -- ME bridge from Advanced Peripherals + c.isMEBridge = true + c.isAE2 = true + if options.denySlotless then + error("cannot use "..options.denySlotless.." when transferring to/from ME bridge") + end + + meta.must_wrap = true -- special methods must be used + c.list = function() + local res = {} + res = c.listItems() + for _,i in pairs(res) do + i.nbt = nil -- FIXME: figure out how to hash the nbt + i.count = i.count or i.amount + i.limit = 1/0 + end + table.insert(res, {count = 0, duplicate = true}) + table.insert(res, {type = "f", limit = 1/0, count = 0, duplicate = true}) + return res + end + c.tanks = function() + local res = {} + for _,tank in ipairs(c.listFluid()) do + table.insert(res, { + name = tank.name, + amount = tank.amount, + }) + end + return res + end + c.size = nil + c.pushItems = function(other_peripheral, from_slot_identifier, count, to_slot_number, additional_info) + local item_name = string.match(from_slot_identifier, "[^;]*") + return c.exportItemToPeripheral({name = item_name, count = count}, other_peripheral) + end + c.pullItems = function(other_peripheral, from_slot_number, count, to_slot_number, additional_info) + local item_name = nil + for _,s in pairs(additional_info) do + item_name = s.name + break + end + return c.importItemFromPeripheral({name = item_name, count = count}, other_peripheral) + end + c.pushFluid = function(to, limit, itemname) + return c.exportFluidToPeripheral({name = itemname, count = limit}, to) + end + c.pullFluid = function(from, limit, itemname) + return c.importFluidFromPeripheral({name = itemname, count = limit}, from) + end + end + if isUPW(c) then + -- this is an UnlimitedPeripheralWorks inventory + c.isUPW = true + if isAE2(c) then + c.isAE2 = true + end + if c.getConfiguration then + local upw_configuration = c.getConfiguration() + upw_item_storage_api_version = upw_configuration.itemStorageAPI or upw_item_storage_api_version + end + -- we use equal here because major change would truly break something + if options.denySlotless and upw_item_storage_api_version[1] == 1 and upw_item_storage_api_version[2] < 2 then + error("cannot use "..options.denySlotless.." when transferring to/from UPW peripheral") + end + + meta.must_wrap = true -- UPW forces us to use its own functions when interacting with a regular inventory + c.list = function() + local amounts = {} + for _,i in ipairs(c.items()) do + local id = i.name..";"..(i.nbt or "") + if not amounts[id] then + amounts[id] = {name = i.name, nbt = i.nbt, maxCount = i.maxCount, displayName = i.displayName, tags = i.tags, count = 0, limit = 1/0} + end + amounts[id].count = amounts[id].count+i.count + end + local res = {} + for _,a in pairs(amounts) do + local slot = a + table.insert(res, slot) + end + table.insert(res, {count = 0, limit = 1/0, duplicate = true}) + return res + end + c.size = nil + c.pushItemRaw = c.pushItem + c.pullItemRaw = c.pullItem + c.pushItem = function(to, query, limit, to_slot_number) + -- pushItem and pullItem are rate limited + -- so we have to keep calling it over and over + local total = 0 + while true do + local amount = c.pushItemRaw(to, query, limit-total, to_slot_number) + total = total+amount + if amount < upw_max_item_transfer or total == limit then + return total + end + end + end + c.pullItem = function(from, query, limit, from_slot_number) + -- pushItem and pullItem are rate limited + -- so we have to keep calling it over and over + local total = 0 + while true do + local amount = c.pullItemRaw(from, query, limit-total, from_slot_number) + total = total+amount + if amount < upw_max_item_transfer or total == limit then + return total + end + end + end + c.pushItems = function(other_peripheral, from_slot_identifier, count, to_slot_number, additional_info) + local item_name = string.match(from_slot_identifier, "[^;]*") + return c.pushItem(other_peripheral, item_name, count, to_slot_number) + end + c.pullItems = function(other_peripheral, from_slot_number, count, to_slot_number, additional_info) + local item_name = nil + for _,s in pairs(additional_info) do + item_name = s.name + break + end + return c.pullItem(other_peripheral, item_name, count, from_slot_number) + end + end + if not (c.list or c.tanks or c.pushEnergy) then + -- failed to wrap it for some reason + return no_c + end + + if c.tanks and not isBonaFideFluidStorage(c) then + -- thrusters from create: propulsion are an example + meta.must_wrap = true + end + if c.list and not (c.pushItems or c.pullItems) then + meta.cannot_wrap = true + end + + local cc = {} + cc.list = function() + local l = {} + local s + local tanks + local tank_capacities + local early_return + PROVISIONS.scan_task_manager:await({ + function() + if c.list then + l = stubbornly(c.list, true) + if not l then + early_return = true + end + end + end, + function() + if c.tanks then + tanks = stubbornly(c.tanks) + if not tanks then + early_return = true + end + end + end, + function() + if c.tanks and c.capacities then + tank_capacities = stubbornly(c.capacities) + if not tank_capacities then + early_return = true + end + end + end, + function() + if c.size then + s = stubbornly(c.size) + if not s then + early_return = true + end + end + end, + }) + if early_return then + return {} + end + + for i,item in pairs(l) do + if item.name then + if stack_sizes_cache[item.name] == nil + or display_name_cache[item.name..";"..(item.nbt or "")] == nil then + -- 1.12 cc + plethora calls getItemDetail "getItemMeta" + -- I am no longer sure where exactly getItemDetailForge is found but it doesn't hurt to check for it + c.getItemDetail = c.getItemDetail or c.getItemMeta or c.getItemDetailForge + + if not l[i].maxCount and c.getItemDetail then + local details = stubbornly(c.getItemDetail, i) + if not details then return {} end + l[i] = details + end + + if l[i].maxCount then + stack_sizes_cache[l[i].name] = l[i].maxCount + end + if l[i].displayName then + display_name_cache[l[i].name..";"..(l[i].nbt or "")] = l[i].displayName + end + if l[i].tags then + tags_cache[l[i].name] = l[i].tags + end + end + end + end + if s then + meta.chest_size = s + for i = 1,s do + if l[i] == nil then + l[i] = {count = 0} -- fill out empty slots + end + l[i].slot_number = i + end + end + + if s and s > 1 then + -- create processing blocks have multiple slots on forge but insertion is only possible on the first slot + local create_processor_slots = isCreateProcessor(c) + if create_processor_slots then + meta.never_dest = true + for i = 1,create_processor_slots do + l[i].never_dest = false + end + elseif isPowahOrb(c) then + l[1].never_dest = true + for i = 2,s do + l[i].never_source = true + end + end + end + + local upw_configuration = {} + if c.getConfiguration then + upw_configuration = c.getConfiguration() + upw_max_item_transfer = upw_configuration.itemStorageTransferLimit or upw_max_item_transfer + upw_max_item_transfer = upw_configuration.fluidStorageTransferLimit or upw_max_item_transfer + end + + local limit_override, limit_is_constant = hardcoded_limit_overrides(c) + if (not limit_override) and c.getItemLimit then + -- takes result of getItemLimit and the item name and returns adjusted limit + local function limit_calculation(lim, name) + if not name then return lim end + return lim*64/stack_sizes_cache[name] + end + + if (c.getConfiguration and not upw_configuration.implementationProvider) -- old UPW fucks up getItemLimit + or isVanilla(c) -- getItemLimit is broken for vanilla chests on forge. it works on fabric but there's no way to know if we're on forge so all vanilla limits are hardcoded instead + then + -- do nothing + elseif isStorageDrawer(c) then -- the drawers from the storage drawers mod have a very messed up api that needs a ton of special casing + for i,item in pairs(l) do + local lim = stubbornly(c.getItemLimit, i) + if not lim then return {} end + if i == 1 and lim == 2^31-1 then + -- weird first slot that we just ignore + l[1] = nil + else + limit_override = limit_calculation(lim, item.name) + if limit_override == 64 then limit_override = nil end + break + end + end + elseif isStorageController(c) then -- storage controllers have different limits for each slot so we need to set all of them individually + if not slot_limits_cache[chest] or slot_limits_cache[chest].size ~= s then + slot_limits_cache[chest] = {size = s, limits = {}} + local tasks = {} + local success = true + for i,item in pairs(l) do + table.insert(tasks, function() + local lim = stubbornly(c.getItemLimit, i) + if not lim then + success = false + return + end + -- I hate storage drawers so much + if lim == 2^31-1 then + limi = 0 + end + slot_limits_cache[chest].limits[i] = lim + end) + end + PROVISIONS.scan_task_manager:await(tasks) + if not success then + slot_limits_cache[chest] = nil + return {} + end + for i,item in pairs(l) do + local lim = slot_limits_cache[chest].limits[i] + local limit = limit_calculation(lim, item.name) + if limit == 64 then limit = nil end + l[i].limit = limit + end + end + else + for i,item in pairs(l) do + local lim = stubbornly(c.getItemLimit, i) + if not lim then return {} end + limit_override = limit_calculation(lim, item.name) + if limit_override == 64 then limit_override = nil end + break + end + end + end + if limit_override == 1 then + -- otherwise it makes no sense + limit_is_constant = true + + if isApotheosisLibrary(c) then + -- apotheosis library swallows books instantly + -- it has a slot limit of 1 so we only need to check here + meta.dest_after_action = function(d, s, transferred) + d.count = 0 + d.name = nil + d.nbt = "" + end + end + end + if limit_override then + for _,item in pairs(l) do + item.limit = limit_override + item.limit_is_constant = limit_is_constant + end + end + local fluid_start = 100000 -- TODO: change this to omega + if tanks then + -- FIXME: how do i fetch displayname of fluids???? + local tanks_count + local unknown_actual_tanks_count = false + if tank_capacities then + tanks_count = #tank_capacities + else + tanks_count = #tanks+1 + unknown_actual_tanks_count = true + end + for fi = 1,tanks_count do + local fluid = tanks[fi] + if fluid ~= nil then -- In some cases, like with AE2 from UPW capacities can be bigger then present fluids + local slot_limit = (tank_capacities and tank_capacities[fi]) or 1/0 + if fluid.name ~= "minecraft:empty" then + table.insert(l, fluid_start+fi, { + name = fluid.name, + count = math.max(fluid.amount, 1), -- api rounds all amounts down, so amounts <1mB appear as 0, yet take up space + limit = slot_limit, + limit_is_constant = true, + type = "f", + }) + else + table.insert(l, fluid_start+fi, {type = "f", limit = slot_limit, limit_is_constant = true, count = 0}) + end + else + if fi == tanks_count and unknown_actual_tanks_count then + table.insert(l, fluid_start, {type = "f", limit = 1/0, count = 0, duplicate = true}) + end + end + end + if c.isAE2 or c.getInfo then + table.insert(l, fluid_start, {type = "f", limit = 1/0, count = 0, duplicate = true}) + end + end + + for _,s in pairs(l) do + setmetatable(s, meta) + end + + return l + end + if options.energy then + cc.list = function() + if not c.pushEnergy then return {} end + local energy_amount + local energy_unit + local energy_limit + PROVISIONS.scan_task_manager:await({ + function() + energy_amount = stubbornly(c.getEnergy)%(1/0) + end, + function() + energy_unit = stubbornly(c.getEnergyUnit) + end, + function() + energy_limit = (stubbornly(c.getEnergyCapacity)-1)%(1/0)+1 + end, + }) + if not (energy_amount and energy_unit and energy_limit) then + return {} + end + local s = {name = energy_unit, count = energy_amount, limit = energy_limit, type = "e"} + setmetatable(s, meta) + return {s} + end + end + cc.pushEnergy = function(to, limit, query) + -- pushEnergy and pullEnergy are rate limited + -- so we have to keep calling it over and over + local total = 0 + while true do + local amount = c.pushEnergy(to, limit-total, query) + total = total+amount + if amount < upw_max_energy_transfer or total == limit then + return total + end + end + end + cc.pullEnergy = function(from, limit, query) + -- pushEnergy and pullEnergy are rate limited + -- so we have to keep calling it over and over + local total = 0 + while true do + local amount = c.pullEnergy(from, limit-total, query) + total = total+amount + if amount < upw_max_energy_transfer or total == limit then + return total + end + end + end + cc.pushFluid = function(to, limit, query) + -- pushFluid and pullFluid are rate limited + -- so we have to keep calling it over and over + local total = 0 + while true do + local amount = c.pushFluid(to, limit-total, query) + total = total+amount + if amount < upw_max_fluid_transfer or total == limit then + return total + end + end + end + cc.pullFluid = function(from, limit, query) + -- pushFluid and pullFluid are rate limited + -- so we have to keep calling it over and over + local total = 0 + while true do + local amount = c.pullFluid(from, limit-total, query) + total = total+amount + if amount < upw_max_fluid_transfer or total == limit then + return total + end + end + end + cc.pullItems = c.pullItems + cc.pushItems = c.pushItems + cc.isAE2 = c.isAE2 + cc.isMEBridge = c.isMEBridge + cc.isUPW = c.isUPW + cc.pullItem = c.pullItem + cc.pushItem = c.pushItem + return cc +end + +local function transfer(from_slot, to_slot, count) + local myself = PROVISIONS.myself + if count <= 0 then + return 0 + end + if from_slot.chest_name == nil then + error("BUG DETECTED: nil source chest?") + end + if to_slot.chest_name == nil then + error("BUG DETECTED: nil dest chest?") + end + if from_slot.type ~= to_slot.type then + error("item type mismatch: "..(from_slot.type or "nil").." -> "..(to_slot.type or "nil")) + end + if to_slot.chest_name == "void" then + -- the void consumes all that you give it + return count + end + if from_slot.type == "e" then + -- energy are to be dealt with here, separately. + if (not from_slot.cannot_wrap) and (not to_slot.must_wrap) then + local other_peripheral = to_slot.chest_name + if other_peripheral == "self" then other_peripheral = myself:local_name(from_slot.chest_name) end + return chest_wrap(from_slot.chest_name).pushEnergy(other_peripheral, count, from_slot.name) + end + if (not from_slot.must_wrap) and (not to_slot.cannot_wrap) then + local other_peripheral = from_slot.chest_name + if other_peripheral == "self" then other_peripheral = myself:local_name(to_slot.chest_name) end + return chest_wrap(to_slot.chest_name).pullEnergy(other_peripheral, count, from_slot.name) + end + error("cannot do energy transfer between "..from_slot.chest_name.." and "..to_slot.chest_name) + end + if from_slot.type == "f" then + -- fluids are to be dealt with here, separately. + if from_slot.count == count then + count = count+1 -- handle stray millibuckets that weren't shown + end + if (not from_slot.cannot_wrap) and (not to_slot.must_wrap) then + return chest_wrap(from_slot.chest_name).pushFluid(to_slot.chest_name, count, from_slot.name) + end + if (not from_slot.must_wrap) and (not to_slot.cannot_wrap) then + return chest_wrap(to_slot.chest_name).pullFluid(from_slot.chest_name, count, from_slot.name) + end + if isUPW(chest_wrap(from_slot.chest_name)) and isUPW(chest_wrap(to_slot.chest_name)) then + return chest_wrap(from_slot.chest_name).pushFluid(to_slot.chest_name, count, from_slot.name) + end + error("cannot do fluid transfer between "..from_slot.chest_name.." and "..to_slot.chest_name) + end + if (not from_slot.cannot_wrap) and (not to_slot.must_wrap) then + local other_peripheral = to_slot.chest_name + if other_peripheral == "self" then other_peripheral = myself:local_name(from_slot.chest_name) end + local c = chest_wrap(from_slot.chest_name) + if not c then + return 0 + end + local from_slot_number = from_slot.slot_number + local additional_info = nil + if isUPW(c) or isMEBridge(c) then + from_slot_number = from_slot.name..";"..(from_slot.nbt or "") + additional_info = {[to_slot.slot_number] = {name = to_slot.name, nbt = to_slot.nbt, count = to_slot.count}} + end + return c.pushItems(other_peripheral, from_slot_number, count, to_slot.slot_number, additional_info) + end + if (not to_slot.cannot_wrap) and (not from_slot.must_wrap) then + local other_peripheral = from_slot.chest_name + if other_peripheral == "self" then other_peripheral = myself:local_name(to_slot.chest_name) end + local c = chest_wrap(to_slot.chest_name) + if not c then + return 0 + end + local additional_info = nil + if isUPW(c) or isMEBridge(c) then + additional_info = {[from_slot.slot_number] = {name = from_slot.name, nbt = from_slot.nbt, count = from_slot.count}} + end + return c.pullItems(other_peripheral, from_slot.slot_number, count, to_slot.slot_number, additional_info) + end + if from_slot.chest_name == "self" and to_slot.chest_name == "self" then + return myself:transfer(from_slot.slot_number, to_slot.slot_number, count) + end + local cf = chest_wrap(from_slot.chest_name) + local ct = chest_wrap(to_slot.chest_name) + if isUPW(cf) and isUPW(ct) then + local c = cf + return c.pushItem(to_slot.chest_name, from_slot.name, count) + end + error("cannot do transfer between "..from_slot.chest_name.." and "..to_slot.chest_name) +end + +local function num_in_ranges(num, ranges, size) + size = size or 1/0 + for _,range in ipairs(ranges) do + if type(range) == "number" then + local target = range + if target < 0 then + target = size+1+target + end + if num == target then + return true + end + elseif type(range) == "table" then + local min = range[1] + local max = range[2] + if min < 0 then + min = size+1+min + end + if max < 0 then + max = size+1+max + end + if min <= num and num <= max then + return true + end + end + end + return false +end + +local function has_tag(tag, name) + return tags_cache[name][tag] +end + +-- check if slot matches a specific filter +local function filter_matches(slot, filter) + if type(filter) == "function" then + -- passable through the table api + return filter({ + chest_name = slot.chest_name, + chest_size = slot.chest_size, + slot_number = slot.slot_number, + name = slot.name, + nbt = slot.nbt, + count = slot.count-(slot.voided or 0), + type = slot.type or "i", + tags = deepcopy(tags_cache[slot.name]), + }) + else + local filter_is_empty = true + if filter.none then + filter_is_empty = false + local filter_list = filter.none + if filter_list[1] == nil then + filter_list = {filter_list} + end + for _,f in ipairs(filter_list) do + if filter_matches(slot, f) then + return false + end + end + end + if filter.all then + filter_is_empty = false + for _,f in ipairs(filter.all) do + if not filter_matches(slot, f) then + return false + end + end + end + if filter.any then + filter_is_empty = false + local matches_any = false + for _,f in ipairs(filter.any) do + if filter_matches(slot, f) then + matches_any = true + break + end + end + if not matches_any then + return false + end + end + if filter.name then + filter_is_empty = false + if not glob(filter.name, slot.name) then + return false + end + end + if filter.tag then + filter_is_empty = false + if not has_tag(filter.tag, slot.name) then + return false + end + end + -- TODO: add a way to specify matching only items without nbt data in string api + if filter.nbt then + filter_is_empty = false + if not (slot.nbt and glob(filter.nbt, slot.nbt)) then + return false + end + end + if filter_is_empty then + error("ERROR: Empty filter struct has been passed in!") + end + return true + end +end + +-- check if slot matches the current command's filters (respecting -negate) +local function matches_filters(slot) + local filters = PROVISIONS.filters + local options = PROVISIONS.options + if slot.name == nil then + error("SLOT NAME IS NIL") + end + + local res = nil + if #filters == 0 then + res = true + else + res = false + for _,filter in pairs(filters) do + if filter_matches(slot, filter) then + res = true + break + end + end + end + if options.negate then + return not res + else + return res + end +end + +local function mark_sources(slots, from) + local filters = PROVISIONS.filters + local options = PROVISIONS.options + for _,s in ipairs(slots) do + if s.from_priority then + s.is_source = true + if s.never_source then + s.is_source = false + end + if options.from_slot then + s.is_source = num_in_ranges(s.slot_number, options.from_slot, s.chest_size) + end + end + end +end + +local function mark_dests(slots, to) + local filters = PROVISIONS.filters + local options = PROVISIONS.options + for _,s in ipairs(slots) do + if s.to_priority then + s.is_dest = true + if s.never_dest then + s.is_dest = false + end + if s.is_dest and options.to_slot then + s.is_dest = num_in_ranges(s.slot_number, options.to_slot, s.chest_size) + end + end + end +end + +local function unmark_overlap_slots(slots) + local options = PROVISIONS.options + for _,s in ipairs(slots) do + if s.is_source and s.is_dest then + -- TODO: option to choose how this gets resolved + -- currently defaults to being dest + s.is_source = false + end + end +end + +local function limit_slot_identifier(limit, primary_slot, other_slot) + local options = PROVISIONS.options + local slot = {} + slot.chest_name = primary_slot.chest_name + slot.slot_number = primary_slot.slot_number + slot.name = primary_slot.name + slot.nbt = primary_slot.nbt + if other_slot == nil then other_slot = {} end + if slot.name == nil then + slot.name = other_slot.name + slot.nbt = other_slot.nbt + end + if slot.name == nil then + error("limit_slot_identifier was given two empty slots", 2) + end + local identifier = "" + if limit.per_chest then + identifier = identifier..slot.chest_name + end + identifier = identifier..";" + if limit.per_slot then + if slot.chest_name ~= "void" then + identifier = identifier..slot.slot_number + end + end + identifier = identifier..";" + if limit.per_name then + identifier = identifier..(slot.type or "") + end + identifier = identifier..";" + if limit.per_name then + identifier = identifier..slot.name + end + identifier = identifier..";" + if limit.per_nbt then + identifier = identifier..(slot.nbt or "") + end + identifier = identifier..";" + if not limit.count_all then + if not matches_filters(slot) then + identifier = identifier.."x" + end + end + + return identifier +end + +-- limit data structure +-- type: transfer/from/to +-- dir: direction; min/max for from/to +-- limit: the set amount that was specified to limit to +-- items: table{identifier -> count} cache of item counts, indexed with an identifier +-- slots : table{position_identifier -> bool}; exists only when the limit represents slot count instead of item count. is nil if false, is a table of the slots that are currently being counted +-- slot_count: table{identifier -> bool} the size of the above table (aka. the number of keys) + +local function inform_limit_of_slot(limit, slot) + local options = PROVISIONS.options + if slot.name == nil then return end + if limit.type == "transfer" then return end + if limit.type == "from" and (not slot.is_source) then return end + if limit.type == "to" and (not slot.is_dest) then return end + -- from and to limits follow + local identifier = limit_slot_identifier(limit, slot) + if limit.slots then + local slot_position_identifier = slot.chest_name..";"..slot.slot_number + limit.slots[slot_position_identifier] = true + limit.slot_count[identifier] = limit.slot_count[identifier]+1 + else + limit.items[identifier] = (limit.items[identifier] or 0)+slot.count + end +end + +local function inform_limit_of_transfer(limit, from, to, amount) + local options = PROVISIONS.options + local from_identifier = limit_slot_identifier(limit, from, to) + local to_identifier = limit_slot_identifier(limit, to, from) + if limit.items[from_identifier] == nil then + limit.items[from_identifier] = 0 + end + if limit.items[to_identifier] == nil then + limit.items[to_identifier] = 0 + end + if limit.slots then + local from_position_identifier = from.chest_name..";"..from.slot_number + local to_position_identifier = to.chest_name..";"..to.slot_number + if limit.type == "transfer" then + if not limit.slots[from_position_identifier] then + limit.slots[from_position_identifier] = true + limit.slot_count[from_identifier] = limit.slot_count[from_identifier]+1 + end + if not limit.slots[to_position_identifier] then + limit.slots[to_position_identifier] = true + limit.slot_count[to_identifier] = limit.slot_count[to_identifier]+1 + end + end + if limit.type == "from" then + if limit.slots[from_position_identifier] and from.count == 0 then + limit.slots[from_position_identifier] = nil + limit.slot_count[from_identifier] = limit.slot_count[from_identifier]-1 + end + end + if limit.type == "to" then + if not limit.slots[to_position_identifier] then + limit.slots[to_position_identifier] = true + limit.slot_count[to_identifier] = limit.slot_count[to_identifier]+1 + end + end + else + if limit.type == "transfer" then + limit.items[from_identifier] = limit.items[from_identifier]+amount + if from_identifier ~= to_identifier then + if to.chest_name ~= "void" then + limit.items[to_identifier] = limit.items[to_identifier]+amount + end + end + elseif limit.type == "from" then + limit.items[from_identifier] = limit.items[from_identifier]-amount + elseif limit.type == "to" then + limit.items[to_identifier] = limit.items[to_identifier]+amount + else + error("UNKNOWN LIMIT TYPE "..limit.type) + end + end +end + +local function willing_to_give(slot) + local options = PROVISIONS.options + if not slot.is_source then + return 0 + end + if slot.name == nil then + return 0 + end + local allowance = slot.count-(slot.voided or 0) + for _,limit in ipairs(options.limits) do + local identifier = limit_slot_identifier(limit, slot) + if limit.slots then + if limit.type == "transfer" then + local slot_position_identifier = slot.chest_name..";"..slot.slot_number + if limit.limit > limit.slot_count[identifier] or limit.slots[slot_position_identifier] then + -- full allowance + else + -- no allowance + return 0 + end + end + if limit.type == "from" then + if limit.dir == "max" then + error("ERROR: -from-limit-max -slots has not been implemented yet") + end + local slot_position_identifier = slot.chest_name..";"..slot.slot_number + if limit.limit < limit.slot_count[identifier] then + -- full allowance + else + -- no allowance + return 0 + end + end + else + if limit.type == "from" then + limit.items[identifier] = limit.items[identifier] or 0 + local amount_present = limit.items[identifier] + if limit.dir == "min" then + allowance = math.min(allowance, amount_present-limit.limit) + else + if amount_present > limit.limit then + allowance = 0 + end + end + elseif limit.type == "transfer" then + limit.items[identifier] = limit.items[identifier] or 0 + local amount_transferred = limit.items[identifier] + allowance = math.min(allowance, limit.limit-amount_transferred) + end + end + end + return math.max(allowance, 0) +end + +local function willing_to_take(slot, source_slot) + local options = PROVISIONS.options + if not slot.is_dest then + return 0 + end + local allowance + local max_capacity = 1/0 + if slot.limit_is_constant then + max_capacity = (slot.limit or 64) + elseif (slot.limit or 64) < 2^25 then -- FIXME: get rid of this magic constant + local stack_size = stack_sizes_cache[source_slot.name] + + if stack_size then + max_capacity = (slot.limit or 64)*stack_size/64 + end + end + allowance = max_capacity-slot.count + for _,limit in ipairs(options.limits) do + local identifier = limit_slot_identifier(limit, slot, source_slot) + if limit.slots then + if limit.type == "to" or limit.type == "transfer" then + if limit.dir == "min" then + error("ERROR: -to-limit-min -slots has not been implemented yet") + end + local slot_position_identifier = slot.chest_name..";"..slot.slot_number + if limit.slots[slot_position_identifier] then + -- full allowance + elseif limit.limit > limit.slot_count[identifier] then + if limit.type == "transfer" and limit.limit == 1+limit.slot_count[identifier] then + -- we need to consult with the source slot as well. + local source_slot_position_identifier = source_slot.chest_name..";"..source_slot.slot_number + if limit.slots[source_slot_position_identifier] then + -- full allowance + else + -- that particular pairing doesn't work. + return 0 + end + else + -- full allowance + end + else + -- no allowance + return 0 + end + end + else + if limit.type == "to" then + limit.items[identifier] = limit.items[identifier] or 0 + local amount_present = limit.items[identifier] + if limit.dir == "max" then + allowance = math.min(allowance, limit.limit-amount_present) + else + if amount_present < limit.limit then + allowance = 0 + end + end + elseif limit.type == "transfer" then + limit.items[identifier] = limit.items[identifier] or 0 + local amount_transferred = limit.items[identifier] + allowance = math.min(allowance, limit.limit-amount_transferred) + end + end + end + return math.max(allowance, 0) +end + +local function sort_sources(sources) + table.sort(sources, function(left, right) + if left.from_priority ~= right.from_priority then + return left.from_priority < right.from_priority + elseif left.count-(left.voided or 0) ~= right.count-(right.voided or 0) then + return left.count-(left.voided or 0) < right.count-(right.voided or 0) + elseif left.chest_name ~= right.chest_name then + return left.chest_name < right.chest_name + elseif left.slot_number ~= right.slot_number then + return left.slot_number > right.slot_number -- TODO: make this configurable + elseif left.name ~= right.name then + return left.name < right.name + elseif left.nbt ~= right.nbt then + return left.nbt < right.nbt + end + end) +end + +local function sort_dests(dests) + table.sort(dests, function(left, right) + local left_space = (left.limit or stack_sizes_cache[left.name] or 64)-left.count + local right_space = (right.limit or stack_sizes_cache[right.name] or 64)-right.count + if left.to_priority ~= right.to_priority then + return left.to_priority < right.to_priority + elseif left_space ~= right_space then + return left_space < right_space + elseif left.chest_name ~= right.chest_name then + return left.chest_name < right.chest_name + elseif left.slot_number ~= right.slot_number then + return left.slot_number < right.slot_number -- different here + elseif left.name ~= right.name then + if left.name == nil then + return false + end + if right.name == nil then + return true + end + return left.name < right.name + elseif left.nbt ~= right.nbt then + return left.nbt < right.nbt + end + end) +end + +local function slot_identifier(slot, include_slot_number) + local ident = (slot.type or "")..";"..(slot.name or "")..";"..(slot.nbt or "") + if include_slot_number then + ident = ident..";"..slot.slot_number + end + return ident +end + +local function empty_slot_identifier(slot, include_slot_number) + local ident = (slot.type or "")..";;" + if include_slot_number then + ident = ident..";"..slot.slot_number + end + return ident +end + +-- sorts destination slots into item types +-- returns a "name;nbt" -> [index] lookup table +-- which can be used to iterate through slots containing a particular item type +local function generate_dests_lookup(dests) + local options = PROVISIONS.options + local dests_lookup = {} + for i,d in ipairs(dests) do -- since we do this right after sorting the resulting lookup table will also be sorted + local ident = slot_identifier(d, options.preserve_slots) + if not dests_lookup[ident] then + dests_lookup[ident] = {slots = {}, s = 1, e = 0} -- s is first non-nil index, end is last non-nil index. if e list of slots +local scan_cache = {} + +local function get_chest_contents(peripherals, from, to) + local slots = {} + local job_queue = {} + + local from_priorities = compute_priorities(peripherals, from) + local to_priorities = compute_priorities(peripherals, to) + + for _,p in pairs(peripherals) do + table.insert(job_queue, function() + local from_priority = from_priorities[p] + local to_priority = to_priorities[p] + if not from_priority and not to_priority then + -- ignore non-matching inv + else + local l = scan_cache[p] + if l ~= nil then + -- TODO: make an option to disable this + for _,s in ipairs(l) do + s.voided = 0 + s.transfer_strikes = nil + end + else + l = chest_wrap(p).list() + if not should_rescan(p) then + scan_cache[p] = l + end + end + if l ~= nil then + for i,s in pairs(l) do + s.is_source = false + s.is_dest = false + s.from_priority = from_priority + s.to_priority = to_priority + if s.name == nil then + s.nbt = nil + s.count = 0 + end + table.insert(slots, s) + end + end + end + end) + end + PROVISIONS.scan_task_manager:await(job_queue) + + return slots +end + +-- returns a new empty slot based on s the passed-in slot +-- this function also updates the scan cache +local function duplicate_slot(d) + local newd = deepcopy(d) + setmetatable(newd, getmetatable(d)) + newd.name = nil + newd.nbt = nil + newd.count = 0 + newd.slot_number = nil + if scan_cache[newd.chest_name] then + table.insert(scan_cache[newd.chest_name], newd) + end + return newd +end + +local latest_warning = nil -- used to update latest_error if another error doesn't show up +-- TODO: get rid of warning and error globals!!!! + +-- how many transfer strikes until the slot is kicked out +local transfer_strike_out = 3 + +local function hopper_step(from, to) + latest_warning = nil + + PROVISIONS.hoppering_stage = "look" + local remote_names = get_names_remote() + local peripherals = get_all_peripheral_names(remote_names, from, to) + + PROVISIONS.hoppering_stage = "reset_limits" + reset_limits() + + PROVISIONS.hoppering_stage = "scan" + local slots = get_chest_contents(peripherals, from, to) + + PROVISIONS.hoppering_stage = "mark" + mark_sources(slots, from) + mark_dests(slots, to) + unmark_overlap_slots(slots) + for _,slot in ipairs(slots) do + for _,limit in ipairs(PROVISIONS.options.limits) do + inform_limit_of_slot(limit, slot) + end + end + + local sources = {} + local dests = {} + local found_dests = false + local found_sources = false + for _,s in pairs(slots) do + if s.is_source then + found_sources = true + if s.count > (s.voided or 0) then + table.insert(sources, s) + end + elseif s.is_dest then + found_dests = true + if (s.limit or stack_sizes_cache[s.name] or 64) > s.count then + table.insert(dests, s) + end + end + end + + if PROVISIONS.just_listing then + -- TODO: options on how to aggregate + local listing = {} + for _,slot in pairs(sources) do + listing[slot.name] = (listing[slot.name] or 0)+slot.count + end + PROVISIONS.output = listing + return + end + + if not found_dests or not found_sources then + if not found_sources then + if not found_dests then + latest_warning = "Warning: No sources nor destinations found." + else + latest_warning = "Warning: No sources found. " + end + else + latest_warning = "Warning: No destinations found. " + end + -- yield to prevent timing out from not doing anything + sleep(0) + return + end + + PROVISIONS.hoppering_stage = "sort" + sort_sources(sources) + sort_dests(dests) + local dests_lookup = generate_dests_lookup(dests) + + PROVISIONS.hoppering_stage = "transfer" + + -- begin a self->self transfer session (if the computer is a turtle) + -- hopper_loop has the job of ending it by calling :destructor() + PROVISIONS.myself:begin_transfer_session() + + for si,s in ipairs(sources) do + if s.name ~= nil and matches_filters(s) then + local sw = willing_to_give(s) + local ident = nil + local iteration_mode = "begin" -- "begin", "partial", "empty", or "done" + local dii = nil + while true do + if sw == 0 then break end + -- if there's too many inactive destination peripherals + -- they can cause the source to be incorrectly blamed + -- so for now we'll only strike destinations + -- if s.transfer_strikes >= transfer_strike_out then break end + if iteration_mode == "done" then break end + if not dii then + if iteration_mode == "begin" then + iteration_mode = "partial" + ident = slot_identifier(s, PROVISIONS.options.preserve_slots) + if dests_lookup[ident] then + dii = dests_lookup[ident].s + end + elseif iteration_mode == "partial" then + iteration_mode = "empty" + ident = empty_slot_identifier(s, PROVISIONS.options.preserve_slots) + if dests_lookup[ident] then + dii = dests_lookup[ident].s + end + else + iteration_mode = "done" + break + end + elseif dii > dests_lookup[ident].e then + dii = nil + else + local di = dests_lookup[ident].slots[dii] + local d = dests[di] + if (d.name ~= nil and d.name ~= s.name) or d.type ~= s.type then + error("BUG DETECTED! dests_lookup inconsistency: "..s.chest_name..":"..s.slot_number..":"..(s.type or "").." -> "..d.chest_name..":"..d.slot_number..":"..(d.type or "")) + end + local dw = willing_to_take(d, s) + if dw == 0 and d.name ~= nil then + -- remove d from list of destinations + if dii == dests_lookup[ident].s then + dests_lookup[ident].slots[dii] = nil + dests_lookup[ident].s = dests_lookup[ident].s+1 + else + table.remove(dests_lookup[ident].slots, dii) + dests_lookup[ident].e = dests_lookup[ident].e-1 + end + end + local to_transfer = math.min(sw, dw) + to_transfer = to_transfer-(to_transfer%(PROVISIONS.options.batch_multiple or 1)) + if to_transfer < (PROVISIONS.options.min_batch or 0) then + to_transfer = 0 + end + if to_transfer > 0 then + if remote_names[s.chest_name] and remote_names[d.chest_name] then + if remote_names[s.chest_name] ~= remote_names[d.chest_name] then + error("cannot transfer between "..s.chest_name.." and "..d.chest_name.." as they're on separate networks!") + end + end + + -- FIXME: propagate errors up correctly + + -- TODO: add a warning for when transfer returns nil + -- TODO: use stubbornly() in transfer() as well + local transferred = transfer(s, d, to_transfer) or 0 + + if transferred ~= to_transfer then + -- either the source or the dest are to blame for this + -- as we cannot know which just from a single transfer + -- we keep a score of how many times a slot has + -- participated in a failed transfer. + -- 3 strikes and it's out + if transferred == 0 then + s.transfer_strikes = s.transfer_strikes+1 + d.transfer_strikes = d.transfer_strikes+1 + end + + -- is the failure expected? (aka. should we raise a warning) + local failure_unexpected = true + if (d.type or "i") == "i" and isUPW(d.chest_name) then + -- the UPW api doesn't give us any indication of how many items an inventory can take + -- therefore the only way to transfer items is to just try and see if it succeeds + -- thus, failure is expected. + failure_unexpected = false + elseif (d.type or "i") == "i" and isMEBridge(s.chest_name) then + -- the AdvancedPeripherals api doesn't give us maxCount + -- so this error is part of normal operation + failure_unexpected = false + elseif s.type == "f" then + -- fluid api doesn't give us inventory size either. + failure_unexpected = false + end + if failure_unexpected then + -- latest_error = "transferred too little, retrying" + latest_warning = "WARNING: transferred less than expected: "..s.chest_name..":"..s.slot_number.." -> "..d.chest_name..":"..d.slot_number + end + end + + local transferred_hook_info = nil + if PROVISIONS.logging.transferred and (transferred > 0 or PROVISIONS.global_options.debug) then + -- we just prepare the info here (because it's easier) + -- the hook is instead called after we finish updating + -- the internal slot information + -- (in case the hook hangs or errors) + transferred_hook_info = { + transferred = transferred, + from = s.chest_name, + to = d.chest_name, + name = s.name, + displayName = display_name_cache[s.name..";"..(s.nbt or "")], + nbt = s.nbt or "", + type = s.type or "i", + } + end + + s.count = s.count-transferred + d.count = d.count+transferred + + for _,limit in ipairs(PROVISIONS.options.limits) do + inform_limit_of_transfer(limit, s, d, transferred) + end + + if transferred > 0 then + -- relevant if d was empty + d.name = s.name + d.nbt = s.nbt + + if d.dest_after_action then + d.dest_after_action(d, s, transferred) + end + end + -- relevant if s became empty + if s.count == 0 then + if s.type ~= "e" then + s.name = nil + s.nbt = nil + end + -- s.limit = 1/0 + end + + if d.count == transferred and transferred > 0 then + -- slot is no longer empty + -- we have to add it to the partial slots index (there might be more source slots of the same item type) + local d_ident = slot_identifier(d, PROVISIONS.options.preserve_slots) + if not dests_lookup[d_ident] then + dests_lookup[d_ident] = {slots = {}, s = 1, e = 0} + end + dests_lookup[d_ident].s = dests_lookup[d_ident].s-1 + dests_lookup[d_ident].slots[dests_lookup[d_ident].s] = di + + -- and we have to remove it from the empty slots index + if not d.duplicate then + if dii == dests_lookup[ident].s then + dests_lookup[ident].slots[dii] = nil + dests_lookup[ident].s = dests_lookup[ident].s+1 + else + table.remove(dests_lookup[ident].slots, dii) + dests_lookup[ident].e = dests_lookup[ident].e-1 + end + else + -- ...except we don't! + -- we instead need to replace it with a new empty slot of the same type + local newd = duplicate_slot(d) + d.duplicate = nil + table.insert(dests, newd) + dests_lookup[ident].slots[dii] = #dests + end + end + + if d.transfer_strikes >= transfer_strike_out then + -- slot is bad, remove it from the indexes completely + if dii == dests_lookup[ident].s then + dests_lookup[ident].slots[dii] = nil + dests_lookup[ident].s = dests_lookup[ident].s+1 + else + table.remove(dests_lookup[ident].slots, dii) + dests_lookup[ident].e = dests_lookup[ident].e-1 + end + end + + PROVISIONS.report_transfer(transferred) + + sw = willing_to_give(s) + + if transferred_hook_info then + PROVISIONS.logging.transferred(transferred_hook_info) + end + end + + dii = dii+1 + end + end + end + end +end + +local function hopper_loop(commands) + local time_to_wake = nil + while true do + for _,command in ipairs(commands) do + local from = command.from + local to = command.to + if not from then + error("no 'from' parameter supplied!") + end + if not to then + error("no 'to' parameter supplied! ('from' is "..from..")") + end + + + local provisions = { + options = command.options, + filters = command.filters, + chest_wrap_cache = {}, + scan_task_manager = TaskManager:new(PROVISIONS.global_options.scan_threads), + myself = Myself:new(), + } + local success, error_msg = provide(provisions, function() + return pcall(hopper_step, command.from, command.to) + end) + PROVISIONS.hoppering_stage = nil + provisions.myself:destructor() + + if not success then + latest_error = error_msg + if PROVISIONS.global_options.once then + error(error_msg, 0) + end + else + latest_error = latest_warning + end + end + + if PROVISIONS.global_options.once then + break + end + + local current_time = os.clock() + time_to_wake = (time_to_wake or current_time)+PROVISIONS.global_options.sleep + + sleep(time_to_wake-current_time) + end +end + + +local function hopper_main(args, is_lua, just_listing, logging) + local args_string = "{"..type(args).."}" + if type(args) == "string" then + args = args:gsub("\n$", "") + args_string = args + end + local commands, global_options = parser(args, is_lua) + local total_transferred = 0 + local provisions = { + global_options = global_options or {}, + is_lua = is_lua or false, + just_listing = just_listing or false, + hoppering_stage = undefined, + report_transfer = function(transferred) + total_transferred = total_transferred+transferred + return total_transferred + end, + output = undefined, + start_time = global_options.quiet or os.clock(), + logging = logging or {}, + } + local function displaying() + display_loop(args_string) + end + local function transferring() + hopper_loop(commands) + end + local terminated + provide(provisions, function() + terminated = exitOnTerminate(function() + parallel.waitForAny(transferring, displaying) + end) + display_exit(args_string) + end) + if just_listing then + return provisions.output + elseif terminated and is_lua then + error(terminated, 0) + else + return total_transferred + end +end + +local function hopper_list(chests) + return hopper_main(chests.." void", true, true, {}) +end + +local function hopper(args, logging) + return hopper_main(args, true, false, logging) +end + +local function isImported(args) + if #args == 2 and type(package.loaded[args[1]]) == "table" and not next(package.loaded[args[1]]) then + return true + else + return false + end +end + +local function main(args) + local is_imported = isImported(args) + + if is_imported then + local exports = { + hopper = hopper, + version = version, + list = hopper_list, + } + setmetatable(exports, { + __call = function(self, ...) return self.hopper(...) end, + debug = { + is_inventory = function(chest) return is_inventory(chest) end, + chest_list = function(chest, options) + return provide({ + chest_wrap_cache = {}, + options = options or {}, + scan_task_manager = TaskManager:new(8), + }, + function() + return chest_wrap(chest).list() + end + ) + end, + }, + }) + return exports + end + + if #args <= 0 then + print(help_message) + return + end + + local args_string = table.concat(args, " ") + hopper_main(args_string) +end + +return main +]==], 'main.lua') or main +numbers = using([==[-- format a number with commas every 3rd digit +function format_number(n, precision) + if precision then + n = string.format("%."..precision.."f", n) + else + n = tostring(n) + end + local k = 1 + while k > 0 do + n, k = n:gsub("^(-?%d+)(%d%d%d)", "%1,%2") + end + return n +end + +-- number parser that supports arithmetic +local lua_tonumber = tonumber +function tonumber(s) + local success, num = pcall(function() + -- check most common case first, faster than the general case + if string.find(s, "^%d+$") then + return lua_tonumber(s) + -- with just these characters you can't execute arbitrary code + elseif string.find(s, "^[%d%+%-%*/%(%)%.]+$") then + return load("return "..s)() + else + error("not a number") + end + end) + if not success or num == nil then + error("not a number: "..s) + end + return num +end +]==], 'numbers.lua') or numbers +parser = using([==[local function argcount(f) + local argcount = debug.getinfo(f, "u").nparams + if not argcount then + error("BUG DETECTED: argcount() returned nil") + end + return argcount +end + +local function is_valid_name(s) + return not string.find(s, "[^a-zA-Z_]") +end + +-- a lookup table of what to do for each flag +-- each entry contains a .call function and an .argcount number +-- if an entry instead contains a string it's an alias +local primary_flags = { + ["-once"] = function(...) + local arg = ({...})[1] + if type(arg) == "boolean" then + PROVISIONS.options.once = arg + else + PROVISIONS.options.once = true + end + end, + ["-forever"] = function(...) + local arg = ({...})[1] + if type(arg) == "boolean" then + PROVISIONS.options.once = not arg + else + PROVISIONS.options.once = false + end + end, + ["-quiet"] = function() PROVISIONS.options.quiet = true end, + ["-verbose"] = function() + if PROVISIONS.is_lua then + error("cannot use -verbose through the lua api") + end + PROVISIONS.options.quiet = false + end, + ["-debug"] = function() PROVISIONS.options.debug = true end, + ["-energy"] = function() PROVISIONS.options.energy = true end, + ["-not"] = "-negate", + ["-negated"] = "-negate", + ["-negate"] = function() PROVISIONS.options.negate = true end, + ["-nbt"] = function(nbt) + -- this should only deny UPW + -- FIXME: implement nbt hashes for ME bridge and then change this and other relevant flags + PROVISIONS.setDenySlotless() + PROVISIONS.positional() + PROVISIONS.filters[#PROVISIONS.filters].nbt = nbt + end, + ["-from-slot"] = function(slot) + PROVISIONS.setDenySlotless() + PROVISIONS.options.from_slot = PROVISIONS.options.from_slot or {} + if type(slot) == "table" then + for _,s in ipairs(slot) do + table.insert(PROVISIONS.options.from_slot, s) + end + else + table.insert(PROVISIONS.options.from_slot, tonumber(slot)) + end + end, + ["-from-slot-range"] = function(s, e) + PROVISIONS.setDenySlotless() + PROVISIONS.positional() + PROVISIONS.options.from_slot = PROVISIONS.options.from_slot or {} + table.insert(PROVISIONS.options.from_slot, {tonumber(s), tonumber(e)}) + end, + ["-to-slot"] = function(slot) + PROVISIONS.setDenySlotless() + PROVISIONS.options.to_slot = PROVISIONS.options.to_slot or {} + if type(slot) == "table" then + for _,s in ipairs(slot) do + table.insert(PROVISIONS.options.to_slot, s) + end + else + table.insert(PROVISIONS.options.to_slot, tonumber(slot)) + end + end, + ["-to-slot-range"] = function(s, e) + PROVISIONS.setDenySlotless() + PROVISIONS.positional() + PROVISIONS.options.to_slot = PROVISIONS.options.to_slot or {} + table.insert(PROVISIONS.options.to_slot, {tonumber(s), tonumber(e)}) + end, + ["-preserve-order"] = "-preserve-slots", + ["-preserve-slots"] = function() + PROVISIONS.setDenySlotless() + PROVISIONS.options.preserve_slots = true + end, + ["-batch-min"] = "-min-batch", + ["-min-batch"] = function(arg) + PROVISIONS.options.min_batch = tonumber(arg) + end, + ["-batch-max"] = "-min-batch", + ["-max-batch"] = function(arg) + table.insert(PROVISIONS.options.limits, { + type = "transfer", + limit = tonumber(arg), + per_slot = true, + per_chest = true, + }) + end, + ["-batch-multiple"] = function(arg) + PROVISIONS.options.batch_multiple = tonumber(arg) + end, + ["-from-limit"] = "-from-limit-min", + ["-from-limit-min"] = function(arg) + PROVISIONS.positional() + table.insert(PROVISIONS.options.limits, { + type = "from", + dir = "min", + limit = tonumber(arg), + }) + end, + ["-from-limit-max"] = function(arg) + PROVISIONS.positional() + table.insert(PROVISIONS.options.limits, { + type = "from", + dir = "max", + limit = tonumber(arg), + }) + end, + ["-to-limit-min"] = function(arg) + PROVISIONS.positional() + table.insert(PROVISIONS.options.limits, { + type = "to", + dir = "min", + limit = tonumber(arg), + }) + end, + ["-to-limit"] = "-to-limit-max", + ["-to-limit-max"] = function(arg) + PROVISIONS.positional() + table.insert(PROVISIONS.options.limits, { + type = "to", + dir = "max", + limit = tonumber(arg), + }) + end, + ["-refill"] = function() + -- -to-limit-min 1 -per-chest -per-item + PROVISIONS.positional() + table.insert(PROVISIONS.options.limits, { + type = "to", + dir = "min", + limit = 1, + per_name = true, + per_chest = true, + }) + end, + ["-transfer-limit"] = function(arg) + PROVISIONS.positional() + table.insert(PROVISIONS.options.limits, { + type = "transfer", + limit = tonumber(arg), + }) + end, + ["-per-slot"] = function() + PROVISIONS.setDenySlotless() + PROVISIONS.positional() + PROVISIONS.options.limits[#PROVISIONS.options.limits].per_slot = true + PROVISIONS.options.limits[#PROVISIONS.options.limits].per_chest = true + end, + ["-per-chest"] = function() + PROVISIONS.positional() + PROVISIONS.options.limits[#PROVISIONS.options.limits].per_chest = true + end, + ["-per-slot-number"] = function() + PROVISIONS.setDenySlotless() + PROVISIONS.positional() + PROVISIONS.options.limits[#PROVISIONS.options.limits].per_slot = true + end, + ["-per-item"] = function() + PROVISIONS.positional() + PROVISIONS.options.limits[#PROVISIONS.options.limits].per_name = true + end, + ["-per-nbt"] = function() + PROVISIONS.setDenySlotless() -- FIXME + PROVISIONS.positional() + PROVISIONS.options.limits[#PROVISIONS.options.limits].per_name = true + PROVISIONS.options.limits[#PROVISIONS.options.limits].per_nbt = true + end, + ["-stacks"] = "-slots", + ["-stack"] = "-slots", + ["-slot"] = "-slots", + ["-slots"] = function() + PROVISIONS.positional() + PROVISIONS.options.limits[#PROVISIONS.options.limits].slots = {} + end, + ["-count-all"] = function() + PROVISIONS.positional() + PROVISIONS.options.limits[#PROVISIONS.options.limits].count_all = true + end, + ["-alias"] = function(name, pattern) + if not is_valid_name(name) then + error("Invalid name for -alias: "..name) + end + register_alias({name = name, pattern = pattern}) + end, + ["-storage"] = function(name, pattern) + if not is_valid_name(name) then + error("Invalid name for -storage: "..name) + end + table.insert(dont_rescan_patterns, pattern) + register_alias({name = name, pattern = pattern}) + end, + ["-sleep"] = function(secs) + PROVISIONS.options.sleep = tonumber(secs) + end, + ["-scan-threads"] = function(secs) + PROVISIONS.options.scan_threads = tonumber(secs) + end, + ["-ender"] = function() + PROVISIONS.options.ender = true + end, + -- purely for the table api + -- (although they'll also be usable through the normal api) + ["-sources"] = "-from", + ["-from"] = function(s) + PROVISIONS.from = s + end, + ["-dests"] = "-to", + ["-destinations"] = "-to", + ["-to"] = function(s) + PROVISIONS.to = s + end, + ["-items"] = "-filters", + ["-filter"] = "-filters", + ["-filters"] = function(l) + if type(l) ~= "table" or l[1] == nil then + l = {l} + end + for _,f in ipairs(l) do + if type(f) == "table" then + table.insert(PROVISIONS.filters, f) + elseif type(f) == "function" then + -- function filter (infinite possibilities) + table.insert(PROVISIONS.filters, f) + else + if f:sub(1, 1) == "$" then + -- tag + table.insert(PROVISIONS.filters, {tag = f:sub(2)}) + else + -- item filter + table.insert(PROVISIONS.filters, {name = f}) + end + end + end + end, + ["-limits"] = function(l) + if l[1] == nil then + -- singular limit + l = {l} + end + for _,limit in ipairs(l) do + local default_dir + if limit.type == "from" then + default_dir = "min" + elseif limit.type == "to" then + default_dir = "max" + elseif limit.type == "transfer" then + -- no dir for it + else + error("unknown limit type: "..limit.type) + end + + table.insert(PROVISIONS.options.limits, { + type = limit.type, + dir = limit.dir or default_dir, + limit = limit.limit, + slots = (limit.slots or limit.stacks or nil) and {}, + per_slot = limit.per_slot_number or limit.per_slot, + per_chest = limit.per_chest or limit.per_slot, + per_name = limit.per_item or limit.per_nbt, + per_nbt = limit.per_nbt, + count_all = limit.count_all, + }) + end + end, +} + +-- the flags table that'll actually be used +-- when indexing aliases it instead returns the unaliased entry it's pointing to +local flags = {} +setmetatable(flags, { + __index = function(t, k) + local f = primary_flags[k] + if not f then return nil end + if type(f) == "string" then + return t[f] + else + return f + end + end, +}) + +local function hopper_parser_singular(args, is_lua) + return provide({ + from = undefined, + to = undefined, + is_lua = is_lua, + options = { + quiet = is_lua, + once = is_lua, + sleep = 1, + scan_threads = 8, + limits = {}, + storages = {}, + denySlotless = nil, -- UPW and MEBridge cannot work with some of the flags here + }, + filters = {}, + setDenySlotless = undefined, + positional = undefined, + }, function() + local i = 1 + local current_flag_name + PROVISIONS.setDenySlotless = function() + PROVISIONS.options.denySlotless = PROVISIONS.options.denySlotless or current_flag_name + end + PROVISIONS.positional = function() end + if type(args) == "table" then + -- table api + -- everything is treated as a flag + PROVISIONS.positional = function() + error("the flag '"..current_flag_name.."' cannot be used through the table API, use an alternative instead") + end + for flag_name,params in pairs(args) do + current_flag_name = flag_name + if type(params) ~= "table" or table[1] == nil then + params = {params} + end + local flag = flags["-"..(flag_name:gsub("_", "-"))] + if not flag then + error("UNKNOWN PARAMETER KEY: "..flag_name) + end + flag(table.unpack(params)) + end + else + -- string api + -- get rid of comments + local args_string = args:gsub("%-%-.-\n", "\n"):gsub("%-%-.-$", "") + -- tokenize + local args = {} + for arg in args_string:gmatch("%S+") do + table.insert(args, arg) + end + -- run through each token and parse + while i <= #args do + if glob("-*", args[i]) then + -- a flag + current_flag_name = args[i] + local flag = flags[args[i]:gsub("_", "-")] + if not flag then + error("UNKNOWN FLAG: "..args[i]) + end + local params = {} + argn = argcount(flag) + for j = 1,argn do + i = i+1 + table.insert(params, args[i]) + end + flag(table.unpack(params)) + else + -- positional argument + if not PROVISIONS.from then + PROVISIONS.from = args[i] + elseif not PROVISIONS.to then + PROVISIONS.to = args[i] + else + -- either an item filter or a tags filter + if args[i]:sub(1, 1) == "$" then + -- tag + table.insert(PROVISIONS.filters, {tag = args[i]:sub(2)}) + else + -- item filter + table.insert(PROVISIONS.filters, {name = args[i]}) + end + end + end + i = i+1 + end + end + return PROVISIONS.from, PROVISIONS.to, PROVISIONS.filters, PROVISIONS.options + end) +end + +-- returns: {from,to,filters,options}[], options +function parser(args, is_lua) + if type(args) == "table" then + -- table api! + if args[1] == nil then + -- singular command and not a list + -- turn it into a list of one command + args = {args} + end + elseif type(args) == "string" then + -- normal api + -- split on `/`s then pass it through hopper_parser_singular as if it's the table api + local args_string = args.." / " + args = {} + for s in args_string:gmatch("(.-)%s/%s") do + table.insert(args, s) + end + end + + local global_options + local commands = {} + for _,arg in ipairs(args) do + local from, to, filters, options = hopper_parser_singular(arg, is_lua) + if from then + table.insert(commands, {from = from, to = to, filters = filters, options = options}) + end + if not global_options then + global_options = options + end + end + return commands, global_options +end +]==], 'parser.lua') or parser +provide = using([==[-- used as a placeholder for a value +-- tables are only equal to themselves so this essentially acts like a unique symbol +-- this is used in the provisions metatable +undefined = {} + +-- provisions: a form of dependency injection inspired by algebraic effects +-- in essense `provide` creates globals that aren't actually global ("local globals") +-- and are instead scoped inside the specific function call +-- (as well as all threads summoned by said function call) +PROVISIONS = {} +setmetatable(PROVISIONS, { + __index = function(t, key) + for i = #t,1,-1 do + if t[i][key] ~= nil then + local v = t[i][key] + if v == undefined then + return nil + else + return v + end + end + end + error("BUG DETECTED: attempted to read unassigned provision key: "..key, 2) + end, + __newindex = function(t, key, val) + for i = #t,1,-1 do + if t[i][key] then + if val == nil then + t[i][key] = undefined + else + t[i][key] = val + end + return + end + end + error("BUG DETECTED: attempted to set unassigned provision key: "..key, 2) + end, +}) + +local function provide(values, f) + local meta = getmetatable(PROVISIONS) + setmetatable(PROVISIONS, {}) + local my_provisions = {} + for i,v in ipairs(PROVISIONS) do + my_provisions[i] = v + end + table.insert(my_provisions, values) + setmetatable(PROVISIONS, meta) + setmetatable(my_provisions, meta) + + local inner_provisions = my_provisions + local outer_provisions = PROVISIONS + + local co = coroutine.create(f) + local next_values = {} + while true do + outer_provisions = PROVISIONS + PROVISIONS = inner_provisions + local msg = {coroutine.resume(co, table.unpack(next_values))} + inner_provisions = PROVISIONS + PROVISIONS = outer_provisions + + local ok = msg[1] + + if ok then + if coroutine.status(co) == "dead" then + -- function has returned, pass the value up + return table.unpack(msg, 2) + else + -- just a yield, pass values up + next_values = {coroutine.yield(table.unpack(msg, 2))} + end + else + error(msg[2], 0) + end + end +end + +return provide +]==], 'provide.lua') or provide +return main({ ... }) diff --git a/src/lib/primeui.lua b/src/lib/primeui.lua index b78570a..83a3b73 100644 --- a/src/lib/primeui.lua +++ b/src/lib/primeui.lua @@ -60,7 +60,7 @@ do if not win.getPosition then return x, y end local wx, wy = win.getPosition() x, y = x + wx - 1, y + wy - 1 - _, win = debug.getupvalue(select(2, debug.getupvalue(win.isColor, 1)), 1) -- gets the parent window through an upvalue + _, win = debug.getupvalue(select(2, debug.getupvalue(win.isColor, 1)), 1) -- gets the parent window through an upvalue end return x, y end @@ -409,12 +409,12 @@ function PrimeUI.selectionBox(win, x, y, width, height, entries, action, selectC end -- Redraw screen. drawEntries() - elseif key == keys.enter then + elseif key ~= keys.up and key ~= keys.down then -- Select the entry: send the action. if type(action) == "string" then - PrimeUI.resolve("selectionBox", action, entries()[selection]) + PrimeUI.resolve("selectionBox", action, entries()[selection], key) else - action(entries()[selection]) + action(entries()[selection], key) end end elseif event == "mouse_click" and key == 1 then @@ -503,6 +503,48 @@ function PrimeUI.selectionBox(win, x, y, width, height, entries, action, selectC return drawEntries end +--- Draws a thin border around a screen region. +---@param win window The window to draw on +---@param x number The X coordinate of the inside of the box +---@param y number The Y coordinate of the inside of the box +---@param width number The width of the inner box +---@param height number The height of the inner box +---@param fgColor color|nil The color of the border (defaults to white) +---@param bgColor color|nil The color of the background (defaults to black) +function PrimeUI.borderBox(win, x, y, width, height, fgColor, bgColor) + expect(1, win, "table") + expect(2, x, "number") + expect(3, y, "number") + expect(4, width, "number") + expect(5, height, "number") + fgColor = expect(6, fgColor, "number", "nil") or colors.white + bgColor = expect(7, bgColor, "number", "nil") or colors.black + -- Draw the top-left corner & top border. + win.setBackgroundColor(bgColor) + win.setTextColor(fgColor) + win.setCursorPos(x - 1, y - 1) + win.write("\x9C" .. ("\x8C"):rep(width)) + -- Draw the top-right corner. + win.setBackgroundColor(fgColor) + win.setTextColor(bgColor) + win.write("\x93") + -- Draw the right border. + for i = 1, height do + win.setCursorPos(win.getCursorPos() - 1, y + i - 1) + win.write("\x95") + end + -- Draw the left border. + win.setBackgroundColor(bgColor) + win.setTextColor(fgColor) + for i = 1, height do + win.setCursorPos(x - 1, y + i - 1) + win.write("\x95") + end + -- Draw the bottom border and corners. + win.setCursorPos(x - 1, y + height) + win.write("\x8D" .. ("\x8C"):rep(width) .. "\x8E") +end + return { PrimeUI = PrimeUI } diff --git a/src/modules/chatbox.lua b/src/modules/chatbox.lua index 4f2d21b..67f9465 100644 --- a/src/modules/chatbox.lua +++ b/src/modules/chatbox.lua @@ -160,7 +160,7 @@ function run() amount = tonumber(args[3], 10) end - local moved = inv.sendItemAwayMultiple(slots, peripInventory, peripInventory, amount) + local moved = inv.sendItemAwayMultiple(slots, peripInventory, config.chatbox.players[user], amount) chatbox.tell(user, "Moved `" .. tostring(moved) .. "` items.", BOT_NAME) elseif args[1] == "withdraw" then @@ -196,7 +196,7 @@ function run() amount = tonumber(args[3], 10) end - local moved = inv.sendItemToSelf(itemName, peripInventory, amount) + local moved = inv.sendItemToSelf(itemName, peripInventory, amount, config.chatbox.players[user]) chatbox.tell(user, "Moved `" .. tostring(moved) .. "` items.", BOT_NAME) end diff --git a/src/modules/inv.lua b/src/modules/inv.lua index 0048938..08e16fd 100644 --- a/src/modules/inv.lua +++ b/src/modules/inv.lua @@ -1,6 +1,10 @@ local config = require("../../config") ---@type Config + if config.inventories == nil and config.remote.connection then return require("modules.inventory_layers.inv_ra") else + if config.beta and config.beta.hopper ~= nil then + return require("modules.inventory_layers.inv_hopper") + end return require("modules.inventory_layers.inv_ail") end diff --git a/src/modules/inventory_layers/inv_ail.lua b/src/modules/inventory_layers/inv_ail.lua index 99ffe41..3fce80c 100644 --- a/src/modules/inventory_layers/inv_ail.lua +++ b/src/modules/inventory_layers/inv_ail.lua @@ -32,7 +32,8 @@ end ---@param itemName string ---@param perip ccTweaked.peripheral.Inventory|string|nil ---@param maxAmount number|nil -local function sendItemToSelf(itemName, perip, maxAmount) +---@param id string|nil +local function sendItemToSelf(itemName, perip, maxAmount, id) if perip == nil then perip = turtleId end diff --git a/src/modules/inventory_layers/inv_hopper.lua b/src/modules/inventory_layers/inv_hopper.lua new file mode 100644 index 0000000..9435e2a --- /dev/null +++ b/src/modules/inventory_layers/inv_hopper.lua @@ -0,0 +1,142 @@ +local previousInventory = {} +local turtleMoveAllowed = true +local config = require("../../config") ---@type Config + +local hopper = require("lib.hopper") + +local hopperInventories = table.concat(config.inventories, "|"); + +local function sync() + hopper("-storage store " .. hopperInventories) + print("Synced."); +end + +---@param itemName string +---@param perip ccTweaked.peripheral.Inventory|string|nil +---@param maxAmount number|nil +---@param id string|nil +local function sendItemToSelf(itemName, perip, maxAmount, id) + if perip == nil then + perip = "self" + end + local rid = "" + if id == nil then + if type(perip) ~= "string" then + return + else + rid = perip + end + else + rid = id; + end + turtleMoveAllowed = false + + local moved = hopper("store " .. rid .. " " .. itemName .. " -transfer-limit " .. tostring(maxAmount or 64)) + turtleMoveAllowed = true + + return moved +end + +---@param slots ccTweaked.turtle.turtleSlot[] +---@param perip ccTweaked.peripheral.Inventory|nil +---@param id string|nil +---@param maxAmount number|nil +local function sendItemAwayMultiple(slots, perip, id, maxAmount) + if perip == nil then + perip = turtle + end + local srcId = id or "self" + + if srcId == nil then return end + + local hopslots = "" + + for _, slot in pairs(slots) do + hopslots = hopslots .. " -from-slot " .. tostring(slot) + end + + return hopper(srcId .. " store -from-limit-max " .. (maxAmount or 64) .. hopslots .. " -per-slot") +end + +local function getTurtleInventory() + local inventory = {} + for slot = 1, 16 do + local item = turtle.getItemDetail(slot, false) + inventory[slot] = item + end + return inventory +end + +local function detectPlayerInsert() + if turtle then + previousInventory = getTurtleInventory() + + while true do + os.pullEvent("turtle_inventory") + + local currentInventory = getTurtleInventory() + + if turtleMoveAllowed then + local newlyAddedSlots = {} + + for slot = 1, 16 do + local prev = previousInventory[slot] + local curr = currentInventory[slot] + + if not prev and curr then + table.insert(newlyAddedSlots, slot) + end + end + + if #newlyAddedSlots > 0 then + sendItemAwayMultiple(newlyAddedSlots) + end + end + + previousInventory = currentInventory + end + end +end + +local function run() + return function() + -- this needs to do something + print("inv.run ran") + while true do + sleep(1) + end + end +end + + +local function listItemAmounts() + return hopper.list("store") +end + + +local function listNames() + local aa = listItemAmounts() + local names = {} + + for name, _ in pairs(aa) do + table.insert(names, name) + end + + return names +end + +local function getItem(z) + return {} -- can't implement this well enough sadly, just expect every item exists +end + +return { + detectPlayerInsert = detectPlayerInsert, + sendItemAwayMultiple = sendItemAwayMultiple, + sendItemToSelf = sendItemToSelf, + getTurtleInventory = getTurtleInventory, + listItemAmounts = listItemAmounts, + listNames = listNames, + getItem = getItem, + sync = sync, + run = run +} diff --git a/src/modules/ui.lua b/src/modules/ui.lua index 12ec7cb..c0c775c 100644 --- a/src/modules/ui.lua +++ b/src/modules/ui.lua @@ -48,10 +48,14 @@ local function run() function() return com end, - function(option) - local z = option:match("^(%S+)") - if inv.getItem(z) then - inv.sendItemToSelf(z) + function(option, key) + if not option then return end + + if key == keys.enter then + local z = option:match("^(%S+)") + if inv.getItem(z) then + inv.sendItemToSelf(z) + end end end ) @@ -79,6 +83,17 @@ local function run() end end) + PrimeUI.addTask(function() + while true do + local _, osTimer = os.pullEvent("timer") + if osTimer == timer then + com = getFiltered() + updateEntries() + timer = os.startTimer(1) + end + end + end) + PrimeUI.run() end