Tableau de bord d'usine
Un mur de Moniteurs LCD qui montre vos stocks en barres et le stress du réseau cinétique, rafraîchi par une minuterie.
Un grand écran au milieu de l'usine, qui répond aux questions pour lesquelles vous courez sans cesse jusqu'aux coffres : combien reste-t-il de fer, la chaîne de laiton suit-elle, le réseau est-il près de la surcharge ? Cette recette le construit : une barre par ressource, colorée selon son remplissage, et une jauge de stress pour le réseau cinétique, rafraîchies toutes les cinq secondes sur un mur de Moniteurs LCD.
C'est aussi un bon modèle pour tout écran qui tourne toute la journée : une minuterie plutôt qu'une boucle qui s'emballe, des lectures qui survivent à un coffre cassé, et un écran qui ne redessine que ce qui a changé.
Ce qu'il vous faut
- Un Ordinateur moderne, ou un Micro-ordinateur. Les deux ont les couleurs, la bibliothèque
gfxet un port réseau pour le Câble de données. Le programme s'adapte à l'écran : 384 × 216 pixels sur l'Ordinateur moderne, 306 × 171 sur le Micro-ordinateur. Lire un inventaire coûte une instruction par emplacement (voir@inventory.list) : un grand stockage est donc plus à l'aise sur l'Ordinateur moderne (100 000 instructions par seconde à 256 tr/min, contre 24 000). Un Mini-ordinateur le ferait tourner aussi, mais sur son écran ambre toutes les barres ont la même couleur. - Des Moniteurs LCD : un mur jusqu'à 8 × 6, qui touche l'ordinateur. Le mur affiche l'écran de l'ordinateur lui-même, étiré pour le remplir : plus de moniteurs donnent une image plus grande, pas plus fine. Un mur proche du 16:9, comme 7 × 4, garde des lettres bien proportionnées.
- Du Câble de données de l'ordinateur jusqu'à votre stockage (coffres, tonneaux, Coffres à objet renforcé) et jusqu'à un bloc cinétique du réseau de l'usine. Un Stressomètre de Create est idéal. Les simples arbres, roues dentées et boîtes à roues dentées ne sont pas des périphériques du câble (voir
Périphériques) : le câble doit toucher une vraie machine ou une jauge. - De la rotation à l'arrière de l'ordinateur, comme toujours.
Vu de dessus (les écrans regardent vers le bas de la page) :
[L][L][L][L][L][L][L] L Moniteur LCD, un mur de 7 x 4
[M]==== arbre M Ordinateur moderne, contre le dos du mur
# # Câble de données
[C]#######+#######[C] C coffres renforcés ou coffres (fer, cuivre, laiton...)
#
[S] S Stressomètre, sur un arbre de l'usineLe câble ne doit toucher chaque coffre renforcé ou coffre double que par un seul bloc. Chaque bloc d'un Coffre à objet renforcé répond pour le coffre entier : un câble qui longe trois de ses blocs compterait son contenu trois fois.
Comment ça marche
1. Les réglages
La liste des ressources est une table en haut du fichier : un nom pour l'écran, l'identifiant de l'objet à compter, et un objectif, la quantité que vous considérez comme « plein ». La barre est rouge sous le quart de l'objectif, jaune sous la moitié, verte au-dessus.
local STOCK = {
{nom = "FER", objet = "minecraft:iron_ingot", objectif = 2048},
{nom = "CUIVRE", objet = "minecraft:copper_ingot", objectif = 2048},
{nom = "ANDESITE", objet = "create:andesite_alloy", objectif = 1024},
{nom = "LAITON", objet = "create:brass_ingot", objectif = 512},
{nom = "CHARBON", objet = "minecraft:coal", objectif = 1024},
}
local JAUGE = nil -- nil : le premier bloc cinétique du câble, ou "kinetic@x,y,z"
local PERIODE = 5 -- secondes entre deux lecturesLes identifiants d'objets sont ceux des infobulles F3 + H ou de @inventory.list : minecraft:iron_ingot, create:brass_ingot. Les noms sont dessinés avec la police de 3 × 5 pixels de gfx.text, en capitales : gardez-les à huit lettres au plus.
2. La mise en page
Toutes les positions sont calculées à partir de gfx.size : le même programme tient sur un Micro-ordinateur et sur un Ordinateur moderne. Les noms prennent les 80 pixels de gauche, les nombres les 56 de droite, les barres le reste. La barre de stress est en bas, et les lignes se partagent la hauteur qui reste entre le titre et elle.
local taille = gfx.size()
local W, H = taille.w, taille.h
local MARGE = 8
local BARRE_X = 80
local BARRE_L = W - BARRE_X - 56
local BARRE_H = 14
local TEXTE_X = BARRE_X + BARRE_L + 8
local HAUT = 34
local STRESS_Y = H - 44
local LIGNE_H = math.min(26, (STRESS_Y - 10 - HAUT) // #STOCK)3. Lire l'usine
lire_stock parcourt une seule fois chaque inventaire du câble, avec peripheral.list et @inventory.list, et additionne toutes les piles dans une table indexée par identifiant d'objet. Un seul passage donne le compte de chaque ressource, quel que soit leur nombre dans STOCK : bien moins cher qu'un appel à @inventory.count par ressource et par coffre.
local function lire_stock()
local totaux = {}
for _, nom in ipairs(peripheral.list("inventory")) do
local inv = peripheral.wrap(nom)
local r = {ok = false}
if inv then r = pcall(inv.list) end -- le coffre a pu être cassé
if r.ok then
for _, pile in pairs(r.value) do
totaux[pile.name] = (totaux[pile.name] or 0) + pile.count
end
end
end
return totaux
endDeux détails le font survivre à la vie d'une usine. La liste des inventaires est relue à chaque rafraîchissement : un nouveau coffre posé sur le câble est compté aussitôt. Et chaque list() passe par pcall : un coffre cassé entre les deux appels lève une erreur (not an inventory anymore) qui arrêterait le programme, alors qu'ici on saute seulement ce coffre. La liste renvoyée par list() a des trous (seuls les emplacements occupés y sont) : on la parcourt avec pairs, pas ipairs.
Pour le stress, n'importe quel bloc cinétique du réseau donne les mêmes nombres : @kinetic.stress et @kinetic.capacity totalisent tout le réseau dont il fait partie. trouver_jauge prend le premier périphérique cinétique dont le nom contient un @, c'est-à-dire qui est sur le câble : les faces de l'ordinateur arrivent en premier dans peripheral.list, et l'arbre qui alimente l'ordinateur est lui aussi un bloc cinétique, d'un réseau qui n'est peut-être pas celui de l'usine.
local function trouver_jauge()
if JAUGE then return peripheral.wrap(JAUGE) end
for _, nom in ipairs(peripheral.list("kinetic")) do
if string.find(nom, "@") then return peripheral.wrap(nom) end
end
return nil
end
local function lire_stress()
local jauge = trouver_jauge()
if jauge == nil then return nil end
local utilise = pcall(jauge.stress)
local capacite = pcall(jauge.capacity)
local surcharge = pcall(jauge.overstressed)
if not (utilise.ok and capacite.ok and surcharge.ok) then return nil end
return {utilise = utilise.value, capacite = capacite.value, surcharge = surcharge.value}
endUne fonction Brass ne renvoie qu'une valeur : lire_stress renvoie donc une table, ou nil quand il n'y a rien à lire.
4. Ne dessiner que ce qui change
L'écran n'est pas effacé à chaque rafraîchissement. La table affiche retient ce que montrent chaque barre et chaque texte, et dessiner_barre et dessiner_texte ne touchent aux pixels que si la nouvelle valeur est différente. Quand rien n'a bougé dans l'usine, un rafraîchissement ne dessine rien du tout.
local affiche = {}
local function dessiner_texte(cle, x, y, l, texte, couleur, echelle)
local avant = affiche[cle]
if avant and avant.texte == texte and avant.couleur == couleur then return end
gfx.rect(x, y, l, 5 * echelle, "black", true)
gfx.text(x, y, texte, couleur, echelle)
affiche[cle] = {texte = texte, couleur = couleur}
end
local function dessiner_barre(cle, y, part, couleur, texte)
local plein = math.floor((BARRE_L - 2) * math.max(0, math.min(part, 1)))
local avant = affiche[cle] or {}
if avant.plein ~= plein or avant.couleur ~= couleur then
gfx.rect(BARRE_X + 1, y + 1, BARRE_L - 2, BARRE_H - 2, "black", true)
if plein > 0 then
gfx.rect(BARRE_X + 1, y + 1, plein, BARRE_H - 2, couleur, true)
end
affiche[cle] = {plein = plein, couleur = couleur}
end
dessiner_texte(cle .. "_texte", TEXTE_X, y + 2, W - TEXTE_X, texte, "white", 2)
endC'est plus important qu'il n'y paraît. Seules les lignes de pixels dessinées depuis la dernière mise à jour voyagent jusqu'aux joueurs qui regardent le mur : un écran immobile ne coûte aucun trafic réseau. Dessiner coûte des instructions (une pour 64 pixels touchés). Et un programme qui efface tout puis redessine peut être surpris à mi-chemin par un joueur quand son travail s'étale sur plus d'un tick.
Ce qui ne change jamais (le titre, les noms, les cadres des barres) est dessiné une fois par dessiner_fond, avant la boucle. Elle commence par term.clear() : le texte écrit au démarrage (la bannière de Brass OS, l'invite) est sur une couche au-dessus du dessin et resterait par-dessus. gfx.rect sans son dernier argument trace un cadre d'un pixel d'épaisseur, avec true un rectangle plein.
5. La boucle principale
Le programme passe sa vie à attendre dans os.pull_event. Une minuterie lancée par os.start_timer le réveille toutes les PERIODE secondes ; on vérifie son numéro, car d'autres minuteries pourraient exister. Un clic sur l'écran lit l'usine tout de suite.
dessiner_fond()
rafraichir()
local minuterie = os.start_timer(PERIODE)
while true do
local e = os.pull_event()
if e.name == "timer" and e.id == minuterie then
rafraichir()
minuterie = os.start_timer(PERIODE)
elseif e.name == "click" then
rafraichir()
end
endComme le programme attend tous les évènements, clics compris, un clic droit sur le mur appuie sur l'écran au lieu d'ouvrir le terminal. Accroupissez-vous et faites un clic droit pour ouvrir le terminal (voir Écrans et moniteurs).
Le programme complet
Enregistrez-le sous le nom startup : le tableau de bord revient tout seul après un redémarrage de l'ordinateur ou du serveur.
-- Tableau de bord d'usine : barres de stock et stress cinétique sur un mur d'écrans.
-- Ordinateur moderne (ou Micro-ordinateur), Moniteurs LCD contre lui,
-- coffres et coffres renforcés plus un Stressomètre sur son Câble de données.
-- 1. Réglages
local STOCK = {
{nom = "FER", objet = "minecraft:iron_ingot", objectif = 2048},
{nom = "CUIVRE", objet = "minecraft:copper_ingot", objectif = 2048},
{nom = "ANDESITE", objet = "create:andesite_alloy", objectif = 1024},
{nom = "LAITON", objet = "create:brass_ingot", objectif = 512},
{nom = "CHARBON", objet = "minecraft:coal", objectif = 1024},
}
local JAUGE = nil -- nil : le premier bloc cinétique du câble, ou "kinetic@x,y,z"
local PERIODE = 5 -- secondes entre deux lectures
-- 2. Mise en page, calculée d'après la taille de l'écran
local taille = gfx.size()
local W, H = taille.w, taille.h
local MARGE = 8 -- marge
local BARRE_X = 80 -- les barres commencent après les noms
local BARRE_L = W - BARRE_X - 56 -- de la place pour un nombre à droite
local BARRE_H = 14
local TEXTE_X = BARRE_X + BARRE_L + 8
local HAUT = 34 -- première ligne, sous le titre
local STRESS_Y = H - 44 -- la barre de stress, en bas
local LIGNE_H = math.min(26, (STRESS_Y - 10 - HAUT) // #STOCK)
local affiche = {} -- ce qui est à l'écran, pour ne redessiner que ce qui change
-- 3. Lire l'usine
-- Tous les inventaires du câble en un passage : totaux["minecraft:iron_ingot"] = 1234
local function lire_stock()
local totaux = {}
for _, nom in ipairs(peripheral.list("inventory")) do
local inv = peripheral.wrap(nom)
local r = {ok = false}
if inv then r = pcall(inv.list) end -- le coffre a pu être cassé
if r.ok then
for _, pile in pairs(r.value) do
totaux[pile.name] = (totaux[pile.name] or 0) + pile.count
end
end
end
return totaux
end
-- Le bloc cinétique à lire : JAUGE, ou le premier dont le nom a un @
-- (sur le câble). L'arbre derrière l'ordinateur est cinétique lui aussi : on le saute.
local function trouver_jauge()
if JAUGE then return peripheral.wrap(JAUGE) end
for _, nom in ipairs(peripheral.list("kinetic")) do
if string.find(nom, "@") then return peripheral.wrap(nom) end
end
return nil
end
-- {utilise =, capacite =, surcharge =}, ou nil s'il n'y a rien à lire
local function lire_stress()
local jauge = trouver_jauge()
if jauge == nil then return nil end
local utilise = pcall(jauge.stress)
local capacite = pcall(jauge.capacity)
local surcharge = pcall(jauge.overstressed)
if not (utilise.ok and capacite.ok and surcharge.ok) then return nil end
return {utilise = utilise.value, capacite = capacite.value, surcharge = surcharge.value}
end
-- 4. Dessin
local function couleur_niveau(part)
if part < 0.25 then return "red" end
if part < 0.5 then return "yellow" end
return "lime"
end
-- 12345 devient "12.3K" : un nombre tient toujours en cinq caractères
local function court(n)
if n >= 100000 then return (n // 1000) .. "K" end
if n >= 10000 then return string.format("%.1fK", n / 1000) end
return tostring(n)
end
-- Une ligne de texte, effacée et réécrite seulement quand elle change
local function dessiner_texte(cle, x, y, l, texte, couleur, echelle)
local avant = affiche[cle]
if avant and avant.texte == texte and avant.couleur == couleur then return end
gfx.rect(x, y, l, 5 * echelle, "black", true)
gfx.text(x, y, texte, couleur, echelle)
affiche[cle] = {texte = texte, couleur = couleur}
end
-- Une barre : son remplissage n'est redessiné que si sa longueur ou sa couleur change
local function dessiner_barre(cle, y, part, couleur, texte)
local plein = math.floor((BARRE_L - 2) * math.max(0, math.min(part, 1)))
local avant = affiche[cle] or {}
if avant.plein ~= plein or avant.couleur ~= couleur then
gfx.rect(BARRE_X + 1, y + 1, BARRE_L - 2, BARRE_H - 2, "black", true)
if plein > 0 then
gfx.rect(BARRE_X + 1, y + 1, plein, BARRE_H - 2, couleur, true)
end
affiche[cle] = {plein = plein, couleur = couleur}
end
dessiner_texte(cle .. "_texte", TEXTE_X, y + 2, W - TEXTE_X, texte, "white", 2)
end
-- Ce qui ne change jamais : dessiné une fois
local function dessiner_fond()
term.clear() -- les lignes écrites au démarrage resteraient sur le dessin
gfx.clear("black")
gfx.text(MARGE, 6, "USINE", "yellow", 3)
gfx.line(MARGE, 26, W - MARGE, 26, "gray")
for i, s in ipairs(STOCK) do
local y = HAUT + (i - 1) * LIGNE_H
gfx.text(MARGE, y + 2, s.nom, "light_gray", 2)
gfx.rect(BARRE_X, y, BARRE_L, BARRE_H, "gray")
end
gfx.line(MARGE, STRESS_Y - 8, W - MARGE, STRESS_Y - 8, "gray")
gfx.text(MARGE, STRESS_Y + 2, "STRESS", "light_gray", 2)
gfx.rect(BARRE_X, STRESS_Y, BARRE_L, BARRE_H, "gray")
end
-- L'heure du monde, "18:30" (day_time 0 vaut 6 h du matin)
local function texte_heure()
local t = (os.day_time() + 6000) % 24000
return string.format("%02d:%02d", t // 1000, t % 1000 * 60 // 1000)
end
-- 5. Une lecture de l'usine, et l'écran mis à jour
local function rafraichir()
local totaux = lire_stock()
for i, s in ipairs(STOCK) do
local nombre = totaux[s.objet] or 0
local part = nombre / s.objectif
dessiner_barre("ligne" .. i, HAUT + (i - 1) * LIGNE_H, part, couleur_niveau(part), court(nombre))
end
local stress = lire_stress()
local detail, couleur_detail = "PAS DE JAUGE SUR LE CÂBLE", "red"
if stress == nil then
dessiner_barre("stress", STRESS_Y, 0, "gray", "--")
else
local part = 0
if stress.capacite > 0 then part = stress.utilise / stress.capacite end
local couleur = "lime"
if stress.surcharge or part >= 0.9 then
couleur = "red"
elseif part >= 0.75 then
couleur = "yellow"
end
dessiner_barre("stress", STRESS_Y, part, couleur, math.floor(part * 100) .. "%")
detail = court(stress.utilise) .. " SUR " .. court(stress.capacite) .. " SU"
couleur_detail = "light_gray"
if stress.surcharge then detail, couleur_detail = "SURCHARGÉ !", "red" end
end
dessiner_texte("detail", BARRE_X, STRESS_Y + BARRE_H + 6, W - BARRE_X, detail, couleur_detail, 2)
dessiner_texte("heure", W - MARGE - 38, 8, 38, texte_heure(), "white", 2)
end
-- 6. Boucle principale : une minuterie pour les lectures ; une pression sur l'écran lit tout de suite
dessiner_fond()
rafraichir()
local minuterie = os.start_timer(PERIODE)
while true do
local e = os.pull_event()
if e.name == "timer" and e.id == minuterie then
rafraichir()
minuterie = os.start_timer(PERIODE)
elseif e.name == "click" then
rafraichir()
end
endÀ quoi ça ressemble
La documentation n'a pas d'usine à lire : cette démonstration garde tel quel le code de dessin du programme et remplace les deux fonctions de lecture par des nombres fixes. Sur un Ordinateur moderne :
local STOCK = {
{nom = "FER", objet = "minecraft:iron_ingot", objectif = 2048},
{nom = "CUIVRE", objet = "minecraft:copper_ingot", objectif = 2048},
{nom = "ANDESITE", objet = "create:andesite_alloy", objectif = 1024},
{nom = "LAITON", objet = "create:brass_ingot", objectif = 512},
{nom = "CHARBON", objet = "minecraft:coal", objectif = 1024},
}
local taille = gfx.size()
local W, H = taille.w, taille.h
local MARGE, BARRE_X, BARRE_H, HAUT = 8, 80, 14, 34
local BARRE_L = W - BARRE_X - 56
local TEXTE_X = BARRE_X + BARRE_L + 8
local STRESS_Y = H - 44
local LIGNE_H = math.min(26, (STRESS_Y - 10 - HAUT) // #STOCK)
local affiche = {}
-- des lectures d'exemple à la place des périphériques
local function lire_stock()
return {["minecraft:iron_ingot"] = 1730, ["minecraft:copper_ingot"] = 900,
["create:andesite_alloy"] = 12480, ["create:brass_ingot"] = 40, ["minecraft:coal"] = 310}
end
local function lire_stress()
return {utilise = 1536, capacite = 2048, surcharge = false}
end
local function couleur_niveau(part)
if part < 0.25 then return "red" end
if part < 0.5 then return "yellow" end
return "lime"
end
local function court(n)
if n >= 100000 then return (n // 1000) .. "K" end
if n >= 10000 then return string.format("%.1fK", n / 1000) end
return tostring(n)
end
local function dessiner_texte(cle, x, y, l, texte, couleur, echelle)
local avant = affiche[cle]
if avant and avant.texte == texte and avant.couleur == couleur then return end
gfx.rect(x, y, l, 5 * echelle, "black", true)
gfx.text(x, y, texte, couleur, echelle)
affiche[cle] = {texte = texte, couleur = couleur}
end
local function dessiner_barre(cle, y, part, couleur, texte)
local plein = math.floor((BARRE_L - 2) * math.max(0, math.min(part, 1)))
local avant = affiche[cle] or {}
if avant.plein ~= plein or avant.couleur ~= couleur then
gfx.rect(BARRE_X + 1, y + 1, BARRE_L - 2, BARRE_H - 2, "black", true)
if plein > 0 then gfx.rect(BARRE_X + 1, y + 1, plein, BARRE_H - 2, couleur, true) end
affiche[cle] = {plein = plein, couleur = couleur}
end
dessiner_texte(cle .. "_texte", TEXTE_X, y + 2, W - TEXTE_X, texte, "white", 2)
end
local function dessiner_fond()
term.clear()
gfx.clear("black")
gfx.text(MARGE, 6, "USINE", "yellow", 3)
gfx.line(MARGE, 26, W - MARGE, 26, "gray")
for i, s in ipairs(STOCK) do
local y = HAUT + (i - 1) * LIGNE_H
gfx.text(MARGE, y + 2, s.nom, "light_gray", 2)
gfx.rect(BARRE_X, y, BARRE_L, BARRE_H, "gray")
end
gfx.line(MARGE, STRESS_Y - 8, W - MARGE, STRESS_Y - 8, "gray")
gfx.text(MARGE, STRESS_Y + 2, "STRESS", "light_gray", 2)
gfx.rect(BARRE_X, STRESS_Y, BARRE_L, BARRE_H, "gray")
end
local function texte_heure()
local t = (os.day_time() + 6000) % 24000
return string.format("%02d:%02d", t // 1000, t % 1000 * 60 // 1000)
end
local function rafraichir()
local totaux = lire_stock()
for i, s in ipairs(STOCK) do
local nombre = totaux[s.objet] or 0
local part = nombre / s.objectif
dessiner_barre("ligne" .. i, HAUT + (i - 1) * LIGNE_H, part, couleur_niveau(part), court(nombre))
end
local stress = lire_stress()
local part = stress.utilise / stress.capacite
local couleur = "lime"
if stress.surcharge or part >= 0.9 then couleur = "red" elseif part >= 0.75 then couleur = "yellow" end
dessiner_barre("stress", STRESS_Y, part, couleur, math.floor(part * 100) .. "%")
dessiner_texte("detail", BARRE_X, STRESS_Y + BARRE_H + 6, W - BARRE_X,
court(stress.utilise) .. " SUR " .. court(stress.capacite) .. " SU", "light_gray", 2)
dessiner_texte("heure", W - MARGE - 38, 8, 38, texte_heure(), "white", 2)
end
dessiner_fond()
rafraichir()
Le tester
Avant les moniteurs, lancez le programme dans le terminal :
edit startup, collez, Ctrl+S, Échap, puisstartup. Demandez à l'ordinateur ce qu'il voit, en une ligne à l'invitebrass:> brass brass> for _, n in ipairs(peripheral.list()) do print(n) end front back inventory@-112,64,230 inventory@-108,64,230 kinetic@-110,63,233
frontest le mur de moniteurs,backl'arbre qui alimente l'ordinateur, les noms avec un@sont sur le câble. Pas de lignekinetic@: le câble ne touche aucune machine, collez-lui le Stressomètre.- Versez une pile de fer dans un coffre renforcé : sa barre grandit au rafraîchissement suivant, ou tout de suite si vous cliquez sur l'écran.
- Cassez le Stressomètre : la barre de stress devient grise et la ligne du dessous affiche PAS DE JAUGE SUR LE CABLE. Reposez-le : la jauge revient, sans rien redémarrer.
- Chargez le réseau de machines jusqu'à la surcharge : la barre et son texte passent au rouge.
- Posez le mur de moniteurs. Si un clic droit ouvre le terminal au lieu de rafraîchir, c'est que le programme ne tourne pas (cherchez une erreur dans le terminal).
Variantes
- Une alarme en plus. À la fin de
rafraichir, allumez une lampe ou une cloche dès qu'une ressource est dans le rouge :rs.set("top", bas), avecbasmis àtruedans la boucle quandpart < 0.25. La recetteAlarme de stock et panneau d'affichageva plus loin. - Une Liaison d'affichage aussi.
display.set({"Fer : " .. totaux["minecraft:iron_ingot"]})envoie les mêmes nombres à un Panneau d'affichage de Create, par une Liaison d'affichage posée contre l'ordinateur (display), pour un second écran au loin. - Les fluides. Les réservoirs du câble apparaissent dans
peripheral.list("tank");@tank.tanksdonnefluid,amountetcapacityen millibuckets, prêts pour une barre :part = amount / capacity. - Les tendances. Gardez les nombres du rafraîchissement précédent dans une table et dessinez une petite flèche (
gfx.triangle) vers le haut ou vers le bas à côté de chaque nombre. - L'historique. La recette
Enregistreur de données et graphiqueenregistre une valeur chaque minute et trace sa courbe : une seconde page du tableau de bord, affichée sur un clic.