-- -- chamber.lua -- -- Reusable Hostile Networks sim-chamber subsystem logic: peripheral discovery, -- status collection, and the load/return/stock operations. No UI, no comms - so -- both the chamber RTU and (eventually) a standalone panel can drive it. -- -- local chamber = require("chamber").new(cfg) -- local st = chamber.collect() -- summary + per-chamber + barrel totals -- local ok, err = chamber.stock_catalyst(3) -- -- cfg fields used: chamber_type, barrel_side, barrel_type, catalyst_top_up, -- catalyst_low (all optional; sensible defaults below). -- local chamber = {} local MODEL_PATTERN = "data_model" local CATALYST_PATTERN = "prediction_matrix" function chamber.new(cfg) cfg = cfg or {} local CHAMBER_TYPE = cfg.chamber_type or "hostilenetworks:sim_chamber" local BARREL_SIDE = cfg.barrel_side or "back" local BARREL_TYPE = cfg.barrel_type or "minecraft:barrel" local CATALYST_TOP_UP = cfg.catalyst_top_up or 8 local CATALYST_LOW = cfg.catalyst_low or 8 -- === discovery === local function findByType(ptype) local out = {} for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == ptype then out[#out + 1] = name end end -- natural sort on the trailing "_N" so ids match in-world order table.sort(out, function (a, b) local na, nb = tonumber(a:match("_(%d+)$")), tonumber(b:match("_(%d+)$")) if na and nb then return na < nb end return a < b end) return out end local chambers = findByType(CHAMBER_TYPE) if #chambers == 0 then error("chamber: no '" .. CHAMBER_TYPE .. "' peripherals found on the network.", 0) end local BARREL if peripheral.isPresent(BARREL_SIDE) and peripheral.getType(BARREL_SIDE) == BARREL_TYPE then BARREL = BARREL_SIDE else local barrels = findByType(BARREL_TYPE) if #barrels == 0 then error("chamber: no barrel on '" .. BARREL_SIDE .. "' and none found on the network.", 0) end BARREL = barrels[1] end local N = #chambers local chamberP = {} for i = 1, N do chamberP[i] = peripheral.wrap(chambers[i]) end local barrelP = peripheral.wrap(BARREL) local energyCapacity = {} -- cached per chamber; doesn't change between ticks -- === helpers === -- Scan an inventory once, returning the model slot and total catalyst count. local function scanInventory(p) if not p then return nil, 0 end local ok, list = pcall(p.list) if not ok or not list then return nil, 0 end local modelSlot, catalystCount = nil, 0 for slot, item in pairs(list) do if item.name:find(MODEL_PATTERN) then modelSlot = slot elseif item.name:find(CATALYST_PATTERN) then catalystCount = catalystCount + item.count end end return modelSlot, catalystCount end local function getChamberStatus(i) local p = chamberP[i] local modelSlot, catalystCount = scanInventory(p) local energy = 0 pcall(function () energy = p.getEnergy() end) if energyCapacity[i] == nil then local capacity = 0 pcall(function () capacity = p.getEnergyCapacity() end) energyCapacity[i] = capacity end return { hasModel = modelSlot ~= nil, catalyst = catalystCount, energy = energy, capacity = energyCapacity[i], } end local function stateLabel(s) if not s.hasModel then return "NO MODEL" end if s.catalyst == 0 then return "NO CATALYST" end return "READY" end -- All data models share one item id (the mob is in NBT), so getItemDetail is -- what exposes the real name; group barrel models by that display name. local function scanBarrelModels() local out, byName = {}, {} local ok, list = pcall(barrelP.list) if ok and list then for slot, item in pairs(list) do if item.name:find(MODEL_PATTERN) then local ok2, det = pcall(barrelP.getItemDetail, slot) local disp = (ok2 and det and det.displayName) or "Data Model" local e = byName[disp] if not e then e = { disp = disp, count = 0 }; byName[disp] = e; out[#out + 1] = e end e.count = e.count + item.count end end end table.sort(out, function (a, b) return a.disp < b.disp end) return out end -- === operations (all return ok:boolean, err_or_nil) === -- Load whichever model the barrel lists first (used by load_all_empty). local function loadModelFromBarrel(i) local c = chamberP[i] local curSlot = scanInventory(c) if curSlot then c.pushItems(BARREL, curSlot) end local barrelSlot = scanInventory(barrelP) if not barrelSlot then return false, "no models" end local moved = barrelP.pushItems(chambers[i], barrelSlot, 1) if not moved or moved == 0 then return false, "push failed" end return true end -- Load a chosen model (by its display name) into chamber i. local function loadModelByDisplay(i, disp) local c = chamberP[i] local curSlot = scanInventory(c) if curSlot then c.pushItems(BARREL, curSlot) end local barrelSlot local ok, list = pcall(barrelP.list) if ok and list then for slot, item in pairs(list) do if item.name:find(MODEL_PATTERN) then local ok2, det = pcall(barrelP.getItemDetail, slot) if ok2 and det and det.displayName == disp then barrelSlot = slot; break end end end end if not barrelSlot then return false, "none left" end local moved = barrelP.pushItems(chambers[i], barrelSlot, 1) if not moved or moved == 0 then return false, "push failed" end return true end local function returnModelToBarrel(i) local c = chamberP[i] local curSlot = scanInventory(c) if not curSlot then return false, "no model loaded" end local moved = c.pushItems(BARREL, curSlot) if not moved or moved == 0 then return false, "push failed" end return true end -- Won't feed a matrix into a model-less chamber (it would be rejected). local function stockCatalyst(i) local modelSlot = scanInventory(chamberP[i]) if not modelSlot then return false, "no model" end local barrelSlot local ok, list = pcall(barrelP.list) if ok and list then for slot, item in pairs(list) do if item.name:find(CATALYST_PATTERN) then barrelSlot = slot; break end end end if not barrelSlot then return false, "no catalyst" end local moved = barrelP.pushItems(chambers[i], barrelSlot, CATALYST_TOP_UP) if not moved or moved == 0 then return false, "push failed" end return true end local function loadAllEmpty() local n = 0 for i = 1, N do if not getChamberStatus(i).hasModel then local ok = loadModelFromBarrel(i) if ok then n = n + 1 else break end -- stop once the barrel runs dry end end return true, { loaded = n } end local function stockAllLow() local n = 0 for i = 1, N do local s = getChamberStatus(i) if s.hasModel and s.catalyst < CATALYST_LOW then local ok, err = stockCatalyst(i) if ok then n = n + 1 elseif err == "no catalyst" then break end end end return true, { stocked = n } end -- Full snapshot: per-chamber rows + rolled-up summary + barrel totals. local function collect() local arr = {} local ready, low, no_model, no_catalyst = 0, 0, 0, 0 for i = 1, N do local s = getChamberStatus(i) if not s.hasModel then no_model = no_model + 1 elseif s.catalyst == 0 then no_catalyst = no_catalyst + 1 else ready = ready + 1 end if s.hasModel and s.catalyst < CATALYST_LOW then low = low + 1 end local pct = s.capacity > 0 and math.floor((s.energy / s.capacity) * 100) or 0 arr[i] = { id = i, state = stateLabel(s), catalyst = s.catalyst, energy = pct } end local models, catalyst = 0, 0 local ok, list = pcall(barrelP.list) if ok and list then for _, item in pairs(list) do if item.name:find(MODEL_PATTERN) then models = models + item.count elseif item.name:find(CATALYST_PATTERN) then catalyst = catalyst + item.count end end end return { -- short line for the coordinator's overview (every subsystem sets one) headline = string.format("%d/%d ready, %d low", ready, N, low), summary = { total = N, ready = ready, low = low, no_model = no_model, no_catalyst = no_catalyst }, barrel = { models = models, catalyst = catalyst }, chambers = arr, -- grouped spare-model list so the coordinator's picker can read it -- straight from status without a separate request/reply round-trip models = scanBarrelModels(), } end -- === public surface === local self = { count = N, catalyst_low = CATALYST_LOW, status = getChamberStatus, collect = collect, list_models = scanBarrelModels, load_model = loadModelByDisplay, return_model = returnModelToBarrel, stock_catalyst = stockCatalyst, load_all_empty = loadAllEmpty, stock_all_low = stockAllLow, } return self end return chamber