-- -- colony_gui.lua -- -- Standalone MineColonies dashboard on an Advanced Peripherals Colony Integrator. -- One computer, one script: reads the colony every couple seconds and draws a -- live mimic panel (population, happiness, alerts, and the pending request list - -- the actionable part of running a colony). -- -- colony_gui -- dashboard on this terminal -- colony_gui monitor_0 -- dashboard on a monitor (best on a 2x2+ wall) -- -- Needs mimic installed. Attach a Colony Integrator to this computer (any side or -- via a wired modem) - it's auto-detected, no config. Read-only: the integrator -- can't fulfil requests, so this monitors, it doesn't command. -- -- Runs the poll in a parallel coroutine and only ps.publish()es scalars/strings -- into the UI, so a slow colony read never freezes the screen. -- require("/initenv").init_env() local mimic = require("mimic") local m = require("mimic.elements") local psil = require("scada-common.psil") local monitor = ... local style = mimic.style local cpair = mimic.cpair local ALIGN = mimic.ALIGN local POLL_INTERVAL = 3 -- seconds between colony reads -- -- find the Colony Integrator. Capability-probe (look for its methods) rather than -- trust a type string - AP peripheral type names have bitten this base before. -- local function findColony() for _, n in ipairs(peripheral.getNames()) do local ok, methods = pcall(peripheral.getMethods, n) if ok and type(methods) == "table" then local has = {} for _, meth in ipairs(methods) do has[meth] = true end if has.isInColony or has.getColonyID then return peripheral.wrap(n), n end end end return nil end local colony, colonyName = findColony() if not colony then error("colony_gui: no Colony Integrator found. Attach one to this computer.", 0) end -- -- read helpers: every integrator call is guarded (a chunk-unloaded colony or a -- detached peripheral must not take the panel down). -- local function callOr(fn, default) if type(fn) ~= "function" then return default end local ok, v = pcall(fn) if not ok then return default end return v end -- Summarise one open request into a display line. AP request shapes vary between -- versions, so pull fields defensively. local function requestLine(r) if type(r) ~= "table" then return nil end local count = tonumber(r.count) or tonumber(r.minCount) or 1 local item = r.name or r.desc -- prefer the concrete item's display name when the request carries one if type(r.items) == "table" and r.items[1] then local it = r.items[1] item = it.displayName or it.name or item end item = tostring(item or "?"):gsub("^minecraft:", ""):gsub("_", " ") local who = r.target or r.desc who = who and tostring(who):gsub("^.*/", "") or "" return string.format("x%-3d %s", count, item), who end -- Full colony snapshot -> the scalars/strings the UI binds to. local function collect() local in_colony = callOr(colony.isInColony, false) and true or false if not in_colony then return { in_colony = false } end local reqs = callOr(colony.getRequests, {}) if type(reqs) ~= "table" then reqs = {} end local work = callOr(colony.getWorkOrders, {}) if type(work) ~= "table" then work = {} end local builds = callOr(colony.getBuildings, {}) local visitors = callOr(colony.getVisitors, {}) return { in_colony = true, name = tostring(callOr(colony.getColonyName, "Colony")), id = tonumber(callOr(colony.getColonyID, 0)) or 0, happiness = tonumber(callOr(colony.getHappiness, 0)) or 0, active = callOr(colony.isActive, false) and true or false, under_attack = callOr(colony.isUnderAttack, false) and true or false, citizens = tonumber(callOr(colony.amountOfCitizens, 0)) or 0, max_citizens = tonumber(callOr(colony.maxOfCitizens, 0)) or 0, graves = tonumber(callOr(colony.amountOfGraves, 0)) or 0, construction = tonumber(callOr(colony.amountOfConstructionSites, 0)) or 0, buildings = type(builds) == "table" and #builds or 0, visitors = type(visitors) == "table" and #visitors or 0, workorders = #work, requests = reqs, } end -- -- UI -- local ps = psil.create() local root = mimic.init{monitor=monitor, ps=ps} local W, H = root.get_width(), root.get_height() -- header: colony name (fills live from the poll) + clock m.TextBox{parent=root, x=1, y=1, width=W-9, text="COLONY", alignment=ALIGN.CENTER, fg_bg=style.theme.header, bind="name"} m.TextBox{parent=root, x=W-8, y=1, width=8, text="", alignment=ALIGN.RIGHT, fg_bg=style.theme.header, bind="clock"} -- status row: colony reachability + activity/attack state m.StateIndicator{parent=root, x=2, y=3, min_width=12, value=1, states={ { color=cpair(colors.black, colors.green), text="ONLINE" }, { color=cpair(colors.white, colors.red), text="NO COLONY" }, }, bind="reach"} m.StateIndicator{parent=root, x=16, y=3, min_width=14, value=1, states={ { color=cpair(colors.black, colors.green), text="ACTIVE" }, { color=cpair(colors.black, colors.gray), text="QUIET" }, { color=cpair(colors.white, colors.red), text="UNDER ATTACK" }, }, bind="state"} -- happiness (0-10) + population fill bars mimic.Gauge{parent=root, x=2, y=5, width=W-4, label="HAPPY ", bind="happy", max=10, color=cpair(colors.green, style.ind_bkg)} mimic.Gauge{parent=root, x=2, y=6, width=W-4, label="POP ", bind="pop", color=cpair(colors.cyan, style.ind_bkg)} -- two columns of stat readouts local colW = math.floor((W - 3) / 2) local function di(x, y, label, key, unit) m.DataIndicator{parent=root, x=x, y=y, label=label, unit=unit or "", format="%4.0f", value=0, width=colW, fg_bg=style.text_colors, lu_colors=style.lu_colors, bind=key} end di(2, 8, "Citizens:", "citz") di(2, 9, "Max pop:", "maxp") di(2, 10, "Visitors:", "vis") di(2+colW+1, 8, "Buildings:", "blds") di(2+colW+1, 9, "Building:", "cons") di(2+colW+1, 10, "Graves:", "grav") -- alert lights m.StateIndicator{parent=root, x=2, y=12, min_width=W-2, value=1, states={ { color=cpair(colors.black, colors.gray), text="No open work orders" }, { color=cpair(colors.black, colors.orange), text="Work orders in progress" }, }, bind="woled"} -- REQUESTS list: the actionable part. Fixed set of bound lines (poll only publishes -- strings into them - no widget mutation off the UI coroutine), header shows count. local reqY = 14 m.TextBox{parent=root, x=2, y=reqY, width=W-2, text="OPEN REQUESTS", fg_bg=style.theme.header, bind="reqhdr"} local REQ_LINES = math.max(1, H - reqY - 1) for i = 1, REQ_LINES do -- item (left) + who wants it (right) on one row m.TextBox{parent=root, x=2, y=reqY+i, width=W-16, text="", fg_bg=style.text_colors, bind="req"..i} m.TextBox{parent=root, x=W-14, y=reqY+i, width=14, text="", alignment=ALIGN.RIGHT, fg_bg=style.label, bind="reqw"..i} end -- -- loops -- local function pollLoop() while true do local ok, d = pcall(collect) if ok and d then if not d.in_colony then ps.publish("reach", 2) ps.publish("name", "NO COLONY") ps.publish("reqhdr", "OPEN REQUESTS") for i = 1, REQ_LINES do ps.publish("req"..i, ""); ps.publish("reqw"..i, "") end else ps.publish("reach", 1) ps.publish("name", string.format("%s #%d", d.name, d.id)) ps.publish("state", d.under_attack and 3 or (d.active and 1 or 2)) ps.publish("happy", d.happiness) ps.publish("pop", d.max_citizens > 0 and (d.citizens / d.max_citizens) or 0) ps.publish("citz", d.citizens) ps.publish("maxp", d.max_citizens) ps.publish("vis", d.visitors) ps.publish("blds", d.buildings) ps.publish("cons", d.construction) ps.publish("grav", d.graves) ps.publish("woled", d.workorders > 0 and 2 or 1) ps.publish("reqhdr", string.format("OPEN REQUESTS (%d)", #d.requests)) for i = 1, REQ_LINES do local r = d.requests[i] if r then local line, who = requestLine(r) ps.publish("req"..i, line or "") ps.publish("reqw"..i, who or "") else ps.publish("req"..i, "") ps.publish("reqw"..i, "") end end end end os.sleep(POLL_INTERVAL) end end local function clockLoop() while true do ps.publish("clock", os.date("%H:%M:%S")) os.sleep(1) end end parallel.waitForAny(mimic.run, pollLoop, clockLoop)