510 lines
15 KiB
Lua
510 lines
15 KiB
Lua
-- ATM10 Create factory logistics test controller
|
|
-- Phase 1: Stock Ticker requests and live stock display, no redstone control.
|
|
|
|
local REFRESH_SECONDS = 1.0
|
|
local CONFIRM_SECONDS = 6.0
|
|
local JOB_TIMEOUT_SECONDS = 180.0
|
|
local LOG_FILE = "factory_test_log.txt"
|
|
|
|
local actions = {
|
|
{
|
|
id = "crush_asurine",
|
|
label = "64 ASURINE CRUSHEN",
|
|
lockKey = "crushing",
|
|
address = "crushing",
|
|
item = "create:asurine",
|
|
inputCount = 64,
|
|
completion = {
|
|
mode = "any_increase",
|
|
outputs = { "alltheores:zinc_clump", "alltheores:zinc_nugget" },
|
|
},
|
|
},
|
|
{
|
|
id = "strip_oak",
|
|
label = "64 STRIPPED OAK",
|
|
lockKey = "saw:2",
|
|
address = "saw:2",
|
|
item = "minecraft:oak_log",
|
|
inputCount = 64,
|
|
completion = {
|
|
mode = "expected",
|
|
output = "minecraft:stripped_oak_log",
|
|
outputPerInput = 1,
|
|
},
|
|
},
|
|
{
|
|
id = "planks_oak",
|
|
label = "64 OAK PLANKS",
|
|
lockKey = "saw:2",
|
|
address = "saw:2",
|
|
item = "minecraft:stripped_oak_log",
|
|
-- Create yields 6 planks per stripped log. 11 inputs produce 66 planks.
|
|
inputCount = 11,
|
|
completion = {
|
|
mode = "expected",
|
|
output = "minecraft:oak_planks",
|
|
outputPerInput = 6,
|
|
},
|
|
},
|
|
{
|
|
id = "shafts",
|
|
label = "64 SHAFTS",
|
|
lockKey = "saw:1",
|
|
address = "saw:1",
|
|
item = "create:andesite_alloy",
|
|
-- Create yields 6 shafts per Andesite Alloy. 11 inputs produce 66 shafts.
|
|
inputCount = 11,
|
|
completion = {
|
|
mode = "expected",
|
|
output = "create:shaft",
|
|
outputPerInput = 6,
|
|
},
|
|
},
|
|
{
|
|
id = "wash_zinc_clumps",
|
|
label = "576 ZINC NUGGETS",
|
|
lockKey = "fan:washing",
|
|
address = "fan:washing",
|
|
item = "alltheores:zinc_clump",
|
|
-- Washing yields 9 nuggets per clump and has a 25% Gunpowder byproduct.
|
|
inputCount = 64,
|
|
completion = {
|
|
mode = "expected",
|
|
output = "alltheores:zinc_nugget",
|
|
outputPerInput = 9,
|
|
},
|
|
},
|
|
}
|
|
|
|
local function findPeripheral(wantedType)
|
|
for _, name in ipairs(peripheral.getNames()) do
|
|
local peripheralType = peripheral.getType(name)
|
|
if peripheralType == wantedType then
|
|
return name
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local tickerName = findPeripheral("Create_StockTicker")
|
|
local monitorName = findPeripheral("monitor")
|
|
|
|
if not tickerName then
|
|
error("Kein Create Stock Ticker am Wired-Modem-Netz gefunden", 0)
|
|
end
|
|
if not monitorName then
|
|
error("Kein Monitor am Wired-Modem-Netz gefunden", 0)
|
|
end
|
|
|
|
local monitor = peripheral.wrap(monitorName)
|
|
monitor.setTextScale(0.5)
|
|
|
|
local stock = {}
|
|
local stockEntries = {}
|
|
local stockPage = 1
|
|
local stockPageCount = 1
|
|
local stockPageButtons = {}
|
|
local buttons = {}
|
|
local messages = {}
|
|
local jobs = {}
|
|
local armedAction = nil
|
|
local armedUntil = 0
|
|
local statusText = "Bereit - nur Testbetrieb, keine Redstone-Steuerung"
|
|
local statusColor = colors.lime
|
|
|
|
local function formatNumber(value)
|
|
local text = tostring(math.floor(tonumber(value) or 0))
|
|
local sign, digits = text:match("^([%-]?)(%d+)$")
|
|
if not digits then
|
|
return text
|
|
end
|
|
local formatted = digits:reverse():gsub("(%d%d%d)", "%1."):reverse():gsub("^%.", "")
|
|
return sign .. formatted
|
|
end
|
|
|
|
local function timeText()
|
|
return textutils.formatTime(os.time(), true)
|
|
end
|
|
|
|
local function prettyItemName(itemId)
|
|
local path = tostring(itemId):match("^[^:]+:(.+)$") or tostring(itemId)
|
|
path = path:gsub("_", " ")
|
|
return (path:gsub("(%a)([%w']*)", function(first, rest)
|
|
return first:upper() .. rest:lower()
|
|
end))
|
|
end
|
|
|
|
local function appendLog(message)
|
|
local line = "[" .. timeText() .. "] " .. message
|
|
messages[#messages + 1] = line
|
|
while #messages > 4 do
|
|
table.remove(messages, 1)
|
|
end
|
|
|
|
local file = fs.open(LOG_FILE, "a")
|
|
if file then
|
|
file.writeLine(line)
|
|
file.close()
|
|
end
|
|
end
|
|
|
|
local function refreshStock()
|
|
local ok, result = pcall(peripheral.call, tickerName, "stock", true)
|
|
if not ok then
|
|
statusText = "Stock-Ticker-Fehler: " .. tostring(result)
|
|
statusColor = colors.red
|
|
return false
|
|
end
|
|
|
|
local nextStock = {}
|
|
local detailsByName = {}
|
|
for _, entry in pairs(result or {}) do
|
|
if type(entry) == "table" and entry.name then
|
|
nextStock[entry.name] = (nextStock[entry.name] or 0) + (tonumber(entry.count) or 0)
|
|
detailsByName[entry.name] = entry
|
|
end
|
|
end
|
|
stock = nextStock
|
|
|
|
local nextEntries = {}
|
|
for itemId, count in pairs(nextStock) do
|
|
local details = detailsByName[itemId] or {}
|
|
local displayName = details.displayName
|
|
if type(displayName) ~= "string" or displayName == "" then
|
|
displayName = prettyItemName(itemId)
|
|
end
|
|
nextEntries[#nextEntries + 1] = {
|
|
id = itemId,
|
|
label = displayName,
|
|
count = count,
|
|
}
|
|
end
|
|
table.sort(nextEntries, function(a, b)
|
|
local aLabel = a.label:lower()
|
|
local bLabel = b.label:lower()
|
|
if aLabel == bLabel then
|
|
return a.id < b.id
|
|
end
|
|
return aLabel < bLabel
|
|
end)
|
|
stockEntries = nextEntries
|
|
return true
|
|
end
|
|
|
|
local function jobComplete(job)
|
|
if job.completion.mode == "expected" then
|
|
local current = stock[job.completion.output] or 0
|
|
return current >= job.baseline[job.completion.output] + job.expectedOutput
|
|
end
|
|
|
|
if job.completion.mode == "any_increase" then
|
|
for _, output in ipairs(job.completion.outputs) do
|
|
if (stock[output] or 0) > (job.baseline[output] or 0) then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
local function updateJobs()
|
|
for lockKey, job in pairs(jobs) do
|
|
if jobComplete(job) then
|
|
statusText = "FERTIG: " .. job.label
|
|
statusColor = colors.lime
|
|
appendLog(statusText)
|
|
jobs[lockKey] = nil
|
|
elseif os.clock() - job.startedAt > JOB_TIMEOUT_SECONDS and not job.timedOut then
|
|
job.timedOut = true
|
|
statusText = "TIMEOUT: " .. job.label .. " - Anlage pruefen"
|
|
statusColor = colors.red
|
|
appendLog(statusText)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function writeAt(x, y, text, foreground, background)
|
|
local width, height = monitor.getSize()
|
|
if y < 1 or y > height or x > width then
|
|
return
|
|
end
|
|
monitor.setCursorPos(math.max(1, x), y)
|
|
monitor.setTextColor(foreground or colors.white)
|
|
monitor.setBackgroundColor(background or colors.black)
|
|
monitor.write(tostring(text):sub(1, math.max(0, width - x + 1)))
|
|
end
|
|
|
|
local function center(y, text, foreground, background)
|
|
local width = monitor.getSize()
|
|
local x = math.max(1, math.floor((width - #text) / 2) + 1)
|
|
writeAt(x, y, text, foreground, background)
|
|
end
|
|
|
|
local function drawStockPage(width)
|
|
local itemsPerPage = 10
|
|
stockPageCount = math.max(1, math.ceil(#stockEntries / itemsPerPage))
|
|
stockPage = math.max(1, math.min(stockPage, stockPageCount))
|
|
stockPageButtons = {}
|
|
|
|
writeAt(3, 3, "<", stockPage > 1 and colors.yellow or colors.gray)
|
|
center(3, "LAGERBESTAND " .. tostring(stockPage) .. "/" .. tostring(stockPageCount), colors.cyan)
|
|
writeAt(width - 2, 3, ">", stockPage < stockPageCount and colors.yellow or colors.gray)
|
|
|
|
stockPageButtons[#stockPageButtons + 1] = {
|
|
direction = -1,
|
|
x1 = 1,
|
|
x2 = math.floor(width / 2),
|
|
y1 = 3,
|
|
y2 = 8,
|
|
enabled = stockPage > 1,
|
|
}
|
|
stockPageButtons[#stockPageButtons + 1] = {
|
|
direction = 1,
|
|
x1 = math.floor(width / 2) + 1,
|
|
x2 = width,
|
|
y1 = 3,
|
|
y2 = 8,
|
|
enabled = stockPage < stockPageCount,
|
|
}
|
|
|
|
local columnWidth = math.floor((width - 6) / 2)
|
|
local leftX = 3
|
|
local rightX = leftX + columnWidth + 2
|
|
local firstIndex = (stockPage - 1) * itemsPerPage + 1
|
|
|
|
for position = 1, itemsPerPage do
|
|
local entry = stockEntries[firstIndex + position - 1]
|
|
if entry then
|
|
local column = (position - 1) % 2
|
|
local row = 4 + math.floor((position - 1) / 2)
|
|
local x = column == 0 and leftX or rightX
|
|
local maxLabelLength = math.max(8, columnWidth - 14)
|
|
local label = entry.label
|
|
if #label > maxLabelLength then
|
|
label = label:sub(1, maxLabelLength - 1) .. "~"
|
|
end
|
|
writeAt(x, row, label .. ": " .. formatNumber(entry.count), colors.lime)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function drawButton(action, x1, y1, x2, y2)
|
|
local isArmed = armedAction == action.id and os.clock() <= armedUntil
|
|
local available = stock[action.item] or 0
|
|
local activeJob = jobs[action.lockKey]
|
|
local enabled = available >= action.inputCount and activeJob == nil
|
|
local background = colors.gray
|
|
local foreground = colors.lightGray
|
|
|
|
if activeJob then
|
|
background = activeJob.timedOut and colors.red or colors.orange
|
|
foreground = activeJob.timedOut and colors.white or colors.black
|
|
elseif enabled then
|
|
background = isArmed and colors.orange or colors.green
|
|
foreground = isArmed and colors.black or colors.white
|
|
end
|
|
|
|
for y = y1, y2 do
|
|
writeAt(x1, y, string.rep(" ", x2 - x1 + 1), foreground, background)
|
|
end
|
|
|
|
local label = action.label
|
|
if activeJob then
|
|
label = (activeJob.timedOut and "TIMEOUT: " or "IN ARBEIT: ") .. activeJob.label
|
|
elseif isArmed then
|
|
label = "NOCHMALS: " .. label
|
|
elseif not enabled then
|
|
label = "ZU WENIG INPUT: " .. label
|
|
end
|
|
|
|
local labelX = math.max(x1, math.floor((x1 + x2 - #label) / 2))
|
|
writeAt(labelX, math.floor((y1 + y2) / 2), label, foreground, background)
|
|
|
|
buttons[#buttons + 1] = {
|
|
action = action,
|
|
x1 = x1,
|
|
y1 = y1,
|
|
x2 = x2,
|
|
y2 = y2,
|
|
enabled = enabled,
|
|
activeJob = activeJob,
|
|
}
|
|
end
|
|
|
|
local function render()
|
|
local width, height = monitor.getSize()
|
|
monitor.setBackgroundColor(colors.black)
|
|
monitor.setTextColor(colors.white)
|
|
monitor.clear()
|
|
buttons = {}
|
|
|
|
center(1, "CREATE FACTORY - LOGISTIKTEST", colors.cyan)
|
|
center(2, "Ticker: " .. tickerName .. " | Monitor: " .. monitorName, colors.gray)
|
|
|
|
drawStockPage(width)
|
|
|
|
local statusRow = 10
|
|
center(statusRow, statusText, statusColor)
|
|
|
|
local buttonTop = statusRow + 2
|
|
local gap = 2
|
|
local buttonWidth = math.floor((width - 6 - gap) / 2)
|
|
local xLeft = 3
|
|
local xRight = xLeft + buttonWidth + gap
|
|
local buttonHeight = 2
|
|
|
|
for index, action in ipairs(actions) do
|
|
local column = (index - 1) % 2
|
|
local actionRow = math.floor((index - 1) / 2)
|
|
local x = column == 0 and xLeft or xRight
|
|
local y = buttonTop + actionRow * 3
|
|
drawButton(action, x, y, x + buttonWidth - 1, y + buttonHeight - 1)
|
|
end
|
|
|
|
local actionRows = math.ceil(#actions / 2)
|
|
local logTitleRow = math.min(height - 4, buttonTop + actionRows * 3)
|
|
writeAt(2, logTitleRow, "Letzte Ereignisse:", colors.yellow)
|
|
for index, message in ipairs(messages) do
|
|
writeAt(2, logTitleRow + index, message, colors.lightGray)
|
|
end
|
|
|
|
writeAt(2, height, "Jede Bestellung muss innerhalb 6 Sekunden zweimal beruehrt werden.", colors.gray)
|
|
end
|
|
|
|
local function submit(action)
|
|
local filter = {
|
|
name = action.item,
|
|
_requestCount = action.inputCount,
|
|
}
|
|
|
|
local baseline = {}
|
|
if action.completion.mode == "expected" then
|
|
baseline[action.completion.output] = stock[action.completion.output] or 0
|
|
else
|
|
for _, output in ipairs(action.completion.outputs) do
|
|
baseline[output] = stock[output] or 0
|
|
end
|
|
end
|
|
|
|
local ok, sent = pcall(
|
|
peripheral.call,
|
|
tickerName,
|
|
"requestFiltered",
|
|
action.address,
|
|
filter
|
|
)
|
|
|
|
if not ok then
|
|
statusText = "BESTELLFEHLER: " .. tostring(sent)
|
|
statusColor = colors.red
|
|
appendLog(statusText)
|
|
return
|
|
end
|
|
|
|
local amount = tonumber(sent) or 0
|
|
if amount <= 0 then
|
|
statusText = "NICHT GESENDET: " .. action.label
|
|
statusColor = colors.orange
|
|
appendLog(statusText)
|
|
refreshStock()
|
|
return
|
|
end
|
|
|
|
local expectedOutput = 0
|
|
if action.completion.mode == "expected" then
|
|
expectedOutput = amount * action.completion.outputPerInput
|
|
end
|
|
|
|
jobs[action.lockKey] = {
|
|
label = action.label,
|
|
actionId = action.id,
|
|
address = action.address,
|
|
inputItem = action.item,
|
|
inputSent = amount,
|
|
completion = action.completion,
|
|
baseline = baseline,
|
|
expectedOutput = expectedOutput,
|
|
startedAt = os.clock(),
|
|
timedOut = false,
|
|
}
|
|
|
|
statusText = "GESTARTET: " .. action.label .. " (" .. formatNumber(amount) .. " Input)"
|
|
statusColor = colors.orange
|
|
appendLog(statusText)
|
|
refreshStock()
|
|
end
|
|
|
|
local function handleTouch(x, y)
|
|
for _, pageButton in ipairs(stockPageButtons) do
|
|
if y >= pageButton.y1 and y <= pageButton.y2
|
|
and x >= pageButton.x1 and x <= pageButton.x2 then
|
|
if pageButton.enabled then
|
|
stockPage = stockPage + pageButton.direction
|
|
statusText = "Lagerseite " .. tostring(stockPage) .. "/" .. tostring(stockPageCount)
|
|
statusColor = colors.cyan
|
|
end
|
|
return
|
|
end
|
|
end
|
|
|
|
for _, button in ipairs(buttons) do
|
|
if x >= button.x1 and x <= button.x2 and y >= button.y1 and y <= button.y2 then
|
|
if button.activeJob then
|
|
statusText = "Auftrag laeuft bereits: " .. button.activeJob.label
|
|
statusColor = button.activeJob.timedOut and colors.red or colors.orange
|
|
return
|
|
end
|
|
if not button.enabled then
|
|
statusText = "Zu wenig Rohstoff fuer diesen Auftrag"
|
|
statusColor = colors.orange
|
|
appendLog(statusText)
|
|
return
|
|
end
|
|
|
|
local now = os.clock()
|
|
if armedAction == button.action.id and now <= armedUntil then
|
|
local selected = button.action
|
|
armedAction = nil
|
|
armedUntil = 0
|
|
submit(selected)
|
|
else
|
|
armedAction = button.action.id
|
|
armedUntil = now + CONFIRM_SECONDS
|
|
statusText = "Zur Bestaetigung denselben Knopf erneut beruehren"
|
|
statusColor = colors.orange
|
|
end
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
appendLog("Testcontroller gestartet auf Computer " .. tostring(os.getComputerID()))
|
|
refreshStock()
|
|
render()
|
|
|
|
local refreshTimer = os.startTimer(REFRESH_SECONDS)
|
|
|
|
while true do
|
|
local event, parameter1, parameter2, parameter3 = os.pullEvent()
|
|
|
|
if event == "monitor_touch" and parameter1 == monitorName then
|
|
handleTouch(parameter2, parameter3)
|
|
render()
|
|
elseif event == "timer" and parameter1 == refreshTimer then
|
|
if armedAction and os.clock() > armedUntil then
|
|
armedAction = nil
|
|
armedUntil = 0
|
|
statusText = "Bestaetigung abgelaufen"
|
|
statusColor = colors.gray
|
|
end
|
|
refreshStock()
|
|
updateJobs()
|
|
render()
|
|
refreshTimer = os.startTimer(REFRESH_SECONDS)
|
|
elseif event == "peripheral_detach" or event == "peripheral" then
|
|
refreshStock()
|
|
render()
|
|
end
|
|
end
|