Compare commits

...

10 Commits

Author SHA1 Message Date
Max 8d9abc8cb2 Add whitelist capabilities 2026-06-01 13:43:18 +02:00
Max 14961629f2 Translate everything to english 2026-06-01 13:31:37 +02:00
Max 2a15e704b2 Auto pull dependencies 2026-06-01 13:29:41 +02:00
Max 991bc5eb3e Let scrollbar only scroll to whole row numbers 2026-06-01 05:19:36 +02:00
Max 0a9380cd22 Remember overview scroll position 2026-06-01 05:14:01 +02:00
Max d381ac1693 Implement page scrolling (seperate into runScrollableGrid) 2026-06-01 05:12:24 +02:00
Max 8a5e4a0fb1 Implement overview scrolling 2026-06-01 05:04:39 +02:00
Max e5f7da3cc5 Update fallback icon 2026-06-01 04:27:01 +02:00
Max 4f623656bb Resort overview every 30seconds 2026-06-01 04:23:27 +02:00
Max acfc5e7324 Added sorting to Overview 2026-06-01 04:22:06 +02:00
3 changed files with 460 additions and 27792 deletions
+431 -72
View File
@@ -1,12 +1,26 @@
local mon = peripheral.find("monitor") local mon = peripheral.find("monitor")
assert(mon, "Kein Monitor gefunden") assert(mon, "No monitor found")
local monName = peripheral.getName(mon) local monName = peripheral.getName(mon)
local SCREEN_WIDTH = 40
mon.setTextScale(0.5) mon.setTextScale(0.5)
local SCREEN_WIDTH, SCREEN_HEIGHT = mon.getSize()
local oldTerm = term.redirect(mon) local oldTerm = term.redirect(mon)
local function ensureDependency(path, url)
if fs.exists(path) then
return
end
assert(shell and type(shell.run) == "function", "Shell API is not available")
assert(shell.run("wget", url, path) and fs.exists(path), "Could not download dependency: " .. path)
end
ensureDependency("Pine3D.lua", "https://raw.githubusercontent.com/Xella37/Pine3D/main/Pine3D.lua")
ensureDependency("betterblittle.lua", "https://raw.githubusercontent.com/Xella37/Pine3D/main/betterblittle.lua")
ensureDependency("morefonts-pe.lua", "https://raw.githubusercontent.com/MichielP1807/more-fonts/main/pine3d/morefonts-pe.lua")
local Pine3D = require("Pine3D") local Pine3D = require("Pine3D")
local mf = require("morefonts-pe") local mf = require("morefonts-pe")
@@ -18,23 +32,61 @@ local chainItemsByBaseId
local baseItemIds local baseItemIds
local itemById local itemById
local defaultBaseId local defaultBaseId
local whitelistLookup
local fallbackIcon = false local fallbackIcon = false
local backButton = false local backButton = false
local scrollUpButton = false
local scrollDownButton = false
local overviewScrollCurrentRow = 0
local overviewScrollTargetRow = 0
local drawOverview local drawOverview
local function ensureWhitelistLoaded()
if whitelistLookup ~= nil then
return
end
if not fs.exists("whitelist.json") then
whitelistLookup = false
return
end
local file = assert(fs.open("whitelist.json", "r"))
local data = textutils.unserializeJSON(file.readAll())
file.close()
assert(type(data) == "table", "Invalid whitelist.json data")
if #data == 0 then
whitelistLookup = false
return
end
whitelistLookup = {}
for i = 1, #data do
local baseId = data[i]
assert(type(baseId) == "string", "Invalid whitelist.json entry")
whitelistLookup[baseId] = true
end
end
local function ensureChainsLoaded() local function ensureChainsLoaded()
if not chainItemsByBaseId then if not chainItemsByBaseId then
ensureWhitelistLoaded()
local file = assert(fs.open("atc_chains.json", "r")) local file = assert(fs.open("atc_chains.json", "r"))
local data = textutils.unserializeJSON(file.readAll()) local data = textutils.unserializeJSON(file.readAll())
file.close() file.close()
assert(type(data) == "table" and type(data.chains) == "table", "Ungueltige JSON-Daten") assert(type(data) == "table" and type(data.chains) == "table", "Invalid JSON data")
chainItemsByBaseId = {} chainItemsByBaseId = {}
baseItemIds = {} baseItemIds = {}
itemById = {} itemById = {}
for _, chain in ipairs(data.chains) do for _, chain in ipairs(data.chains) do
if not whitelistLookup or whitelistLookup[chain.base_id] then
if not defaultBaseId then if not defaultBaseId then
defaultBaseId = chain.base_id defaultBaseId = chain.base_id
end end
@@ -45,7 +97,12 @@ local function ensureChainsLoaded()
local itemCount = 0 local itemCount = 0
for _, item in ipairs(chain.items or {}) do for _, item in ipairs(chain.items or {}) do
if item.stage ~= "item" then if item.stage == "item" then
itemById[item.item_id] = {
id = item.item_id,
icon_nfp = item.icon_nfp_16x16,
}
else
itemCount = itemCount + 1 itemCount = itemCount + 1
items[itemCount] = { items[itemCount] = {
id = item.item_id, id = item.item_id,
@@ -59,6 +116,7 @@ local function ensureChainsLoaded()
end end
end end
end end
end
local function getPageItems(base_id) local function getPageItems(base_id)
ensureChainsLoaded() ensureChainsLoaded()
@@ -193,8 +251,24 @@ end
local function getFallbackIcon() local function getFallbackIcon()
if fallbackIcon == false then if fallbackIcon == false then
local ok, image = pcall(paintutils.loadImage, "/icons16/diamond16.nfp") fallbackIcon = parseNfpImage(table.concat({
fallbackIcon = ok and image or nil "0000000000000000",
"000000eeee000000",
"00000eeeeee00000",
"0000eee00eee0000",
"00000e00000ee000",
"0000000000eee000",
"000000000eee0000",
"00000000eee00000",
"00000000ee000000",
"0000000000000000",
"00000000ee000000",
"00000000ee000000",
"0000000000000000",
"0000000000000000",
"0000000000000000",
"0000000000000000",
}, "\n"))
end end
return fallbackIcon return fallbackIcon
@@ -212,6 +286,30 @@ local function getBackButton()
return backButton return backButton
end end
local function getScrollUpButton()
if scrollUpButton == false then
scrollUpButton = parseNfpImage(table.concat({
" 00 ",
"0000",
"0 0",
}, "\n"))
end
return scrollUpButton
end
local function getScrollDownButton()
if scrollDownButton == false then
scrollDownButton = parseNfpImage(table.concat({
"0 0",
"0000",
" 00 ",
}, "\n"))
end
return scrollDownButton
end
local function imageSize(img) local function imageSize(img)
local w, h = 0, 0 local w, h = 0, 0
@@ -266,22 +364,52 @@ local function drawNfpScaled(buffer, img, x, y, scaleX, scaleY)
for imgY, row in pairs(img) do for imgY, row in pairs(img) do
for imgX, col in pairs(row) do for imgX, col in pairs(row) do
-- WICHTIG: -- IMPORTANT:
-- Nur echte ComputerCraft-Farben in den Pine3D-Buffer schreiben. -- Only write real ComputerCraft colors into the Pine3D buffer.
-- Keine Strings, keine nils, keine 0. -- No strings, no nil values, no 0.
if type(col) == "number" and col > 0 then if type(col) == "number" and col > 0 then
local px = x + (imgX - 1) * scaleX local px = x + (imgX - 1) * scaleX
local py = y + (imgY - 1) * scaleY local py = y + (imgY - 1) * scaleY
for dy = 0, scaleY - 1 do for dy = 0, scaleY - 1 do
for dx = 0, scaleX - 1 do for dx = 0, scaleX - 1 do
buffer:setPixel(px + dx, py + dy, col) local drawX = px + dx
local drawY = py + dy
if drawX >= 1 and drawX <= buffer.width and drawY >= 1 and drawY <= buffer.height then
buffer:setPixel(drawX, drawY, col)
end end
end end
end end
end end
end end
end end
end
local function fillRect(buffer, x, y, width, height, color)
local startX = math.max(1, x)
local startY = math.max(1, y)
local endX = math.min(buffer.width, x + width - 1)
local endY = math.min(buffer.height, y + height - 1)
if startX > endX or startY > endY then
return
end
for py = startY, endY do
for px = startX, endX do
buffer:setPixel(px, py, color)
end
end
end
local function cellToPixelX(cellX)
return (cellX - 1) * 2 + 1
end
local function cellToPixelY(cellY)
return (cellY - 1) * 3 + 1
end
local function assertBufferValid(frame) local function assertBufferValid(frame)
local valid = { local valid = {
@@ -308,16 +436,16 @@ local function assertBufferValid(frame)
local c = frame.buffer.colorValues[y][x] local c = frame.buffer.colorValues[y][x]
if not valid[c] then if not valid[c] then
error("Ungueltiger Pixel im Buffer bei x=" .. x .. ", y=" .. y .. ": " .. tostring(c)) error("Invalid pixel in buffer at x=" .. x .. ", y=" .. y .. ": " .. tostring(c))
end end
end end
end end
end end
local function drawItem(img, cellX, cellY, count) local function drawItem(img, cellX, cellY, count, offsetY)
-- Normale Monitor-Zellen in Pine3D-Teletext-Pixel umrechnen. -- Convert normal monitor cells into Pine3D teletext pixels.
local x = (cellX - 1) * 2 + 1 local x = cellToPixelX(cellX)
local y = (cellY - 1) * 3 + 1 local y = cellToPixelY(cellY) + (offsetY or 0)
local imgW, imgH = 0, 0 local imgW, imgH = 0, 0
@@ -326,8 +454,10 @@ local function drawItem(img, cellX, cellY, count)
if img then if img then
imgW, imgH = imageSize(img) imgW, imgH = imageSize(img)
if y <= frame.buffer.height and y + imgH * scaleY - 1 >= 1 then
drawNfpScaled(frame.buffer, img, x, y, scaleX, scaleY) drawNfpScaled(frame.buffer, img, x, y, scaleX, scaleY)
end end
end
local text = formatCount(count) local text = formatCount(count)
@@ -345,91 +475,320 @@ local function drawItem(img, cellX, cellY, count)
dy = 1, dy = 1,
} }
-- Schatten if bottomY >= 1 and y <= frame.buffer.height then
-- Shadow
mf.writeOn(frame, text, colors.black, rightX + 1, bottomY + 1, options) mf.writeOn(frame, text, colors.black, rightX + 1, bottomY + 1, options)
-- Weißer Count -- White count
mf.writeOn(frame, text, colors.white, rightX, bottomY, options) mf.writeOn(frame, text, colors.white, rightX, bottomY, options)
end end
end
local function runScrollableGrid(options)
local entries = {}
local counts = {}
local visibleItemCount = 0
local totalRows = 0
local maxScrollRow = 0
local currentScrollRow = 0
local targetScrollRow = 0
local refreshTimer
local animationTimer
local columns = 3
local rowsPerView = 3
local baseCellX = 4
local baseCellY = 2
local colStepCells = 12
local rowStepCells = 6
local rowStepPixels = rowStepCells * 3
local scrollbarX = SCREEN_WIDTH - 1
local scrollbarWidth = 2
local scrollbarButtonHeight = 3
local scrollbarTrackY = scrollbarButtonHeight + 1
local scrollbarTrackHeight = SCREEN_HEIGHT - scrollbarButtonHeight * 2
local scrollbarPixelX = cellToPixelX(scrollbarX)
local scrollbarPixelWidth = scrollbarWidth * 2
local scrollbarTrackPixelY = cellToPixelY(scrollbarTrackY)
local scrollbarTrackPixelHeight = math.max(0, scrollbarTrackHeight * 3)
if options.getInitialScrollState then
local initialCurrentRow, initialTargetRow = options.getInitialScrollState()
currentScrollRow = tonumber(initialCurrentRow) or 0
targetScrollRow = tonumber(initialTargetRow) or currentScrollRow
end
local function snapScrollRow(row)
return math.floor((tonumber(row) or 0) + 0.5)
end
local function persistScrollState()
if options.setScrollState then
options.setScrollState(snapScrollRow(targetScrollRow), snapScrollRow(targetScrollRow))
end
end
local function clampScrollRow(row)
return math.max(0, math.min(maxScrollRow, row))
end
local function getThumbMetrics()
if scrollbarTrackPixelHeight <= 0 then
return scrollbarTrackPixelY, 0
end
if totalRows <= rowsPerView or maxScrollRow == 0 then
return scrollbarTrackPixelY, scrollbarTrackPixelHeight
end
local thumbHeight = math.max(4, math.floor(scrollbarTrackPixelHeight * rowsPerView / totalRows + 0.5))
local thumbTravel = scrollbarTrackPixelHeight - thumbHeight
local thumbY = scrollbarTrackPixelY + math.floor((currentScrollRow / maxScrollRow) * thumbTravel + 0.5)
return thumbY, thumbHeight
end
local function scheduleAnimation()
if not animationTimer and math.abs(targetScrollRow - currentScrollRow) > 0.001 then
animationTimer = os.startTimer(0.05)
end
end
local function scrollTo(row)
local clampedRow = clampScrollRow(snapScrollRow(row))
if clampedRow ~= targetScrollRow then
targetScrollRow = clampedRow
persistScrollState()
scheduleAnimation()
end
end
local function refreshGridData()
entries, counts = options.getEntriesAndCounts()
end
local function renderGrid()
totalRows = math.ceil(#entries / columns)
maxScrollRow = math.max(0, totalRows - rowsPerView)
currentScrollRow = clampScrollRow(currentScrollRow)
targetScrollRow = clampScrollRow(snapScrollRow(targetScrollRow))
persistScrollState()
local firstVisibleRow = math.floor(currentScrollRow)
local rowOffsetPixels = -math.floor((currentScrollRow - firstVisibleRow) * rowStepPixels + 0.5)
local firstVisibleIndex = firstVisibleRow * columns + 1
local lastVisibleIndex = math.min(#entries, (firstVisibleRow + rowsPerView + 1) * columns)
frame.buffer:clear()
if options.drawChrome then
options.drawChrome()
end
for i = firstVisibleIndex, lastVisibleIndex do
local entry = entries[i]
local relativeIndex = i - firstVisibleIndex
local col = relativeIndex % columns
local row = math.floor(relativeIndex / columns)
drawItem(
options.getIcon(entry),
baseCellX + colStepCells * col,
baseCellY + rowStepCells * row,
counts[options.getId(entry)] or 0,
rowOffsetPixels
)
end
visibleItemCount = math.max(0, math.min(#entries - firstVisibleRow * columns, rowsPerView * columns))
local upButtonY = cellToPixelY(1)
local downButtonY = cellToPixelY(SCREEN_HEIGHT - scrollbarButtonHeight + 1)
local upColor = targetScrollRow > 0 and colors.gray or colors.lightGray
local downColor = targetScrollRow < maxScrollRow and colors.gray or colors.lightGray
local thumbY, thumbHeight = getThumbMetrics()
fillRect(frame.buffer, scrollbarPixelX, upButtonY, scrollbarPixelWidth, scrollbarButtonHeight * 3, upColor)
fillRect(frame.buffer, scrollbarPixelX, downButtonY, scrollbarPixelWidth, scrollbarButtonHeight * 3, downColor)
if scrollbarTrackPixelHeight > 0 then
fillRect(frame.buffer, scrollbarPixelX, scrollbarTrackPixelY, scrollbarPixelWidth, scrollbarTrackPixelHeight, colors.gray)
fillRect(frame.buffer, scrollbarPixelX, thumbY, scrollbarPixelWidth, thumbHeight, colors.white)
end
drawNfpScaled(frame.buffer, getScrollUpButton(), scrollbarPixelX, upButtonY + 2, 1, 1)
drawNfpScaled(frame.buffer, getScrollDownButton(), scrollbarPixelX, downButtonY + 3, 1, 1)
assertBufferValid(frame)
frame:drawBuffer()
end
refreshGridData()
renderGrid()
refreshTimer = os.startTimer(options.refreshSeconds or 30)
while true do
local event, p1, x, y = os.pullEvent()
if event == "timer" and p1 == refreshTimer then
refreshGridData()
renderGrid()
refreshTimer = os.startTimer(options.refreshSeconds or 30)
scheduleAnimation()
elseif event == "timer" and p1 == animationTimer then
local delta = targetScrollRow - currentScrollRow
if math.abs(delta) <= 0.001 then
currentScrollRow = targetScrollRow
animationTimer = nil
else
local step = delta * 0.35
if step > 0 then
step = math.max(0.08, math.min(step, 0.45))
else
step = math.min(-0.08, math.max(step, -0.45))
end
if math.abs(step) >= math.abs(delta) then
currentScrollRow = targetScrollRow
animationTimer = nil
else
currentScrollRow = currentScrollRow + step
animationTimer = os.startTimer(0.05)
end
end
persistScrollState()
renderGrid()
elseif event == "monitor_touch" and p1 == monName then
if options.handleChromeTouch and options.handleChromeTouch(x, y) then
return
elseif x >= scrollbarX and x < scrollbarX + scrollbarWidth then
if y <= scrollbarButtonHeight then
scrollTo(targetScrollRow - 1)
elseif y > SCREEN_HEIGHT - scrollbarButtonHeight then
scrollTo(targetScrollRow + 1)
elseif scrollbarTrackHeight > 0 then
local touchPixelY = cellToPixelY(y) + 1
local thumbY, thumbHeight = getThumbMetrics()
if (touchPixelY < thumbY or touchPixelY > thumbY + thumbHeight - 1) and maxScrollRow > 0 then
local thumbTravel = scrollbarTrackPixelHeight - thumbHeight
local targetPixelY = math.max(
scrollbarTrackPixelY,
math.min(scrollbarTrackPixelY + thumbTravel, touchPixelY - math.floor(thumbHeight / 2))
)
local progress = (targetPixelY - scrollbarTrackPixelY) / math.max(1, thumbTravel)
scrollTo(progress * maxScrollRow)
end
end
else
local col = math.floor((x - baseCellX) / colStepCells)
local firstVisibleRow = math.floor(currentScrollRow)
local rowOffsetPixels = (currentScrollRow - firstVisibleRow) * rowStepPixels
local touchPixelY = cellToPixelY(y) + 1
local row = math.floor((touchPixelY - cellToPixelY(baseCellY) + rowOffsetPixels) / rowStepPixels)
if col >= 0 and col < columns and row >= 0 and row < rowsPerView + 1 then
local index = (firstVisibleRow + row) * columns + col + 1
if index >= 1 and index <= #entries and index <= firstVisibleRow * columns + visibleItemCount + columns then
if options.onSelect(entries[index], index) then
return
end
end
end
end
end
end
end
local function drawPage(base_id) local function drawPage(base_id)
local pageItems = getPageItems(base_id) local pageItems = getPageItems(base_id)
local defaultIcon = getFallbackIcon() local defaultIcon = getFallbackIcon()
local backIcon = getBackButton() local backIcon = getBackButton()
local backWidth, backHeight = imageSize(backIcon) local backWidth, backHeight = imageSize(backIcon)
local backX = SCREEN_WIDTH - backWidth + 1 local backX = 1
local pageItemIds = {} local pageItemIds = {}
local visibleItemCount = math.min(math.max(#pageItems, 9), 9)
for i = 1, visibleItemCount do for i = 1, #pageItems do
pageItemIds[i] = pageItems[i].id pageItemIds[i] = pageItems[i].id
end end
local function renderPage() runScrollableGrid({
local itemCounts = getMeItemCounts(pageItemIds) refreshSeconds = 5,
getEntriesAndCounts = function()
frame.buffer:clear() return pageItems, getMeItemCounts(pageItemIds)
drawNfpScaled(frame.buffer, backIcon, (backX - 1) * 2 + 1, 1) end,
getId = function(entry)
for i = 1, visibleItemCount do return entry.id
local item = pageItems[i] end,
local icon = getItemIcon(item) or defaultIcon getIcon = function(entry)
drawItem(icon, 4+(8+4)*((i-1)%3), 2+(5+1)*math.floor((i-1)/3), itemCounts[item.id] or 0) return getItemIcon(entry) or defaultIcon
end end,
drawChrome = function()
assertBufferValid(frame) drawNfpScaled(frame.buffer, backIcon, cellToPixelX(backX), 1)
frame:drawBuffer() end,
end handleChromeTouch = function(x, y)
if x >= backX and x < backX + backWidth and y >= 1 and y <= backHeight then
renderPage()
local refreshTimer = os.startTimer(5)
while true do
local event, p1, x, y = os.pullEvent()
if event == "timer" and p1 == refreshTimer then
renderPage()
refreshTimer = os.startTimer(5)
elseif event == "monitor_touch" and p1 == monName and x >= backX and x < backX + backWidth and y >= 1 and y <= backHeight then
drawOverview() drawOverview()
return return true
end
end end
return false
end,
onSelect = function()
return false
end,
})
end end
drawOverview = function() drawOverview = function()
local items = getBaseItemIds() local items = getBaseItemIds()
local visibleItemCount = math.min(math.max(#items, 9), 9) local defaultIcon = getFallbackIcon()
-- TODO LATER: get item counts from me-system using item, then sort items according to their counts and display the top 9. For now, just use random numbers.
frame.buffer:clear() runScrollableGrid({
refreshSeconds = 30,
getInitialScrollState = function()
return overviewScrollCurrentRow, overviewScrollTargetRow
end,
setScrollState = function(currentRow, targetRow)
overviewScrollCurrentRow = currentRow
overviewScrollTargetRow = targetRow
end,
getEntriesAndCounts = function()
local itemCounts = getMeItemCounts(items)
local sortedItemIds = {}
for i = 1, visibleItemCount do for i = 1, #items do
local item = getItemById(items[i]) sortedItemIds[i] = items[i]
local icon = getItemIcon(item) or getFallbackIcon()
drawItem(icon, 4+(8+4)*((i-1)%3), 2+(5+1)*math.floor((i-1)/3), math.random(0, 1000000000000))
end end
assertBufferValid(frame) table.sort(sortedItemIds, function(a, b)
frame:drawBuffer() local countA = itemCounts[a] or 0
local countB = itemCounts[b] or 0
while true do if countA ~= countB then
local _, side, x, y = os.pullEvent("monitor_touch") return countA > countB
end
if side == monName then return a < b
local col = math.floor((x - 4) / 12) end)
local row = math.floor((y - 2) / 6)
if col >= 0 and col < 3 and row >= 0 and row < 3 then return sortedItemIds, itemCounts
local index = row * 3 + col + 1 end,
getId = function(entry)
if index <= visibleItemCount then return entry
drawPage(items[index]) end,
return getIcon = function(entry)
end return getItemIcon(getItemById(entry)) or defaultIcon
end end,
end onSelect = function(entry)
end drawPage(entry)
return true
end,
})
end end
drawOverview() drawOverview()
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
[
]