first commit

This commit is contained in:
Soph :3 2025-11-11 17:58:10 +02:00
commit d533cad870
12 changed files with 2726 additions and 0 deletions

81
src/.beta-installer.lua Normal file
View file

@ -0,0 +1,81 @@
local base_url = "https://files.sad.ovh/public/storage-solution"
local api_url = base_url .. "?ls"
local download_root = "storage-solution"
local function fetch_folder_list()
local response = http.get(api_url)
if not response then
error("Failed to get folder list from Copyparty API")
end
local body = response.readAll()
response.close()
local data = textutils.unserializeJSON(body)
return data
end
local function download_file(path)
local file_url = base_url .. "/" .. path
local local_path = download_root .. "/" .. path
local response = http.get(file_url)
if response then
local file = fs.open(local_path, "wb")
if file then
file.write(response.readAll())
file.close()
end
response.close()
else
print("Failed to download: " .. file_url)
end
end
local function traverse_and_download(folder_data, prefix)
prefix = prefix or ""
for _, file in ipairs(folder_data.files or {}) do
print("Downloading: " .. prefix .. file.href)
download_file(prefix .. file.href)
end
for _, dir in ipairs(folder_data.dirs or {}) do
fs.makeDir(download_root .. "/" .. prefix .. dir.href)
local subdir_url = base_url .. "/" .. dir.href .. "?ls"
local response = http.get(subdir_url)
if response then
local body = response.readAll()
response.close()
local subdir_data = textutils.unserializeJSON(body)
traverse_and_download(subdir_data, prefix .. dir.href)
else
print("Failed to get subdirectory: " .. subdir_url)
end
end
end
if fs.exists(download_root) then
if fs.exists(download_root .. "/version") then
local previousVersion = fs.open(download_root .. "/version", "r").readAll()
local currentVersion = http.get(base_url .. "/version").readAll();
if previousVersion == currentVersion then
print("Previous version " .. previousVersion .. " is already installed, we're on " .. currentVersion .. " aswell, so skipping installation.")
shell.run("storage-solution/main.lua")
return
else
print("Version " .. previousVersion .. " was already installed. Uninstalling.")
fs.delete(download_root)
end
else
print("Version marker does not exist. Cannot install.")
shell.run("storage-solution/main.lua")
return
end
end
fs.makeDir(download_root)
local folder_list = fetch_folder_list()
traverse_and_download(folder_list, "")
print("Done :), installed Sophie's Storage Solution version " .. (http.get(base_url .. "/version").readAll()))
shell.run("storage-solution/main.lua")

101
src/.main-installer.lua Normal file
View file

@ -0,0 +1,101 @@
-- Forgejo-based installer for Sophie's Storage Solution
local repo_api = "https://git.sad.ovh/api/v1/repos/sophie/storage-solution"
local raw_base = "https://git.sad.ovh/sophie/storage-solution/raw/branch/main"
local branch = "main"
local download_root = "storage-solution"
-- Get latest commit hash for branch
local function fetch_commit_hash()
local url = repo_api .. "/branches/" .. branch
local response = http.get(url)
if not response then
error("Failed to fetch branch info: " .. url)
end
local body = response.readAll()
response.close()
local data = textutils.unserializeJSON(body)
return data.commit.id
end
-- Fetch file/folder listing from Forgejo API
local function fetch_repo_tree(path)
local url = repo_api .. "/contents" .. (path and ("/" .. path) or "")
local response = http.get(url)
if not response then
error("Failed to fetch repo tree: " .. url)
end
local body = response.readAll()
response.close()
return textutils.unserializeJSON(body)
end
-- Download a single file
local function download_file(path)
local url = raw_base .. "/" .. path
local local_path = download_root .. "/" .. path
fs.makeDir(fs.getDir(local_path))
local response = http.get(url)
if response then
local file = fs.open(local_path, "wb")
if file then
file.write(response.readAll())
file.close()
end
response.close()
print("Downloaded: " .. path)
else
print("Failed to download: " .. url)
end
end
-- Recursively traverse Forgejo folders
local function traverse_and_download(path)
local tree = fetch_repo_tree(path)
for _, entry in ipairs(tree) do
if entry.type == "file" then
download_file(entry.path)
elseif entry.type == "dir" then
traverse_and_download(entry.path)
end
end
end
-- Get current remote commit hash
local remote_hash = fetch_commit_hash()
print("Latest commit: " .. remote_hash)
-- Check for existing version
if fs.exists(download_root) then
if fs.exists(download_root .. "/version") then
local f = fs.open(download_root .. "/version", "r")
local local_hash = f.readAll()
f.close()
if local_hash == remote_hash then
print("Already up to date (commit " .. remote_hash:sub(1, 7) .. ").")
shell.run(download_root .. "/main.lua")
return
else
print("Outdated (" .. local_hash:sub(1, 7) .. " -> " .. remote_hash:sub(1, 7) .. "), reinstalling...")
fs.delete(download_root)
end
else
print("Version marker missing, reinstalling.")
fs.delete(download_root)
end
end
-- Create root directory
fs.makeDir(download_root)
-- Download everything
print("Fetching repository tree from Forgejo...")
traverse_and_download("")
-- Save commit hash as version
local f = fs.open(download_root .. "/version", "w")
f.write(remote_hash)
f.close()
print("Installed Sophie's Storage Solution (commit " .. remote_hash:sub(1, 7) .. ")")
shell.run(download_root .. "/main.lua")

File diff suppressed because it is too large Load diff

466
src/lib/primeui.lua Normal file
View file

@ -0,0 +1,466 @@
-- PrimeUI by JackMacWindows
-- Public domain/CC0
local expect = require "cc.expect".expect
-- Initialization code
local PrimeUI = {}
do
local coros = {}
local restoreCursor
--- Adds a task to run in the main loop.
---@param func function The function to run, usually an `os.pullEvent` loop
function PrimeUI.addTask(func)
expect(1, func, "function")
local t = {coro = coroutine.create(func)}
coros[#coros+1] = t
_, t.filter = coroutine.resume(t.coro)
end
--- Sends the provided arguments to the run loop, where they will be returned.
---@param ... any The parameters to send
function PrimeUI.resolve(...)
coroutine.yield(coros, ...)
end
--- Clears the screen and resets all components. Do not use any previously
--- created components after calling this function.
function PrimeUI.clear()
-- Reset the screen.
term.setCursorPos(1, 1)
term.setCursorBlink(false)
term.setBackgroundColor(colors.black)
term.setTextColor(colors.white)
term.clear()
-- Reset the task list and cursor restore function.
coros = {}
restoreCursor = nil
end
--- Sets or clears the window that holds where the cursor should be.
---@param win window|nil The window to set as the active window
function PrimeUI.setCursorWindow(win)
expect(1, win, "table", "nil")
restoreCursor = win and win.restoreCursor
end
--- Gets the absolute position of a coordinate relative to a window.
---@param win window The window to check
---@param x number The relative X position of the point
---@param y number The relative Y position of the point
---@return number x The absolute X position of the window
---@return number y The absolute Y position of the window
function PrimeUI.getWindowPos(win, x, y)
if win == term then return x, y end
while win ~= term.native() and win ~= term.current() 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
end
return x, y
end
--- Runs the main loop, returning information on an action.
---@return any ... The result of the coroutine that exited
function PrimeUI.run()
while true do
-- Restore the cursor and wait for the next event.
if restoreCursor then restoreCursor() end
local ev = table.pack(os.pullEvent())
-- Run all coroutines.
for _, v in ipairs(coros) do
if v.filter == nil or v.filter == ev[1] then
-- Resume the coroutine, passing the current event.
local res = table.pack(coroutine.resume(v.coro, table.unpack(ev, 1, ev.n)))
-- If the call failed, bail out. Coroutines should never exit.
if not res[1] then error(res[2], 2) end
-- If the coroutine resolved, return its values.
if res[2] == coros then return table.unpack(res, 3, res.n) end
-- Set the next event filter.
v.filter = res[2]
end
end
end
end
end
--- Creates a text input box.
---@param win window The window to draw on
---@param x number The X position of the left side of the box
---@param y number The Y position of the box
---@param width number The width/length of the box
---@param action function|string A function or `run` event to call when a key is pressed
---@param fgColor color|nil The color of the text (defaults to white)
---@param bgColor color|nil The color of the background (defaults to black)
---@param replacement string|nil A character to replace typed characters with
---@param history string[]|nil A list of previous entries to provide
---@param completion function|nil A function to call to provide completion
function PrimeUI.inputBox(win, x, y, width, action, fgColor, bgColor, replacement, history, completion, default)
expect(1, win, "table")
expect(2, x, "number")
expect(3, y, "number")
expect(4, width, "number")
expect(5, action, "function", "string")
fgColor = expect(6, fgColor, "number", "nil") or colors.white
bgColor = expect(7, bgColor, "number", "nil") or colors.black
expect(8, replacement, "string", "nil")
expect(9, history, "table", "nil")
expect(10, completion, "function", "nil")
expect(11, default, "string", "nil")
local box = window.create(win, x, y, width, 1)
box.setTextColor(fgColor)
box.setBackgroundColor(bgColor)
box.clear()
PrimeUI.addTask(function()
local text = default or ""
local cursor = #text + 1
local histIndex = nil
local function runAction()
if type(action) == "string" then
PrimeUI.resolve("inputBox", action, text)
else
action(text)
end
end
local function redraw()
box.clear()
box.setCursorPos(1, 1)
if replacement then
box.write(string.rep(replacement, #text))
else
box.write(text)
end
box.setCursorPos(cursor, 1)
runAction()
end
redraw()
while true do
local ev, p1 = os.pullEvent()
if ev == "char" then
text = text:sub(1, cursor - 1) .. p1 .. text:sub(cursor)
cursor = cursor + 1
redraw()
elseif ev == "key" then
if p1 == keys.enter then
runAction()
elseif p1 == keys.left then
cursor = math.max(1, cursor - 1)
box.setCursorPos(cursor, 1)
elseif p1 == keys.right then
cursor = math.min(#text + 1, cursor + 1)
box.setCursorPos(cursor, 1)
elseif p1 == keys.backspace then
if cursor > 1 then
text = text:sub(1, cursor - 2) .. text:sub(cursor)
cursor = cursor - 1
redraw()
end
elseif p1 == keys.delete then
text = text:sub(1, cursor - 1) .. text:sub(cursor + 1)
redraw()
elseif p1 == keys.up and history then
if not histIndex then histIndex = #history + 1 end
histIndex = math.max(1, histIndex - 1)
text = history[histIndex] or ""
cursor = #text + 1
redraw()
elseif p1 == keys.down and history then
if histIndex then
histIndex = math.min(#history + 1, histIndex + 1)
text = history[histIndex] or ""
cursor = #text + 1
redraw()
end
end
end
end
end)
end
--- Draws a line of text at a position.
---@param win window The window to draw on
---@param x number The X position of the left side of the text
---@param y number The Y position of the text
---@param text string The text to draw
---@param fgColor color|nil The color of the text (defaults to white)
---@param bgColor color|nil The color of the background (defaults to black)
function PrimeUI.label(win, x, y, text, fgColor, bgColor)
expect(1, win, "table")
expect(2, x, "number")
expect(3, y, "number")
expect(4, text, "string")
fgColor = expect(5, fgColor, "number", "nil") or colors.white
bgColor = expect(6, bgColor, "number", "nil") or colors.black
win.setCursorPos(x, y)
win.setTextColor(fgColor)
win.setBackgroundColor(bgColor)
win.write(text)
end
--- Creates a scrollable window, which allows drawing large content in a small area.
---@param win window The parent window of the scroll box
---@param x number The X position of the box
---@param y number The Y position of the box
---@param width number The width of the box
---@param height number The height of the outer box
---@param innerHeight number The height of the inner scroll area
---@param allowArrowKeys boolean|nil Whether to allow arrow keys to scroll the box (defaults to true)
---@param showScrollIndicators boolean|nil Whether to show arrow indicators on the right side when scrolling is available, which reduces the inner width by 1 (defaults to false)
---@param fgColor number|nil The color of scroll indicators (defaults to white)
---@param bgColor color|nil The color of the background (defaults to black)
---@return window inner The inner window to draw inside
---@return fun(pos:number) scroll A function to manually set the scroll position of the window
function PrimeUI.scrollBox(win, x, y, width, height, innerHeight, allowArrowKeys, showScrollIndicators, fgColor, bgColor)
expect(1, win, "table")
expect(2, x, "number")
expect(3, y, "number")
expect(4, width, "number")
expect(5, height, "number")
expect(6, innerHeight, "number")
expect(7, allowArrowKeys, "boolean", "nil")
expect(8, showScrollIndicators, "boolean", "nil")
fgColor = expect(9, fgColor, "number", "nil") or colors.white
bgColor = expect(10, bgColor, "number", "nil") or colors.black
if allowArrowKeys == nil then allowArrowKeys = true end
-- Create the outer container box.
local outer = window.create(win == term and term.current() or win, x, y, width, height)
outer.setBackgroundColor(bgColor)
outer.clear()
-- Create the inner scrolling box.
local inner = window.create(outer, 1, 1, width - (showScrollIndicators and 1 or 0), innerHeight)
inner.setBackgroundColor(bgColor)
inner.clear()
-- Draw scroll indicators if desired.
if showScrollIndicators then
outer.setBackgroundColor(bgColor)
outer.setTextColor(fgColor)
outer.setCursorPos(width, height)
outer.write(innerHeight > height and "\31" or " ")
end
-- Get the absolute position of the window.
x, y = PrimeUI.getWindowPos(win, x, y)
-- Add the scroll handler.
local scrollPos = 1
PrimeUI.addTask(function()
while true do
-- Wait for next event.
local ev = table.pack(os.pullEvent())
-- Update inner height in case it changed.
innerHeight = select(2, inner.getSize())
-- Check for scroll events and set direction.
local dir
if ev[1] == "key" and allowArrowKeys then
if ev[2] == keys.up then dir = -1
elseif ev[2] == keys.down then dir = 1 end
elseif ev[1] == "mouse_scroll" and ev[3] >= x and ev[3] < x + width and ev[4] >= y and ev[4] < y + height then
dir = ev[2]
end
-- If there's a scroll event, move the window vertically.
if dir and (scrollPos + dir >= 1 and scrollPos + dir <= innerHeight - height) then
scrollPos = scrollPos + dir
inner.reposition(1, 2 - scrollPos)
end
-- Redraw scroll indicators if desired.
if showScrollIndicators then
outer.setBackgroundColor(bgColor)
outer.setTextColor(fgColor)
outer.setCursorPos(width, 1)
outer.write(scrollPos > 1 and "\30" or " ")
outer.setCursorPos(width, height)
outer.write(scrollPos < innerHeight - height and "\31" or " ")
end
end
end)
-- Make a function to allow external scrolling.
local function scroll(pos)
expect(1, pos, "number")
pos = math.floor(pos)
expect.range(pos, 1, innerHeight - height)
-- Scroll the window.
scrollPos = pos
inner.reposition(1, 2 - scrollPos)
-- Redraw scroll indicators if desired.
if showScrollIndicators then
outer.setBackgroundColor(bgColor)
outer.setTextColor(fgColor)
outer.setCursorPos(width, 1)
outer.write(scrollPos > 1 and "\30" or " ")
outer.setCursorPos(width, height)
outer.write(scrollPos < innerHeight - height and "\31" or " ")
end
end
return inner, scroll
end
--- Creates a list of entries that can each be selected.
---@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 entries function A function that returns a list of entries to show, where the value is whether the item is pre-selected (or `"R"` for required/forced selected)
---@param action function|string A function or `run` event that's called when a selection is made
---@param selectChangeAction function|string|nil A function or `run` event that's called when the current selection is changed
---@param fgColor color|nil The color of the text (defaults to white)
---@param bgColor color|nil The color of the background (defaults to black)
function PrimeUI.selectionBox(win, x, y, width, height, entries, action, selectChangeAction, fgColor, bgColor)
expect(1, win, "table")
expect(2, x, "number")
expect(3, y, "number")
expect(4, width, "number")
expect(5, height, "number")
expect(6, entries, "function")
expect(7, action, "function", "string")
expect(8, selectChangeAction, "function", "string", "nil")
fgColor = expect(9, fgColor, "number", "nil") or colors.white
bgColor = expect(10, bgColor, "number", "nil") or colors.black
local entrywin = window.create(win, x, y, width, height)
local selection, scroll = 1, 1
-- Create a function to redraw the entries on screen.
local function drawEntries()
-- Clear and set invisible for performance.
entrywin.setVisible(false)
entrywin.setBackgroundColor(bgColor)
entrywin.clear()
-- Draw each entry in the scrolled region.
for i = scroll, scroll + height - 1 do
-- Get the entry; stop if there's no more.
local e = entries()[i]
if not e then break end
-- Set the colors: invert if selected.
entrywin.setCursorPos(2, i - scroll + 1)
if i == selection then
entrywin.setBackgroundColor(fgColor)
entrywin.setTextColor(bgColor)
else
entrywin.setBackgroundColor(bgColor)
entrywin.setTextColor(fgColor)
end
-- Draw the selection.
entrywin.clearLine()
entrywin.write(#e > width - 1 and e:sub(1, width - 4) .. "..." or e)
end
-- Draw scroll arrows.
entrywin.setBackgroundColor(bgColor)
entrywin.setTextColor(fgColor)
entrywin.setCursorPos(width, 1)
entrywin.write("\30")
entrywin.setCursorPos(width, height)
entrywin.write("\31")
-- Send updates to the screen.
entrywin.setVisible(true)
end
-- Draw first screen.
drawEntries()
-- Add a task for selection keys.
PrimeUI.addTask(function()
while true do
local event, key, cx, cy = os.pullEvent()
if event == "key" then
if key == keys.down and selection < #entries() then
-- Move selection down.
selection = selection + 1
if selection > scroll + height - 1 then scroll = scroll + 1 end
-- Send action if necessary.
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
elseif selectChangeAction then selectChangeAction(selection) end
-- Redraw screen.
drawEntries()
elseif key == keys.up and selection > 1 then
-- Move selection up.
selection = selection - 1
if selection < scroll then scroll = scroll - 1 end
-- Send action if necessary.
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
elseif selectChangeAction then selectChangeAction(selection) end
-- Redraw screen.
drawEntries()
elseif key == keys.enter then
-- Select the entry: send the action.
if type(action) == "string" then PrimeUI.resolve("selectionBox", action, entries()[selection])
else action(entries()[selection]) end
end
elseif event == "mouse_click" and key == 1 then
-- Handle clicking the scroll arrows.
local wx, wy = PrimeUI.getWindowPos(entrywin, 1, 1)
if cx == wx + width - 1 then
if cy == wy and selection > 1 then
-- Move selection up.
selection = selection - 1
if selection < scroll then scroll = scroll - 1 end
-- Send action if necessary.
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
elseif selectChangeAction then selectChangeAction(selection) end
-- Redraw screen.
drawEntries()
elseif cy == wy + height - 1 and selection < #entries() then
-- Move selection down.
selection = selection + 1
if selection > scroll + height - 1 then scroll = scroll + 1 end
-- Send action if necessary.
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
elseif selectChangeAction then selectChangeAction(selection) end
-- Redraw screen.
drawEntries()
end
elseif cx >= wx and cx < wx + width - 1 and cy >= wy and cy < wy + height then
local sel = scroll + (cy - wy)
if sel == selection then
-- Select the entry: send the action.
if type(action) == "string" then PrimeUI.resolve("selectionBox", action, entries()[selection])
else action(entries()[selection]) end
else
selection = sel
-- Send action if necessary.
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
elseif selectChangeAction then selectChangeAction(selection) end
-- Redraw screen.
drawEntries()
end
end
elseif event == "mouse_scroll" then
-- Handle mouse scrolling.
local wx, wy = PrimeUI.getWindowPos(entrywin, 1, 1)
if cx >= wx and cx < wx + width and cy >= wy and cy < wy + height then
if key < 0 and selection > 1 then
-- Move selection up.
selection = selection - 1
if selection < scroll then scroll = scroll - 1 end
-- Send action if necessary.
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
elseif selectChangeAction then selectChangeAction(selection) end
-- Redraw screen.
drawEntries()
elseif key > 0 and selection < #entries() then
-- Move selection down.
selection = selection + 1
if selection > scroll + height - 1 then scroll = scroll + 1 end
-- Send action if necessary.
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
elseif selectChangeAction then selectChangeAction(selection) end
-- Redraw screen.
drawEntries()
end
end
end
end
end)
return drawEntries
end
return {
PrimeUI = PrimeUI
}

42
src/main.lua Normal file
View file

@ -0,0 +1,42 @@
---@class Config
---@field inventories string[]
---@field import string[]|nil
---@field chatbox table<string, string>|nil
local inv = require("modules.inv")
local ui = require("modules.ui")
local chatbox = require("modules.chatbox")
local config = require("../config") ---@type Config
local function importMechanism()
if config.import == nil then
return
end
if #config.import == 0 then
return
end
while true do
for _, import_from_inv in ipairs(config.import) do
---@type ccTweaked.peripheral.Inventory
local perip = peripheral.wrap(import_from_inv)
if perip and perip.size then
local slotsToSend = {}
for slot = 1, perip.size() do
local item = perip.getItemDetail(slot)
if item then
table.insert(slotsToSend, slot)
end
end
if #slotsToSend > 0 then
inv.sendItemAwayMultiple(slotsToSend, perip, import_from_inv)
end
end
end
sleep(0.1)
end
end
inv.sync()
parallel.waitForAll(chatbox.run, inv.getAIL().run, importMechanism, ui.runUi, inv.detectPlayerInsert)

212
src/modules/chatbox.lua Normal file
View file

@ -0,0 +1,212 @@
local config = require("../../config") ---@type Config
local inv = require("modules.inv")
local function levDist(s, t)
local n = #s
local m = #t
if n == 0 then return m end
if m == 0 then return n end
-- create matrix
local d = {}
for i = 0, n do
d[i] = {}
end
-- initialize
for i = 0, n do d[i][0] = i end
for j = 0, m do d[0][j] = j end
-- main loop
for i = 1, n do
local s_i = s:sub(i, i)
for j = 1, m do
-- safeguard shortcut
if i == j and d[i][j] and d[i][j] > 4 then
return n
end
local t_j = t:sub(j, j)
local cost = (s_i == t_j) and 0 or 1
local mi = math.min(
d[i - 1][j] + 1,
d[i][j - 1] + 1,
d[i - 1][j - 1] + cost
)
d[i][j] = mi
-- Damerau transposition
if i > 1 and j > 1 and s_i == t:sub(j - 1, j - 1) and s:sub(i - 1, i - 1) == t_j then
d[i][j] = math.min(d[i][j], d[i - 2][j - 2] + cost)
end
end
end
return d[n][m]
end
local function findBest(data, item)
local sorted_data = {}
for _, z in ipairs(data) do
local parts = {}
for part in string.gmatch(z, "([^:]+)") do
table.insert(parts, part)
end
local key = parts[1] .. ":" .. (parts[2] or "")
local dist = levDist(item, parts[2] or "")
table.insert(sorted_data, { key, dist })
end
table.sort(sorted_data, function(a, b)
return a[2] < b[2]
end)
local best = {}
local best_dist = sorted_data[1][2]
for _, z in ipairs(sorted_data) do
if z[2] == best_dist then
table.insert(best, z)
else
break
end
end
return best
end
local BOT_NAME = "&cS &eI&an&3c &5S&cI&6S"
function auth(user)
local manip = config.chatbox[user]
if manip then
return true
else
chatbox.tell(user, "You are not authorized to use this command.", BOT_NAME)
return false
end
end
function run()
if config.chatbox == nil then return end
if #config.chatbox == 0 then return end
while true do
local _, user, command, args = os.pullEvent("command")
if command == "sis" then
if args[1] == "whoami" then
local manip = config.chatbox[user]
if manip then
chatbox.tell(user, "You are " .. user .. ", linked with `" .. manip .."`.", BOT_NAME)
else
chatbox.tell(user, "You are not registered. Ask the creator of this SIS to be registered.", BOT_NAME)
end
elseif args[1] == "deposit" then
if not auth(user) then goto continue end
if not args[2] then
chatbox.tell(user, "Supply a item (minecraft:`diamond` part) to deposit it!", BOT_NAME)
goto continue
end
local perip = peripheral.wrap(config.chatbox[user])
---@type ccTweaked.peripheral.Inventory
local peripInventory = perip.getInventory()
local invList = peripInventory.list()
local allItems = {}
local seen = {}
for _, stack in pairs(invList) do
local name = stack and stack.name
if name and not seen[name] then
seen[name] = true
allItems[#allItems + 1] = name
end
end
local best = findBest(allItems, args[2])
local itemName = ""
if #best > 1 then
itemName = best[1][1]
if string.find(":", args[2]) then
itemName = args[2]
end
chatbox.tell(user, "`WARNING`: Item most likely inaccurate. Assuming " .. itemName, BOT_NAME)
else
itemName = best[1][1]
end
local slots = {}
for slot, item in pairs(peripInventory.list()) do
if item.name == itemName then
table.insert(slots, slot)
end
end
local amount = nil
if args[3] then
amount = tonumber(args[3], 10)
end
local moved = inv.sendItemAwayMultiple(slots, peripInventory, peripInventory, amount)
chatbox.tell(user, "Moved `" .. tostring(moved) .. "` items.", BOT_NAME)
elseif args[1] == "withdraw" then
if not auth(user) then goto continue end
if not args[2] then
chatbox.tell(user, "Supply a item (minecraft:`diamond` part) to withdraw it!", BOT_NAME)
goto continue
end
local perip = peripheral.wrap(config.chatbox[user])
---@type ccTweaked.peripheral.Inventory
local peripInventory = perip.getInventory()
local best = findBest(inv.getAIL().listNames(), args[2])
local itemName = ""
if #best > 1 then
itemName = best[1][1]
if string.find(":", args[2]) then
itemName = args[2]
end
chatbox.tell(user, "`WARNING`: Item most likely inaccurate. Assuming " .. itemName, BOT_NAME)
else
itemName = best[1][1]
end
local amount = nil
if args[3] then
amount = tonumber(args[3], 10)
end
local moved = inv.sendItemToSelf(itemName, peripInventory, amount)
chatbox.tell(user, "Moved `" .. tostring(moved) .. "` items.", BOT_NAME)
end
end
::continue::
end
end
return {
run = run
}

199
src/modules/inv.lua Normal file
View file

@ -0,0 +1,199 @@
local previousInventory = {}
---@type ccTweaked.peripheral.WiredModem
local modem = peripheral.find("modem");
local turtleId = modem.getNameLocal()
local turtleMoveAllowed = true
local config = require("../../config") ---@type Config
local abstractInventoryLib = require("lib.abstractInventoryLib")
local peripherals = peripheral.getNames();
---@type AbstractInventory
local ail = nil
local function sync()
print("Syncing..")
---@type string[]
local foundInventories = {}
for _, invPattern in ipairs(config.inventories) do
for _, inv in ipairs(peripherals) do
if string.find(inv, invPattern) then
table.insert(foundInventories, inv)
end
end
end
ail = abstractInventoryLib(foundInventories, true)
print("Synced.");
end
---@param itemName string
---@param perip ccTweaked.peripheral.Inventory|nil
---@param maxAmount number|nil
local function sendItemToSelf(itemName, perip, maxAmount)
if perip == nil then
perip = turtleId
end
turtleMoveAllowed = false
local remaining = maxAmount or 64
local total = 0
if remaining <= 0 then
turtleMoveAllowed = true
return
end
if ail.getItem(itemName) then
local nbtList = ail.listNBT(itemName)
local chosenNBT = nil
if nbtList and #nbtList > 0 then
chosenNBT = nbtList[math.random(1, #nbtList)]
end
if chosenNBT == "NONE" then
chosenNBT = nil
end
while remaining > 0 do
local toSend = math.min(64, remaining)
local ok, moved = pcall(function()
ail.performTransfer()
--chatbox.tell("hartbreix", tostring(perip) .. " <> " .. tostring(itemName) .. " <> " .. tostring(toSend) .. " <> " .. tostring(chosenNBT), "debug")
local amount = ail.pushItems(perip, itemName, toSend, nil, chosenNBT, {
["allowBadTransfers"] = true,
["optimal"] = true
})
ail.performTransfer()
return amount
end)
total = total + moved
if ok and moved and moved > 0 then
remaining = remaining - moved
else
break
end
end
end
-- sleep(0.1)
turtleMoveAllowed = true
return total
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 turtleId
local itemsInSlots = {}
for _, slot in ipairs(slots) do
local item = perip.getItemDetail(slot)
if item then
itemsInSlots[slot] = item
end
end
local totalMoved = 0
local remaining = maxAmount or math.huge
if remaining <= 0 then
return 0
end
for _, slot in ipairs(slots) do
if remaining <= 0 then break end
local toSend = math.min(64, remaining)
local ok, pulled = pcall(function()
ail.performTransfer()
local moved = ail.pullItems(srcId, slot, toSend, nil, nil, {
["allowBadTransfers"] = true,
["optimal"] = false
})
ail.performTransfer()
return moved
end)
if ok and pulled and pulled > 0 then
totalMoved = totalMoved + pulled
remaining = remaining - pulled
end
end
local numItems = 0
for _, _ in pairs(itemsInSlots) do
numItems = numItems + 1
end
if numItems > 0 then
os.queueEvent("update_ui")
end
return totalMoved
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()
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
local function getAIL()
return ail
end
return {
detectPlayerInsert = detectPlayerInsert,
sendItemAwayMultiple = sendItemAwayMultiple,
sendItemToSelf = sendItemToSelf,
getTurtleInventory = getTurtleInventory,
sync = sync,
getAIL = getAIL,
}

71
src/modules/ui.lua Normal file
View file

@ -0,0 +1,71 @@
local inv = require("modules.inv");
local PrimeUI = require("lib.primeui").PrimeUI;
local function runUi()
local search = ""
local function getFiltered()
local filtered = {}
for name, count in pairs(inv.getAIL().listItemAmounts()) do
if search == "" or string.find(name:lower(), search:lower(), 1, true) then
table.insert(filtered, { name = name, count = count })
end
end
table.sort(filtered, function(a, b) return a.count > b.count end)
local refiltered = {}
for i, filter in ipairs(filtered) do
refiltered[i] = filter.name .. " -> " .. tostring(filter.count)
end
return refiltered
end
local com = getFiltered("")
local win = term.current();
local w , h= term.getSize()
PrimeUI.clear()
local updateEntries = PrimeUI.selectionBox(
win, 1, 4, w, h-7,
function ()
return com
end,
function(option)
local z = option:match("^([^\\s]*)")
if inv.getAIL().getItem(z) then
inv.sendItemToSelf(z)
end
end
)
PrimeUI.inputBox(win, 2, 2, w-2, function (data)
search = data
com = getFiltered()
updateEntries()
end)
PrimeUI.label(win, 2, h-1, "primeui ui refucked")
PrimeUI.label(win, 2, h-2, "rev. " .. fs.open("storage-solution/version", "r").readAll())
local timer = os.startTimer(1)
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
return {
runUi = runUi
}