Compare commits
39 Commits
5362d31036
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 60cbaccb8e | |||
| 2ccf008f7f | |||
| b6f49cb6bb | |||
| 408ced35ea | |||
| c780a4ec92 | |||
| 9e8fbd17ed | |||
| 76e734f6ba | |||
| 49e4363cd9 | |||
| 1480119c8e | |||
| 090fec482a | |||
| 5c3b092727 | |||
| eee51ee05a | |||
| c963557649 | |||
| a349ca92f0 | |||
| 756957ece6 | |||
| 2476e7b78f | |||
| 10d96bba81 | |||
| 8a14c96534 | |||
| d4f18113a6 | |||
| d23a39dbfa | |||
| b0e9b3ed23 | |||
| 3245afa156 | |||
| 741fad1f22 | |||
| 1f61140498 | |||
| 661becfb45 | |||
| 03a88fb2c5 | |||
| 5a24e96217 | |||
| 17ed31c60c | |||
| 8ea978196e | |||
| a0302d764d | |||
| 28b3dc0494 | |||
| b347855349 | |||
| 484b6a5117 | |||
| 2eea78980e | |||
| 543385a880 | |||
| d586491a07 | |||
| aac7e4f52d | |||
| a91b354295 | |||
| 215df3f75d |
@@ -2,6 +2,136 @@
|
|||||||
|
|
||||||
Modulares CC:Tweaked-Steuerungssystem fuer die ATM10-Megabase.
|
Modulares CC:Tweaked-Steuerungssystem fuer die ATM10-Megabase.
|
||||||
|
|
||||||
|
## Create Factory Scheduler V1
|
||||||
|
|
||||||
|
`create_factory_scheduler.lua` ist der persistente Nachfolger des Logistiktests.
|
||||||
|
Der funktionierende `create_factory_test.lua` bleibt als Rueckfallstand erhalten.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
delete create_factory_scheduler.lua
|
||||||
|
wget https://git.peli-server.de/Dystroyer8/ATM10_CC_Codes/raw/branch/main/create_factory_scheduler.lua create_factory_scheduler.lua
|
||||||
|
create_factory_scheduler
|
||||||
|
```
|
||||||
|
|
||||||
|
Neue normale Module und Rezepte werden oben in der Tabelle `RECIPES` registriert.
|
||||||
|
Die Scheduler-Logik muss dafuer nicht veraendert werden. Mehrstufige oder zufaellige
|
||||||
|
Produktionsketten erhalten einen benannten Planner. V1.2 nutzt einen gemeinsamen
|
||||||
|
Metall-Planner fuer Asurine -> Zinc und Crimsite -> Iron. Im AUTO-Modus haelt der
|
||||||
|
erste Keep-Stock-Regler kleine Cogwheels, grosse Cogwheels und Iron Nuggets auf
|
||||||
|
jeweils 64 Stueck. Mixer-, Casing- und Precision-Rezepte sind ueber blätterbare
|
||||||
|
Rezeptseiten erreichbar.
|
||||||
|
|
||||||
|
Seit V1.3 erzeugt der Scheduler fehlende Zwischenprodukte als persistente
|
||||||
|
Vorauftraege. Beispielsweise wartet ein Andesite-Casing-Auftrag automatisch auf
|
||||||
|
fehlendes Andesite Alloy. `presse:1` bis `presse:6` bilden einen gemeinsamen Pool
|
||||||
|
fuer Gold-, Iron-, Copper-, Brass-, Zinc- und Steel-Plates. Precision Mechanisms
|
||||||
|
verwenden in ATM10 `alltheores:gold_plate`. Ihre lokalen Deployer-, Chute- und
|
||||||
|
Vault-Puffer werden durch Factory Gauges versorgt; der globale Keep-Stock fuellt
|
||||||
|
verbrauchte Cogwheels und Iron Nuggets parallel wieder auf.
|
||||||
|
|
||||||
|
V1.3.2 synchronisiert Vorauftrag und Hauptauftrag explizit: Das erzeugte Material
|
||||||
|
wird erst freigegeben, nachdem der Vorauftrag seine Ankunft registriert hat. So
|
||||||
|
kann ein nachfolgender Auftrag den neuen Bestand nicht mehr im selben Tick
|
||||||
|
wegnehmen und dadurch einen falschen Retry oder Fehler ausloesen.
|
||||||
|
|
||||||
|
V1.4 bindet die Advanced-Peripherals-ME-Bridge als geschuetzten Fallback ein.
|
||||||
|
Vorhandener Create-Bestand und Create-eigene Produktionsketten haben weiterhin
|
||||||
|
Vorrang. Nur externe, im Create-Netz fehlende Rohstoffe werden in exakt der
|
||||||
|
fehlenden Menge aus dem ME in `sophisticatedstorage:chest_0` exportiert. Diese
|
||||||
|
erste Stufe startet bewusst noch kein ME-Autocrafting.
|
||||||
|
|
||||||
|
V1.5 ergaenzt Ore-/Tag-Substitution fuer vereinheitlichte Materialien. Ingots,
|
||||||
|
Nuggets, Plates, Dusts, Gears, Rods und Clumps werden ueber ihre gemeinsamen
|
||||||
|
`c:`-Tags erkannt. Dadurch sind beispielsweise `create:brass_ingot` und
|
||||||
|
`alltheores:brass_ingot` fuer Lagerzaehlung, Create-Versand und ME-Export
|
||||||
|
gleichwertig. Eindeutige Create-Bauteile verwenden weiterhin exakte IDs.
|
||||||
|
|
||||||
|
V1.6 integriert die Blaze-Cake-Vorstufe. Netherrack wird ueber `crushing` zu
|
||||||
|
Cinder Flour verarbeitet. Egg, Sugar und Cinder Flour gehen anschliessend an die
|
||||||
|
separate Basin-Presse `presse:7` und werden zu Blaze Cake Bases kompaktiert. Die
|
||||||
|
Depot-Pressen `presse:1` bis `presse:6` bleiben ausschliesslich fuer Plates
|
||||||
|
reserviert. Fuer fertige Blaze Cakes fehlt noch eine adressierbare Filling-Station
|
||||||
|
mit 250 mB Lava pro Base. Eine Charge umfasst wegen der Stackgroesse von Eggs
|
||||||
|
genau 16 Bases.
|
||||||
|
|
||||||
|
V1.7 automatisiert auch den letzten Schritt: Blaze Cake Bases werden an die
|
||||||
|
Filling-Station `Blazecake` geschickt und dort mit der fest angeschlossenen
|
||||||
|
Lava-Versorgung zu fertigen Blaze Cakes gefuellt. Im AUTO-Modus haelt der
|
||||||
|
Scheduler 64 Blaze Cakes vor. Der Auftrag darf insgesamt 64 Cakes umfassen,
|
||||||
|
arbeitet wegen der Egg-Stackgroesse jedoch durchgehend in sicheren 16er-Chargen.
|
||||||
|
Sobald eine Charge im Lager angekommen ist, startet ohne zusaetzliche
|
||||||
|
60-Sekunden-Wartezeit die naechste; nur nach der letzten Charge gilt der normale
|
||||||
|
Nachlauf.
|
||||||
|
|
||||||
|
## Create-Fabrik: Peripherie erkennen
|
||||||
|
|
||||||
|
Das rein lesende Diagnoseprogramm herunterladen und starten:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
delete create_factory_debug.lua
|
||||||
|
wget https://git.peli-server.de/Dystroyer8/ATM10_CC_Codes/raw/branch/main/create_factory_debug.lua create_factory_debug.lua
|
||||||
|
create_factory_debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Es erzeugt auf dem CC-Computer die Datei `factory_peripheral_debug.txt`. Darin
|
||||||
|
stehen alle verbundenen Peripherien, Methoden, Frogport-Adressen sowie abrufbare
|
||||||
|
Paket-, Inventar- und Stock-Ticker-Daten. Das Programm versendet keine Pakete und
|
||||||
|
veraendert keine Redstone-Ausgaenge.
|
||||||
|
|
||||||
|
## Greenhouse-Inventar erfassen
|
||||||
|
|
||||||
|
`greenhouse_inventory_debug.lua` erkennt einen per Wired Modem verbundenen
|
||||||
|
Drawer-/Storage-Controller und schreibt dessen Inhalt rein lesend nach
|
||||||
|
`greenhouse_inventory_debug.txt`. Essenzen, Seeds und die vollstaendige
|
||||||
|
Itemliste werden mit Registry-ID, Menge und Slot ausgegeben.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
delete greenhouse_inventory_debug.lua
|
||||||
|
wget https://git.peli-server.de/Dystroyer8/ATM10_CC_Codes/raw/branch/main/greenhouse_inventory_debug.lua greenhouse_inventory_debug.lua
|
||||||
|
greenhouse_inventory_debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Falls mehrere Controller verbunden sind, kann der im Dump angezeigte
|
||||||
|
Peripheral-Name gezielt uebergeben werden:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
greenhouse_inventory_debug <peripheral_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
## AE2-Pattern und Duplikate erfassen
|
||||||
|
|
||||||
|
Der rein lesende ME-Pattern-Auditor nutzt die Advanced-Peripherals-ME-Bridge und
|
||||||
|
schreibt einen kompakten Pattern-Index, exakte Duplikate, Rezepte mit gleichem
|
||||||
|
Output sowie kleine Rohdaten-Samples nach `me_pattern_debug.txt`. Die kompakte
|
||||||
|
Ausgabe passt in den begrenzten Speicher eines CC-Computers. Er startet keine
|
||||||
|
Crafts und veraendert keine Pattern.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
delete me_pattern_debug.lua
|
||||||
|
wget https://git.peli-server.de/Dystroyer8/ATM10_CC_Codes/raw/branch/main/me_pattern_debug.lua me_pattern_debug.lua
|
||||||
|
me_pattern_debug
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erster Logistik-Funktionstest
|
||||||
|
|
||||||
|
Nach erfolgreicher Erkennung kann der Monitor-Testcontroller installiert werden:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
delete create_factory_test.lua
|
||||||
|
wget https://git.peli-server.de/Dystroyer8/ATM10_CC_Codes/raw/branch/main/create_factory_test.lua create_factory_test.lua
|
||||||
|
create_factory_test
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Controller zeigt automatisch alle Items des Stock-Ticker-Netzes auf
|
||||||
|
durchblaetterbaren Lagerseiten und bietet doppelt zu bestaetigende
|
||||||
|
Testbestellungen fuer `crushing`, `saw:1` und `saw:2`. Pro Verarbeitungslinie
|
||||||
|
bleibt nur ein Auftrag aktiv, bis sein Ergebnis im Lager erkannt wurde. Er
|
||||||
|
steuert noch keine Clutches oder sonstigen Redstone-Ausgaenge.
|
||||||
|
|
||||||
|
Als erster generischer Fan-Prozess ist `fan:washing` enthalten. Der Testauftrag
|
||||||
|
verarbeitet 64 vereinheitlichte Zinc Clumps zu mindestens 576 Zinc Nuggets; das
|
||||||
|
zufaellige Gunpowder-Nebenprodukt erscheint automatisch in der Lagerliste.
|
||||||
|
|
||||||
## Zielbild
|
## Zielbild
|
||||||
|
|
||||||
- zentrale Leitwarte fuer Status, Energie, Alarme und Sektor-Hauptschalter
|
- zentrale Leitwarte fuer Status, Energie, Alarme und Sektor-Hauptschalter
|
||||||
@@ -19,6 +149,77 @@ install
|
|||||||
Der Installer ist momentan ein Bootstrap. Hardwaretreiber und konkrete Anlagenlogik
|
Der Installer ist momentan ein Bootstrap. Hardwaretreiber und konkrete Anlagenlogik
|
||||||
werden ergaenzt, sobald die Peripherie- und Methodennamen der gebauten Anlagen feststehen.
|
werden ergaenzt, sobald die Peripherie- und Methodennamen der gebauten Anlagen feststehen.
|
||||||
|
|
||||||
|
## Einfache Create-Boiler-Anzeige
|
||||||
|
|
||||||
|
Fuer die erste Kraftwerksanzeige werden ein Monitor sowie pro Boiler ein Block Reader aus
|
||||||
|
Advanced Peripherals benoetigt. Mindestens ein weiterer Block Reader zeigt auf das Create-
|
||||||
|
Speedometer oder Stressometer der gemeinsamen Hauptwelle. Alle Reader und der Monitor werden
|
||||||
|
ueber Wired Modems mit dem Computer verbunden. Installation und Start:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
wget https://git.peli-server.de/Dystroyer8/ATM10_CC_Codes/raw/branch/main/create_boiler_display.lua create_boiler_display.lua
|
||||||
|
create_boiler_display
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Programm liest Online-Status und Heizmodus jedes Boilers ueber dessen Block Reader.
|
||||||
|
Drehzahl, Kapazitaet, Verbrauch und Auslastung liest es aus den Netzwerkdaten des Gauge-Readers.
|
||||||
|
Die Monitoraufloesung wird automatisch erkannt und die groesste passende Textskalierung gewaehlt.
|
||||||
|
Ein Boiler ohne Wasserversorgung oder angeschlossene Engine wird als IDLE angezeigt, selbst wenn
|
||||||
|
eine Campfire- oder Blaze-Burner-Waermequelle erkannt wird.
|
||||||
|
|
||||||
|
### Clutch-Teststeuerung
|
||||||
|
|
||||||
|
Die linke Seite des Computers ist als Redstone-Ausgang fuer die Test-Clutch konfiguriert.
|
||||||
|
Zwischen Ausgang und Clutch sitzt eine Redstone-Fackel als Fail-safe-Inverter: Ausgang `ON`
|
||||||
|
bedeutet `PRODUCTION: RUNNING`; Ausgang `OFF`, Programmabbruch oder Computerneustart bedeutet
|
||||||
|
`PRODUCTION: STOPPED`. Die Verbindung kann direkt oder ueber Create Redstone Links erfolgen.
|
||||||
|
|
||||||
|
Der gewuenschte Zustand wird nach jeder Touch-Bedienung in `create_boiler_state.txt` gespeichert.
|
||||||
|
Beim naechsten Programmstart wird er wiederhergestellt. Fuer automatischen Start nach einem
|
||||||
|
Computer- oder Weltneustart kann das Startprogramm als `startup.lua` installiert werden:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
wget https://git.peli-server.de/Dystroyer8/ATM10_CC_Codes/raw/branch/main/create_boiler_startup.lua startup.lua
|
||||||
|
reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
Waehrend der Computer aus ist, bleibt die Hardware durch den Fackel-Inverter abgeschaltet. Erst
|
||||||
|
nach erfolgreichem Programmstart wird ein zuvor gespeicherter RUNNING-Zustand wieder aktiviert.
|
||||||
|
|
||||||
|
Die Boiler werden nicht anhand wechselnder Peripheral-Namen, sondern fest ueber ihre
|
||||||
|
Controller-Koordinaten `11266/64/10810`, `11260/64/10810` und `11254/64/10810` zugeordnet.
|
||||||
|
Fehlt ein Reader, bleibt dadurch exakt die betroffene Boiler-Zeile auf `MISSING / NO READER`.
|
||||||
|
Jeder Block Reader muss direkt auf den Controller-Tank seines Boiler-Multiblocks zeigen.
|
||||||
|
|
||||||
|
## Andesite-Alloy-Produktion
|
||||||
|
|
||||||
|
Der erste lokale Create-Produktionscontroller ueberwacht einen Zink-Nugget-Vault, einen
|
||||||
|
Andesite-Alloy-Vault und die per Clutch geschaltete Produktionsanlage. Die beiden Vaults muessen
|
||||||
|
jeweils direkt ueber einen aktivierten Wired Modem Full Block als Inventory-Peripheral mit dem
|
||||||
|
Computer verbunden sein. Der Redstone-Ausgang `left` steuert die Clutch ueber einen Fail-safe-Inverter:
|
||||||
|
Redstone `ON` schaltet die Fackel aus und gibt die Welle frei; bei Redstone `OFF`, Programmabbruch
|
||||||
|
oder Computer-Ausfall stoppt die Fackel die Welle.
|
||||||
|
|
||||||
|
Installation und erster Start:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
wget https://git.peli-server.de/Dystroyer8/ATM10_CC_Codes/raw/branch/main/create_andesite_display.lua create_andesite_display.lua
|
||||||
|
create_andesite_display
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Controller erkennt die Vaults anhand von `create:zinc_nugget` und
|
||||||
|
`create:andesite_alloy` und speichert ihre Peripheral-Namen. Die Freigabe wird ebenfalls
|
||||||
|
gespeichert. Fehlendes Zink, ein voller Ausgang oder fehlende Vault-Daten schalten die
|
||||||
|
Produktionsanlage ab; nach Behebung wird eine weiterhin freigegebene Anlage automatisch
|
||||||
|
fortgesetzt. Globale RPM-, Kapazitaets- und Stresswerte gehoeren in den spaeteren Create-
|
||||||
|
Serverraum, da ein Stressometer immer das gesamte zusammenhaengende kinetische Netzwerk misst.
|
||||||
|
Fuer den automatischen Start:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
wget https://git.peli-server.de/Dystroyer8/ATM10_CC_Codes/raw/branch/main/create_andesite_startup.lua startup.lua
|
||||||
|
reboot
|
||||||
|
```
|
||||||
|
|
||||||
## Struktur
|
## Struktur
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -34,4 +235,3 @@ docs/ Architektur- und Protokolldokumentation
|
|||||||
|
|
||||||
Die Zentrale fordert nur einen Zielzustand an. Lokale Controller fuehren sichere
|
Die Zentrale fordert nur einen Zielzustand an. Lokale Controller fuehren sichere
|
||||||
Start-/Stoppsequenzen aus und bestaetigen den tatsaechlichen Zustand.
|
Start-/Stoppsequenzen aus und bestaetigen den tatsaechlichen Zustand.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,344 @@
|
|||||||
|
-- Andesite Alloy production controller for CC:Tweaked.
|
||||||
|
-- Two Create Item Vaults are discovered by their contents and remembered.
|
||||||
|
|
||||||
|
local REFRESH_SECONDS = 1
|
||||||
|
local MIN_WIDTH = 28
|
||||||
|
local MIN_HEIGHT = 13
|
||||||
|
local CLUTCH_OUTPUT_SIDE = "left"
|
||||||
|
-- The external redstone-torch inverter makes output ON release the Clutch.
|
||||||
|
local OUTPUT_LEVEL_WHEN_RUNNING = true
|
||||||
|
local STATE_FILE = "create_andesite_state.txt"
|
||||||
|
local DEVICE_FILE = "create_andesite_devices.txt"
|
||||||
|
|
||||||
|
local ZINC_ITEMS = {
|
||||||
|
["create:zinc_nugget"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local ALLOY_ITEMS = {
|
||||||
|
["create:andesite_alloy"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local button = { x1 = 0, x2 = 0, y = 0 }
|
||||||
|
|
||||||
|
local function loadText(path)
|
||||||
|
if not fs.exists(path) then return nil end
|
||||||
|
local handle = fs.open(path, "r")
|
||||||
|
if not handle then return nil end
|
||||||
|
local value = handle.readAll()
|
||||||
|
handle.close()
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
local function saveText(path, value)
|
||||||
|
local handle = fs.open(path, "w")
|
||||||
|
if not handle then return false end
|
||||||
|
handle.write(value)
|
||||||
|
handle.close()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local requestedRunning = loadText(STATE_FILE) == "RUNNING"
|
||||||
|
local devices = textutils.unserialize(loadText(DEVICE_FILE) or "") or {}
|
||||||
|
local capacityCache = {}
|
||||||
|
|
||||||
|
local function setMachineRunning(running)
|
||||||
|
local level = running and OUTPUT_LEVEL_WHEN_RUNNING or not OUTPUT_LEVEL_WHEN_RUNNING
|
||||||
|
redstone.setOutput(CLUTCH_OUTPUT_SIDE, level)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Fail safe while peripherals are being discovered.
|
||||||
|
setMachineRunning(false)
|
||||||
|
|
||||||
|
local monitor = peripheral.find("monitor")
|
||||||
|
if not monitor then
|
||||||
|
error("Kein Monitor gefunden. Monitor per Wired Modem verbinden.", 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fitMonitor()
|
||||||
|
local chosen = 0.5
|
||||||
|
for step = 10, 1, -1 do
|
||||||
|
local scale = step / 2
|
||||||
|
local ok = pcall(monitor.setTextScale, scale)
|
||||||
|
if ok then
|
||||||
|
local width, height = monitor.getSize()
|
||||||
|
if width >= MIN_WIDTH and height >= MIN_HEIGHT then
|
||||||
|
chosen = scale
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
monitor.setTextScale(chosen)
|
||||||
|
monitor.setCursorBlink(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
fitMonitor()
|
||||||
|
|
||||||
|
local function center(y, text, foreground, background)
|
||||||
|
local width = monitor.getSize()
|
||||||
|
text = tostring(text or "")
|
||||||
|
if #text > width then text = text:sub(1, width) end
|
||||||
|
local x = math.max(1, math.floor((width - #text) / 2) + 1)
|
||||||
|
monitor.setBackgroundColor(background or colors.black)
|
||||||
|
monitor.setTextColor(foreground or colors.white)
|
||||||
|
monitor.setCursorPos(1, y)
|
||||||
|
monitor.write((" "):rep(width))
|
||||||
|
monitor.setCursorPos(x, y)
|
||||||
|
monitor.write(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function formatInteger(value)
|
||||||
|
value = math.floor(math.abs(tonumber(value) or 0) + 0.5)
|
||||||
|
local digits, parts = tostring(value), {}
|
||||||
|
while #digits > 3 do
|
||||||
|
table.insert(parts, 1, digits:sub(-3))
|
||||||
|
digits = digits:sub(1, -4)
|
||||||
|
end
|
||||||
|
table.insert(parts, 1, digits)
|
||||||
|
return table.concat(parts, ".")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isVault(name)
|
||||||
|
return peripheral.isPresent(name)
|
||||||
|
and peripheral.hasType(name, "inventory")
|
||||||
|
and peripheral.hasType(name, "create:item_vault")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getInventory(name)
|
||||||
|
if not name or not isVault(name) then return nil end
|
||||||
|
local okList, listed = pcall(peripheral.call, name, "list")
|
||||||
|
local okSize, size = pcall(peripheral.call, name, "size")
|
||||||
|
if not okList or not okSize or type(listed) ~= "table" then return nil end
|
||||||
|
|
||||||
|
local result = {
|
||||||
|
name = name,
|
||||||
|
list = listed,
|
||||||
|
size = tonumber(size) or 0,
|
||||||
|
capacity = 0,
|
||||||
|
total = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
local cached = capacityCache[name]
|
||||||
|
if cached and cached.size == result.size then
|
||||||
|
result.capacity = cached.capacity
|
||||||
|
else
|
||||||
|
for slot = 1, result.size do
|
||||||
|
local ok, limit = pcall(peripheral.call, name, "getItemLimit", slot)
|
||||||
|
result.capacity = result.capacity + (ok and tonumber(limit) or 64)
|
||||||
|
end
|
||||||
|
capacityCache[name] = { size = result.size, capacity = result.capacity }
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, item in pairs(listed) do
|
||||||
|
result.total = result.total + (tonumber(item.count) or 0)
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function countItems(inventory, wanted)
|
||||||
|
if not inventory then return 0 end
|
||||||
|
local count = 0
|
||||||
|
for _, item in pairs(inventory.list) do
|
||||||
|
local name = tostring(item.name or ""):lower()
|
||||||
|
local matches = wanted[item.name]
|
||||||
|
if wanted == ZINC_ITEMS and name:find("zinc", 1, true) and name:find("nugget", 1, true) then
|
||||||
|
matches = true
|
||||||
|
elseif wanted == ALLOY_ITEMS and name:find("andesite_alloy", 1, true) then
|
||||||
|
matches = true
|
||||||
|
end
|
||||||
|
if matches then count = count + (tonumber(item.count) or 0) end
|
||||||
|
end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
local function containsItems(inventory, wanted)
|
||||||
|
return countItems(inventory, wanted) > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local function discoverVaults()
|
||||||
|
local oldZinc, oldAlloy = devices.zinc, devices.alloy
|
||||||
|
local savedZinc = getInventory(devices.zinc)
|
||||||
|
local savedAlloy = getInventory(devices.alloy)
|
||||||
|
local connectedVaults = {}
|
||||||
|
|
||||||
|
for _, name in ipairs(peripheral.getNames()) do
|
||||||
|
if isVault(name) then
|
||||||
|
local inventory = getInventory(name)
|
||||||
|
if inventory then
|
||||||
|
connectedVaults[#connectedVaults + 1] = inventory
|
||||||
|
if containsItems(inventory, ZINC_ITEMS) then
|
||||||
|
devices.zinc = name
|
||||||
|
savedZinc = inventory
|
||||||
|
end
|
||||||
|
if containsItems(inventory, ALLOY_ITEMS) then
|
||||||
|
devices.alloy = name
|
||||||
|
savedAlloy = inventory
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This controller intentionally owns exactly two vaults. If a mod supplies
|
||||||
|
-- Zinc Nuggets under an unexpected registry name, the non-alloy vault is
|
||||||
|
-- still unambiguous and becomes the input vault.
|
||||||
|
if not savedZinc and savedAlloy and #connectedVaults == 2 then
|
||||||
|
for _, inventory in ipairs(connectedVaults) do
|
||||||
|
if inventory.name ~= savedAlloy.name then
|
||||||
|
devices.zinc = inventory.name
|
||||||
|
savedZinc = inventory
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not savedAlloy and savedZinc and #connectedVaults == 2 then
|
||||||
|
for _, inventory in ipairs(connectedVaults) do
|
||||||
|
if inventory.name ~= savedZinc.name then
|
||||||
|
devices.alloy = inventory.name
|
||||||
|
savedAlloy = inventory
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if devices.zinc ~= oldZinc or devices.alloy ~= oldAlloy then
|
||||||
|
saveText(DEVICE_FILE, textutils.serialize(devices))
|
||||||
|
end
|
||||||
|
return savedZinc or getInventory(devices.zinc), savedAlloy or getInventory(devices.alloy)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function evaluate()
|
||||||
|
local zincVault, alloyVault = discoverVaults()
|
||||||
|
local zinc = countItems(zincVault, ZINC_ITEMS)
|
||||||
|
local alloy = countItems(alloyVault, ALLOY_ITEMS)
|
||||||
|
|
||||||
|
local missing = not zincVault or not alloyVault
|
||||||
|
local noMaterial = zincVault and zinc <= 0
|
||||||
|
local outputFull = alloyVault and alloyVault.capacity > 0 and alloyVault.total >= alloyVault.capacity
|
||||||
|
local allowed = not missing and not noMaterial and not outputFull
|
||||||
|
local effectiveRunning = requestedRunning and allowed
|
||||||
|
|
||||||
|
setMachineRunning(effectiveRunning)
|
||||||
|
|
||||||
|
local status, color = "STOPPED", colors.red
|
||||||
|
if requestedRunning then
|
||||||
|
if missing then status, color = "NO VAULT DATA", colors.red
|
||||||
|
elseif noMaterial then status, color = "WAITING: NO ZINC", colors.orange
|
||||||
|
elseif outputFull then status, color = "STANDBY: OUTPUT FULL", colors.orange
|
||||||
|
else status, color = "RUNNING", colors.lime end
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
zincVault = zincVault,
|
||||||
|
alloyVault = alloyVault,
|
||||||
|
zinc = zinc,
|
||||||
|
alloy = alloy,
|
||||||
|
noMaterial = noMaterial,
|
||||||
|
outputFull = outputFull,
|
||||||
|
status = status,
|
||||||
|
statusColor = color,
|
||||||
|
effectiveRunning = effectiveRunning,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawInventory(y, label, count, inventory, color)
|
||||||
|
if not inventory then
|
||||||
|
center(y, label .. ": NO DATA", colors.red)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local text = ("%s %s / %s"):format(
|
||||||
|
label,
|
||||||
|
formatInteger(count),
|
||||||
|
formatInteger(inventory.capacity)
|
||||||
|
)
|
||||||
|
center(y, text, color or colors.white)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawButton(height, data)
|
||||||
|
local width = monitor.getSize()
|
||||||
|
local label = requestedRunning and " CONTROL: ENABLED " or " CONTROL: DISABLED "
|
||||||
|
local x = math.max(1, math.floor((width - #label) / 2) + 1)
|
||||||
|
local y = math.max(12, height - 1)
|
||||||
|
|
||||||
|
button.x1 = x
|
||||||
|
button.x2 = math.min(width, x + #label - 1)
|
||||||
|
button.y = y
|
||||||
|
|
||||||
|
monitor.setCursorPos(1, y)
|
||||||
|
monitor.setBackgroundColor(colors.black)
|
||||||
|
monitor.write((" "):rep(width))
|
||||||
|
monitor.setCursorPos(x, y)
|
||||||
|
monitor.setBackgroundColor(requestedRunning and (data.effectiveRunning and colors.lime or colors.orange) or colors.red)
|
||||||
|
monitor.setTextColor(colors.black)
|
||||||
|
monitor.write(label)
|
||||||
|
monitor.setBackgroundColor(colors.black)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function render()
|
||||||
|
local _, height = monitor.getSize()
|
||||||
|
local data = evaluate()
|
||||||
|
|
||||||
|
monitor.setBackgroundColor(colors.black)
|
||||||
|
monitor.clear()
|
||||||
|
center(1, "ANDESITE FACTORY", colors.cyan)
|
||||||
|
|
||||||
|
drawInventory(3, "ZINC", data.zinc, data.zincVault,
|
||||||
|
data.zinc <= 0 and colors.orange or colors.lime)
|
||||||
|
drawInventory(4, "ALLOY", data.alloy, data.alloyVault,
|
||||||
|
data.alloyVault and data.alloyVault.total >= data.alloyVault.capacity and colors.orange or colors.lime)
|
||||||
|
|
||||||
|
center(6, "LOCAL CONTROL", colors.cyan)
|
||||||
|
if not data.zincVault then
|
||||||
|
center(7, "INPUT: NO DATA", colors.red)
|
||||||
|
else
|
||||||
|
center(7, data.noMaterial and "INPUT: EMPTY" or "INPUT: READY",
|
||||||
|
data.noMaterial and colors.orange or colors.lime)
|
||||||
|
end
|
||||||
|
if not data.alloyVault then
|
||||||
|
center(8, "OUTPUT: NO DATA", colors.red)
|
||||||
|
else
|
||||||
|
center(8, data.outputFull and "OUTPUT: FULL" or "OUTPUT: SPACE",
|
||||||
|
data.outputFull and colors.orange or colors.lime)
|
||||||
|
end
|
||||||
|
center(10, data.status, data.statusColor)
|
||||||
|
drawButton(height, data)
|
||||||
|
center(height, "Touch = Anlage freigeben/stoppen", colors.gray)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function safeRender()
|
||||||
|
local ok, reason = pcall(render)
|
||||||
|
if not ok then
|
||||||
|
setMachineRunning(false)
|
||||||
|
monitor.setBackgroundColor(colors.black)
|
||||||
|
monitor.clear()
|
||||||
|
center(1, "CONTROLLER ERROR", colors.red)
|
||||||
|
center(3, tostring(reason), colors.orange)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function toggleControl()
|
||||||
|
requestedRunning = not requestedRunning
|
||||||
|
saveText(STATE_FILE, requestedRunning and "RUNNING" or "STOPPED")
|
||||||
|
end
|
||||||
|
|
||||||
|
safeRender()
|
||||||
|
local refreshTimer = os.startTimer(REFRESH_SECONDS)
|
||||||
|
|
||||||
|
local function run()
|
||||||
|
while true do
|
||||||
|
local event, first, second, third = os.pullEventRaw()
|
||||||
|
if event == "terminate" then
|
||||||
|
return
|
||||||
|
elseif event == "timer" and first == refreshTimer then
|
||||||
|
safeRender()
|
||||||
|
refreshTimer = os.startTimer(REFRESH_SECONDS)
|
||||||
|
elseif event == "monitor_touch" then
|
||||||
|
local x, y = second, third
|
||||||
|
if y == button.y and x >= button.x1 and x <= button.x2 then
|
||||||
|
toggleControl()
|
||||||
|
safeRender()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, reason = pcall(run)
|
||||||
|
setMachineRunning(false)
|
||||||
|
if not ok then error(reason, 0) end
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- Startup wrapper for the local Andesite Alloy production controller.
|
||||||
|
|
||||||
|
if not fs.exists("create_andesite_display.lua") then
|
||||||
|
printError("create_andesite_display.lua fehlt")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
shell.run("create_andesite_display.lua")
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
-- Create boiler status board for CC:Tweaked.
|
||||||
|
-- All Create values are read through Advanced Peripherals Block Readers.
|
||||||
|
|
||||||
|
local REFRESH_SECONDS = 1
|
||||||
|
local MIN_WIDTH = 38
|
||||||
|
local MIN_HEIGHT = 14
|
||||||
|
local CLUTCH_OUTPUT_SIDE = "left"
|
||||||
|
local STATE_FILE = "create_boiler_state.txt"
|
||||||
|
|
||||||
|
local BOILERS = {
|
||||||
|
{ label = "BOILER 1", position = { 11266, 64, 10810 } },
|
||||||
|
{ label = "BOILER 2", position = { 11260, 64, 10810 } },
|
||||||
|
{ label = "BOILER 3", position = { 11254, 64, 10810 } },
|
||||||
|
}
|
||||||
|
|
||||||
|
local clutchButton = { x1 = 0, x2 = 0, y = 0 }
|
||||||
|
|
||||||
|
local function loadProductionState()
|
||||||
|
if not fs.exists(STATE_FILE) then return false end
|
||||||
|
local handle = fs.open(STATE_FILE, "r")
|
||||||
|
if not handle then return false end
|
||||||
|
local saved = handle.readAll()
|
||||||
|
handle.close()
|
||||||
|
return saved == "RUNNING"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function saveProductionState(running)
|
||||||
|
local handle = fs.open(STATE_FILE, "w")
|
||||||
|
if not handle then return false end
|
||||||
|
handle.write(running and "RUNNING" or "STOPPED")
|
||||||
|
handle.close()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local productionRunning = loadProductionState()
|
||||||
|
redstone.setOutput(CLUTCH_OUTPUT_SIDE, productionRunning)
|
||||||
|
|
||||||
|
local monitor = peripheral.find("monitor")
|
||||||
|
if not monitor then
|
||||||
|
error("Kein Monitor gefunden. Monitor direkt oder per Wired Modem verbinden.", 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fitMonitor()
|
||||||
|
local chosen = 0.5
|
||||||
|
for step = 10, 1, -1 do
|
||||||
|
local scale = step / 2
|
||||||
|
local ok = pcall(monitor.setTextScale, scale)
|
||||||
|
if ok then
|
||||||
|
local width, height = monitor.getSize()
|
||||||
|
if width >= MIN_WIDTH and height >= MIN_HEIGHT then
|
||||||
|
chosen = scale
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
monitor.setTextScale(chosen)
|
||||||
|
monitor.setCursorBlink(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
fitMonitor()
|
||||||
|
|
||||||
|
local function center(y, text, foreground, background)
|
||||||
|
local width = monitor.getSize()
|
||||||
|
text = tostring(text)
|
||||||
|
if #text > width then text = text:sub(1, width) end
|
||||||
|
local x = math.max(1, math.floor((width - #text) / 2) + 1)
|
||||||
|
monitor.setBackgroundColor(background or colors.black)
|
||||||
|
monitor.setTextColor(foreground or colors.white)
|
||||||
|
monitor.setCursorPos(1, y)
|
||||||
|
monitor.write((" "):rep(width))
|
||||||
|
monitor.setCursorPos(x, y)
|
||||||
|
monitor.write(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getValueIgnoringCase(data, wantedKeys)
|
||||||
|
if type(data) ~= "table" then return nil end
|
||||||
|
for key, value in pairs(data) do
|
||||||
|
local lowered = tostring(key):lower()
|
||||||
|
for _, wanted in ipairs(wantedKeys) do
|
||||||
|
if lowered == wanted:lower() then return value end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function samePosition(a, b)
|
||||||
|
if type(a) ~= "table" or type(b) ~= "table" then return true end
|
||||||
|
return tonumber(a[1]) == tonumber(b[1])
|
||||||
|
and tonumber(a[2]) == tonumber(b[2])
|
||||||
|
and tonumber(a[3]) == tonumber(b[3])
|
||||||
|
end
|
||||||
|
|
||||||
|
local function positionKey(position)
|
||||||
|
if type(position) ~= "table" then return nil end
|
||||||
|
local x, y, z = tonumber(position[1]), tonumber(position[2]), tonumber(position[3])
|
||||||
|
if not x or not y or not z then return nil end
|
||||||
|
return ("%d,%d,%d"):format(x, y, z)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function discoverReaders()
|
||||||
|
local boilers, networks = {}, {}
|
||||||
|
local names = peripheral.getNames()
|
||||||
|
table.sort(names)
|
||||||
|
|
||||||
|
for _, name in ipairs(names) do
|
||||||
|
if peripheral.hasType(name, "block_reader") then
|
||||||
|
local ok, data = pcall(peripheral.call, name, "getBlockData")
|
||||||
|
if ok and type(data) == "table" then
|
||||||
|
local boiler = getValueIgnoringCase(data, { "Boiler" })
|
||||||
|
local network = getValueIgnoringCase(data, { "Network" })
|
||||||
|
local speed = getValueIgnoringCase(data, { "Speed" })
|
||||||
|
|
||||||
|
if type(boiler) == "table" then
|
||||||
|
local position = getValueIgnoringCase(data, { "LastKnownPos", "Controller" })
|
||||||
|
local key = positionKey(position)
|
||||||
|
if key then boilers[key] = { name = name, data = data, boiler = boiler } end
|
||||||
|
elseif type(network) == "table" and tonumber(speed) then
|
||||||
|
networks[#networks + 1] = { name = name, data = data, network = network }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return boilers, networks
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readBoiler(entry)
|
||||||
|
if not entry then return "MISSING", "NO READER", colors.red end
|
||||||
|
|
||||||
|
local controller = getValueIgnoringCase(entry.data, { "Controller" })
|
||||||
|
local readerPos = getValueIgnoringCase(entry.data, { "LastKnownPos" })
|
||||||
|
if not samePosition(controller, readerPos) then
|
||||||
|
return "READER", "WRONG TANK", colors.red
|
||||||
|
end
|
||||||
|
|
||||||
|
local boiler = entry.boiler
|
||||||
|
local passive = tonumber(getValueIgnoringCase(boiler, { "PassiveHeat", "passive" })) or 0
|
||||||
|
local heat = tonumber(getValueIgnoringCase(boiler, { "ActiveHeat", "Heat", "actualHeat" })) or 0
|
||||||
|
local supply = tonumber(getValueIgnoringCase(boiler, { "Supply", "waterSupply" })) or 0
|
||||||
|
local engines = tonumber(getValueIgnoringCase(boiler, { "Engines", "attachedEngines" })) or 0
|
||||||
|
|
||||||
|
-- A heat source alone does not make the boiler operational. Create reports
|
||||||
|
-- PassiveHeat even when a campfire boiler has no water and is IDLE.
|
||||||
|
if supply <= 0 then return "IDLE", "NO WATER", colors.red end
|
||||||
|
if engines <= 0 then return "IDLE", "NO ENGINE", colors.red end
|
||||||
|
|
||||||
|
if passive > 0 then return "ONLINE", "CAMPFIRE", colors.lime end
|
||||||
|
if heat >= 18 then return "ONLINE", "SUPERHEATED", colors.lime end
|
||||||
|
if heat > 0 then return "ONLINE", "HEATED", colors.lime end
|
||||||
|
return "IDLE", "NO HEAT", colors.orange
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readMainShaft(entry)
|
||||||
|
if not entry then return nil, nil, nil, nil end
|
||||||
|
local speed = math.abs(tonumber(getValueIgnoringCase(entry.data, { "Speed" })) or 0)
|
||||||
|
local capacity = tonumber(getValueIgnoringCase(entry.network, { "Capacity" })) or 0
|
||||||
|
local usage = tonumber(getValueIgnoringCase(entry.network, { "Stress" })) or 0
|
||||||
|
local load = capacity > 0 and usage / capacity * 100 or 0
|
||||||
|
return speed, capacity, usage, load
|
||||||
|
end
|
||||||
|
|
||||||
|
local function formatInteger(value)
|
||||||
|
value = math.floor(math.abs(tonumber(value) or 0) + 0.5)
|
||||||
|
local digits, parts = tostring(value), {}
|
||||||
|
while #digits > 3 do
|
||||||
|
table.insert(parts, 1, digits:sub(-3))
|
||||||
|
digits = digits:sub(1, -4)
|
||||||
|
end
|
||||||
|
table.insert(parts, 1, digits)
|
||||||
|
return table.concat(parts, ".")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawClutchButton(height)
|
||||||
|
local width = monitor.getSize()
|
||||||
|
-- With the external redstone-torch inverter, output ON means the clutch is
|
||||||
|
-- unpowered and production runs. Output OFF is the fail-safe stopped state.
|
||||||
|
local label = productionRunning and " PRODUCTION: RUNNING " or " PRODUCTION: STOPPED "
|
||||||
|
local x = math.max(1, math.floor((width - #label) / 2) + 1)
|
||||||
|
local y = math.max(13, height - 1)
|
||||||
|
|
||||||
|
clutchButton.x1 = x
|
||||||
|
clutchButton.x2 = math.min(width, x + #label - 1)
|
||||||
|
clutchButton.y = y
|
||||||
|
|
||||||
|
monitor.setCursorPos(1, y)
|
||||||
|
monitor.setBackgroundColor(colors.black)
|
||||||
|
monitor.write((" "):rep(width))
|
||||||
|
monitor.setCursorPos(x, y)
|
||||||
|
monitor.setBackgroundColor(productionRunning and colors.lime or colors.red)
|
||||||
|
monitor.setTextColor(colors.black)
|
||||||
|
monitor.write(label)
|
||||||
|
monitor.setBackgroundColor(colors.black)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function toggleProduction()
|
||||||
|
productionRunning = not productionRunning
|
||||||
|
redstone.setOutput(CLUTCH_OUTPUT_SIDE, productionRunning)
|
||||||
|
saveProductionState(productionRunning)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function render()
|
||||||
|
local _, height = monitor.getSize()
|
||||||
|
local boilers, networks = discoverReaders()
|
||||||
|
local speed, capacity, usage, load = readMainShaft(networks[1])
|
||||||
|
|
||||||
|
monitor.setBackgroundColor(colors.black)
|
||||||
|
monitor.clear()
|
||||||
|
center(1, "CREATE KRAFTWERK", colors.cyan)
|
||||||
|
|
||||||
|
for index, configuredBoiler in ipairs(BOILERS) do
|
||||||
|
local reader = boilers[positionKey(configuredBoiler.position)]
|
||||||
|
local state, heatMode, color = readBoiler(reader)
|
||||||
|
center(2 + index, ("%s %-7s %s"):format(configuredBoiler.label, state, heatMode), color)
|
||||||
|
end
|
||||||
|
|
||||||
|
center(7, "MAIN SHAFT", colors.cyan)
|
||||||
|
if speed then
|
||||||
|
center(8, ("Speed: %.1f RPM"):format(speed), speed > 0 and colors.lime or colors.orange)
|
||||||
|
center(9, "Capacity: " .. formatInteger(capacity) .. " SU", colors.white)
|
||||||
|
center(10, "Usage: " .. formatInteger(usage) .. " SU", colors.white)
|
||||||
|
local loadText = ("Load: %.1f %%"):format(load):gsub("%.", ",")
|
||||||
|
center(11, loadText, load >= 90 and colors.red or load >= 70 and colors.orange or colors.lime)
|
||||||
|
else
|
||||||
|
center(8, "Kein Gauge-Reader gefunden", colors.red)
|
||||||
|
center(9, "Reader auf Speed-/Stressometer", colors.orange)
|
||||||
|
end
|
||||||
|
|
||||||
|
drawClutchButton(height)
|
||||||
|
center(height, "Touch = Produktion umschalten", colors.gray)
|
||||||
|
monitor.setTextColor(colors.white)
|
||||||
|
monitor.setBackgroundColor(colors.black)
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function safeRender()
|
||||||
|
local ok, reason = pcall(render)
|
||||||
|
if not ok then
|
||||||
|
monitor.setBackgroundColor(colors.black)
|
||||||
|
monitor.clear()
|
||||||
|
center(1, "ANZEIGEFEHLER", colors.red)
|
||||||
|
center(3, tostring(reason), colors.orange)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
safeRender()
|
||||||
|
local refreshTimer = os.startTimer(REFRESH_SECONDS)
|
||||||
|
|
||||||
|
local function run()
|
||||||
|
while true do
|
||||||
|
local event, first, second, third = os.pullEventRaw()
|
||||||
|
if event == "terminate" then
|
||||||
|
return
|
||||||
|
elseif event == "timer" and first == refreshTimer then
|
||||||
|
safeRender()
|
||||||
|
refreshTimer = os.startTimer(REFRESH_SECONDS)
|
||||||
|
elseif event == "monitor_touch" then
|
||||||
|
local x, y = second, third
|
||||||
|
if y == clutchButton.y and x >= clutchButton.x1 and x <= clutchButton.x2 then
|
||||||
|
toggleProduction()
|
||||||
|
safeRender()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, reason = pcall(run)
|
||||||
|
redstone.setOutput(CLUTCH_OUTPUT_SIDE, false)
|
||||||
|
if not ok then error(reason, 0) end
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- Install this file as startup.lua on the Create control computer.
|
||||||
|
-- It resumes the persisted production state through create_boiler_display.lua.
|
||||||
|
|
||||||
|
if not fs.exists("create_boiler_display.lua") then
|
||||||
|
printError("create_boiler_display.lua fehlt")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
sleep(1)
|
||||||
|
shell.run("create_boiler_display.lua")
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
-- ATM10 Create factory peripheral discovery
|
||||||
|
-- Read-only: this program does not send packages or change redstone outputs.
|
||||||
|
|
||||||
|
local OUTPUT_FILE = "factory_peripheral_debug.txt"
|
||||||
|
local MAX_TABLE_ENTRIES = 500
|
||||||
|
local MAX_DEPTH = 5
|
||||||
|
local MAX_VALUE_LENGTH = 30000
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
|
||||||
|
local function emit(value)
|
||||||
|
local line = tostring(value)
|
||||||
|
lines[#lines + 1] = line
|
||||||
|
print(line)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sortedKeys(value)
|
||||||
|
local keys = {}
|
||||||
|
for key in pairs(value) do
|
||||||
|
keys[#keys + 1] = key
|
||||||
|
end
|
||||||
|
table.sort(keys, function(a, b)
|
||||||
|
return tostring(a) < tostring(b)
|
||||||
|
end)
|
||||||
|
return keys
|
||||||
|
end
|
||||||
|
|
||||||
|
local function serialize(value, depth, seen)
|
||||||
|
depth = depth or 0
|
||||||
|
seen = seen or {}
|
||||||
|
|
||||||
|
local valueType = type(value)
|
||||||
|
if valueType == "string" then
|
||||||
|
return string.format("%q", value)
|
||||||
|
elseif valueType ~= "table" then
|
||||||
|
return tostring(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
if seen[value] then
|
||||||
|
return "<cycle>"
|
||||||
|
end
|
||||||
|
if depth >= MAX_DEPTH then
|
||||||
|
return "<max-depth>"
|
||||||
|
end
|
||||||
|
|
||||||
|
seen[value] = true
|
||||||
|
local parts = { "{" }
|
||||||
|
local keys = sortedKeys(value)
|
||||||
|
local limit = math.min(#keys, MAX_TABLE_ENTRIES)
|
||||||
|
|
||||||
|
for index = 1, limit do
|
||||||
|
local key = keys[index]
|
||||||
|
parts[#parts + 1] = " " .. string.rep(" ", depth)
|
||||||
|
.. "[" .. serialize(key, depth + 1, seen) .. "] = "
|
||||||
|
.. serialize(value[key], depth + 1, seen) .. ","
|
||||||
|
end
|
||||||
|
|
||||||
|
if #keys > limit then
|
||||||
|
parts[#parts + 1] = " " .. string.rep(" ", depth)
|
||||||
|
.. "<truncated " .. tostring(#keys - limit) .. " entries>"
|
||||||
|
end
|
||||||
|
|
||||||
|
parts[#parts + 1] = string.rep(" ", depth) .. "}"
|
||||||
|
seen[value] = nil
|
||||||
|
return table.concat(parts, "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function packResults(...)
|
||||||
|
return { n = select("#", ...), ... }
|
||||||
|
end
|
||||||
|
|
||||||
|
local function hasMethod(methods, wanted)
|
||||||
|
for _, method in ipairs(methods) do
|
||||||
|
if method == wanted then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function safeRead(name, method)
|
||||||
|
local result = packResults(pcall(peripheral.call, name, method))
|
||||||
|
if not result[1] then
|
||||||
|
return false, tostring(result[2])
|
||||||
|
end
|
||||||
|
|
||||||
|
local values = { n = result.n - 1 }
|
||||||
|
for index = 2, result.n do
|
||||||
|
values[index - 1] = result[index]
|
||||||
|
end
|
||||||
|
return true, values
|
||||||
|
end
|
||||||
|
|
||||||
|
local function truncate(value)
|
||||||
|
if #value <= MAX_VALUE_LENGTH then
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
return value:sub(1, MAX_VALUE_LENGTH)
|
||||||
|
.. "\n<truncated " .. tostring(#value - MAX_VALUE_LENGTH) .. " characters>"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function typeList(name)
|
||||||
|
local result = packResults(peripheral.getType(name))
|
||||||
|
local types = {}
|
||||||
|
for index = 1, result.n do
|
||||||
|
if result[index] ~= nil then
|
||||||
|
types[#types + 1] = tostring(result[index])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return types
|
||||||
|
end
|
||||||
|
|
||||||
|
local names = peripheral.getNames()
|
||||||
|
table.sort(names)
|
||||||
|
|
||||||
|
emit("ATM10 CREATE FACTORY PERIPHERAL DEBUG")
|
||||||
|
emit("Computer ID: " .. tostring(os.getComputerID()))
|
||||||
|
local computerLabel = os.getComputerLabel()
|
||||||
|
emit("Computer label: " .. tostring(computerLabel or "<not set>"))
|
||||||
|
emit("Uptime timer/clock: " .. tostring(os.clock()))
|
||||||
|
emit("Peripheral count: " .. tostring(#names))
|
||||||
|
emit(string.rep("=", 72))
|
||||||
|
|
||||||
|
-- Only no-argument, read-only methods belong in this list.
|
||||||
|
local readMethods = {
|
||||||
|
"getAddress",
|
||||||
|
"getName",
|
||||||
|
"getSize",
|
||||||
|
"getNetworkId",
|
||||||
|
"getFrequency",
|
||||||
|
"isConnected",
|
||||||
|
"list",
|
||||||
|
"stock",
|
||||||
|
}
|
||||||
|
|
||||||
|
for peripheralIndex, name in ipairs(names) do
|
||||||
|
local methods = peripheral.getMethods(name) or {}
|
||||||
|
table.sort(methods)
|
||||||
|
local types = typeList(name)
|
||||||
|
|
||||||
|
emit("")
|
||||||
|
emit("PERIPHERAL " .. tostring(peripheralIndex) .. "/" .. tostring(#names))
|
||||||
|
emit("Name: " .. name)
|
||||||
|
emit("Type(s): " .. table.concat(types, ", "))
|
||||||
|
emit("Methods (" .. tostring(#methods) .. "): " .. table.concat(methods, ", "))
|
||||||
|
|
||||||
|
for _, method in ipairs(readMethods) do
|
||||||
|
if hasMethod(methods, method) then
|
||||||
|
local ok, value = safeRead(name, method)
|
||||||
|
if ok then
|
||||||
|
emit("READ " .. method .. " OK:")
|
||||||
|
emit(truncate(serialize(value)))
|
||||||
|
else
|
||||||
|
emit("READ " .. method .. " ERROR: " .. value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
emit(string.rep("-", 72))
|
||||||
|
end
|
||||||
|
|
||||||
|
local file = assert(fs.open(OUTPUT_FILE, "w"))
|
||||||
|
file.write(table.concat(lines, "\n"))
|
||||||
|
file.write("\n")
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Debug complete.")
|
||||||
|
print("Created: " .. OUTPUT_FILE)
|
||||||
|
print("Please send this TXT file to Codex.")
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,509 @@
|
|||||||
|
-- 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
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
-- Writes all attached peripheral types, methods and Block Reader data to a file.
|
||||||
|
|
||||||
|
local outputName = "create_debug.txt"
|
||||||
|
local output = assert(fs.open(outputName, "w"))
|
||||||
|
|
||||||
|
local function writeLine(text)
|
||||||
|
text = tostring(text or "")
|
||||||
|
output.writeLine(text)
|
||||||
|
print(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
local names = peripheral.getNames()
|
||||||
|
table.sort(names)
|
||||||
|
|
||||||
|
writeLine("CREATE PERIPHERAL DEBUG")
|
||||||
|
writeLine("Computer ID: " .. os.getComputerID())
|
||||||
|
writeLine("Peripherals: " .. #names)
|
||||||
|
writeLine("")
|
||||||
|
|
||||||
|
for _, name in ipairs(names) do
|
||||||
|
local types = { peripheral.getType(name) }
|
||||||
|
local methods = peripheral.getMethods(name) or {}
|
||||||
|
table.sort(methods)
|
||||||
|
|
||||||
|
writeLine(("[%s]"):format(name))
|
||||||
|
writeLine("Types: " .. table.concat(types, ", "))
|
||||||
|
writeLine("Methods: " .. table.concat(methods, ", "))
|
||||||
|
|
||||||
|
local hasBlockData = false
|
||||||
|
local hasInventoryList = false
|
||||||
|
for _, method in ipairs(methods) do
|
||||||
|
if method == "getBlockData" then hasBlockData = true end
|
||||||
|
if method == "list" then hasInventoryList = true end
|
||||||
|
end
|
||||||
|
|
||||||
|
if hasBlockData then
|
||||||
|
local ok, data = pcall(peripheral.call, name, "getBlockData")
|
||||||
|
writeLine("getBlockData OK: " .. tostring(ok))
|
||||||
|
writeLine(ok and textutils.serialize(data) or tostring(data))
|
||||||
|
end
|
||||||
|
if hasInventoryList then
|
||||||
|
local okSize, size = pcall(peripheral.call, name, "size")
|
||||||
|
local okList, items = pcall(peripheral.call, name, "list")
|
||||||
|
writeLine("Inventory size: " .. (okSize and tostring(size) or "ERROR"))
|
||||||
|
writeLine("Inventory list OK: " .. tostring(okList))
|
||||||
|
writeLine(okList and textutils.serialize(items) or tostring(items))
|
||||||
|
end
|
||||||
|
writeLine("")
|
||||||
|
end
|
||||||
|
|
||||||
|
output.close()
|
||||||
|
print("Fertig: " .. outputName)
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
-- ATM10 Greenhouse inventory diagnostic
|
||||||
|
-- Read-only: detects a connected drawer/storage controller and writes its contents.
|
||||||
|
|
||||||
|
local OUTPUT_FILE = "greenhouse_inventory_debug.txt"
|
||||||
|
local requestedName = ({ ... })[1]
|
||||||
|
|
||||||
|
local function containsMethod(methods, wanted)
|
||||||
|
for _, method in ipairs(methods or {}) do
|
||||||
|
if method == wanted then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function allTypes(name)
|
||||||
|
local types = { peripheral.getType(name) }
|
||||||
|
local result = {}
|
||||||
|
for _, value in ipairs(types) do
|
||||||
|
if type(value) == "string" then result[#result + 1] = value end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function joinedTypes(name)
|
||||||
|
return table.concat(allTypes(name), ", ")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isInventory(name)
|
||||||
|
if peripheral.hasType and peripheral.hasType(name, "inventory") then return true end
|
||||||
|
local methods = peripheral.getMethods(name) or {}
|
||||||
|
return containsMethod(methods, "list") and containsMethod(methods, "size")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function safeCall(name, method, ...)
|
||||||
|
local result = table.pack(pcall(peripheral.call, name, method, ...))
|
||||||
|
if not result[1] then return false, tostring(result[2]) end
|
||||||
|
return true, table.unpack(result, 2, result.n)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function itemCount(contents)
|
||||||
|
local total, occupied = 0, 0
|
||||||
|
for _, item in pairs(contents or {}) do
|
||||||
|
if type(item) == "table" and item.name then
|
||||||
|
total = total + (tonumber(item.count) or 0)
|
||||||
|
occupied = occupied + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return total, occupied
|
||||||
|
end
|
||||||
|
|
||||||
|
local function scoreCandidate(candidate)
|
||||||
|
local text = (candidate.name .. " " .. candidate.types):lower()
|
||||||
|
local score = candidate.occupied
|
||||||
|
if text:find("functionalstorage", 1, true) then score = score + 10000 end
|
||||||
|
if text:find("storage_controller", 1, true) then score = score + 5000 end
|
||||||
|
if text:find("controller", 1, true) then score = score + 2000 end
|
||||||
|
if text:find("drawer", 1, true) then score = score + 1000 end
|
||||||
|
return score
|
||||||
|
end
|
||||||
|
|
||||||
|
local candidates = {}
|
||||||
|
for _, name in ipairs(peripheral.getNames()) do
|
||||||
|
if isInventory(name) then
|
||||||
|
local okList, contents = safeCall(name, "list")
|
||||||
|
if okList and type(contents) == "table" then
|
||||||
|
local total, occupied = itemCount(contents)
|
||||||
|
local okSize, size = safeCall(name, "size")
|
||||||
|
candidates[#candidates + 1] = {
|
||||||
|
name = name,
|
||||||
|
types = joinedTypes(name),
|
||||||
|
contents = contents,
|
||||||
|
total = total,
|
||||||
|
occupied = occupied,
|
||||||
|
size = okSize and tonumber(size) or 0,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(candidates, function(left, right)
|
||||||
|
local leftScore, rightScore = scoreCandidate(left), scoreCandidate(right)
|
||||||
|
if leftScore == rightScore then return left.name < right.name end
|
||||||
|
return leftScore > rightScore
|
||||||
|
end)
|
||||||
|
|
||||||
|
local selected
|
||||||
|
if requestedName then
|
||||||
|
for _, candidate in ipairs(candidates) do
|
||||||
|
if candidate.name == requestedName then selected = candidate break end
|
||||||
|
end
|
||||||
|
if not selected then error("Inventar-Peripheral nicht gefunden: " .. requestedName, 0) end
|
||||||
|
else
|
||||||
|
selected = candidates[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
if not selected then
|
||||||
|
error("Kein verbundenes Inventar gefunden. Wired Modem am Drawer Controller pruefen.", 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
local aggregate = {}
|
||||||
|
for slot, item in pairs(selected.contents) do
|
||||||
|
if type(item) == "table" and item.name then
|
||||||
|
local entry = aggregate[item.name]
|
||||||
|
if not entry then
|
||||||
|
entry = { name = item.name, displayName = item.displayName, count = 0, slots = {} }
|
||||||
|
aggregate[item.name] = entry
|
||||||
|
end
|
||||||
|
entry.count = entry.count + (tonumber(item.count) or 0)
|
||||||
|
entry.slots[#entry.slots + 1] = tonumber(slot) or slot
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local items = {}
|
||||||
|
for _, item in pairs(aggregate) do items[#items + 1] = item end
|
||||||
|
table.sort(items, function(left, right) return left.name < right.name end)
|
||||||
|
|
||||||
|
local function isEssence(item)
|
||||||
|
return item.name:find("_essence", 1, true) ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isSeed(item)
|
||||||
|
return item.name:find("_seeds", 1, true) ~= nil or item.name:find("_seed", 1, true) ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local file = fs.open(OUTPUT_FILE, "w")
|
||||||
|
if not file then error("Kann " .. OUTPUT_FILE .. " nicht schreiben", 0) end
|
||||||
|
|
||||||
|
local function line(text)
|
||||||
|
file.writeLine(tostring(text or ""))
|
||||||
|
end
|
||||||
|
|
||||||
|
line("ATM10 GREENHOUSE INVENTORY DEBUG")
|
||||||
|
line("Computer ID: " .. os.getComputerID())
|
||||||
|
line("Selected peripheral: " .. selected.name)
|
||||||
|
line("Types: " .. selected.types)
|
||||||
|
line("Slots reported: " .. tostring(selected.size))
|
||||||
|
line("Occupied entries: " .. selected.occupied)
|
||||||
|
line("Total items: " .. selected.total)
|
||||||
|
line("")
|
||||||
|
line("INVENTORY CANDIDATES")
|
||||||
|
for index, candidate in ipairs(candidates) do
|
||||||
|
line(string.format("%d. %s | %s | slots=%d occupied=%d items=%d%s",
|
||||||
|
index, candidate.name, candidate.types, candidate.size, candidate.occupied,
|
||||||
|
candidate.total, candidate == selected and " | SELECTED" or ""))
|
||||||
|
end
|
||||||
|
|
||||||
|
line("")
|
||||||
|
line("ESSENCES")
|
||||||
|
local essenceCount = 0
|
||||||
|
for _, item in ipairs(items) do
|
||||||
|
if isEssence(item) then
|
||||||
|
essenceCount = essenceCount + 1
|
||||||
|
line(string.format("%s | count=%d | slots=%s",
|
||||||
|
item.name, item.count, table.concat(item.slots, ",")))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
line("Essence types: " .. essenceCount)
|
||||||
|
|
||||||
|
line("")
|
||||||
|
line("SEEDS")
|
||||||
|
local seedCount = 0
|
||||||
|
for _, item in ipairs(items) do
|
||||||
|
if isSeed(item) then
|
||||||
|
seedCount = seedCount + 1
|
||||||
|
line(string.format("%s | count=%d | slots=%s",
|
||||||
|
item.name, item.count, table.concat(item.slots, ",")))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
line("Seed types: " .. seedCount)
|
||||||
|
|
||||||
|
line("")
|
||||||
|
line("ALL ITEMS")
|
||||||
|
for _, item in ipairs(items) do
|
||||||
|
line(string.format("%s | count=%d | slots=%s",
|
||||||
|
item.name, item.count, table.concat(item.slots, ",")))
|
||||||
|
end
|
||||||
|
line("Unique item types: " .. #items)
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
print("Greenhouse-Dump erstellt:")
|
||||||
|
print(OUTPUT_FILE)
|
||||||
|
print("Controller: " .. selected.name)
|
||||||
|
print("Items: " .. #items .. " | Essenzen: " .. essenceCount .. " | Seeds: " .. seedCount)
|
||||||
|
print("Falls der falsche Controller gewaehlt wurde:")
|
||||||
|
print("greenhouse_inventory_debug <peripheral_name>")
|
||||||
+4
-1
@@ -4,6 +4,10 @@ return {
|
|||||||
channel = "main",
|
channel = "main",
|
||||||
files = {
|
files = {
|
||||||
"startup.lua",
|
"startup.lua",
|
||||||
|
"create_boiler_display.lua",
|
||||||
|
"create_andesite_display.lua",
|
||||||
|
"create_andesite_startup.lua",
|
||||||
|
"create_peripheral_debug.lua",
|
||||||
"core/constants.lua",
|
"core/constants.lua",
|
||||||
"core/log.lua",
|
"core/log.lua",
|
||||||
"core/protocol.lua",
|
"core/protocol.lua",
|
||||||
@@ -26,4 +30,3 @@ return {
|
|||||||
"config/default.lua",
|
"config/default.lua",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
-- ATM10 compact AE2 pattern audit via Advanced Peripherals ME Bridge
|
||||||
|
-- Read-only: does not craft, export, import, cancel or modify patterns.
|
||||||
|
|
||||||
|
local VERSION = "1.1.0"
|
||||||
|
local OUTPUT_FILE = "me_pattern_debug.txt"
|
||||||
|
local SAMPLE_COUNT = 3
|
||||||
|
local SAMPLE_LIMIT = 8000
|
||||||
|
local LINE_LIMIT = 500
|
||||||
|
|
||||||
|
local bridgeName
|
||||||
|
local function hasType(name, wanted)
|
||||||
|
if peripheral.hasType then return peripheral.hasType(name, wanted) end
|
||||||
|
return peripheral.getType(name) == wanted
|
||||||
|
end
|
||||||
|
for _, name in ipairs(peripheral.getNames()) do
|
||||||
|
if hasType(name, "me_bridge") then bridgeName = name break end
|
||||||
|
end
|
||||||
|
if not bridgeName then error("Keine ME Bridge am Computer gefunden", 0) end
|
||||||
|
|
||||||
|
local function sortedKeys(value)
|
||||||
|
local keys = {}
|
||||||
|
for key in pairs(value or {}) do keys[#keys + 1] = key end
|
||||||
|
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
||||||
|
return keys
|
||||||
|
end
|
||||||
|
|
||||||
|
local function compact(value, depth, seen)
|
||||||
|
depth, seen = depth or 0, seen or {}
|
||||||
|
local kind = type(value)
|
||||||
|
if kind == "string" then return string.format("%q", value) end
|
||||||
|
if kind ~= "table" then return tostring(value) end
|
||||||
|
if seen[value] then return "<cycle>" end
|
||||||
|
if depth >= 8 then return "<max-depth>" end
|
||||||
|
seen[value] = true
|
||||||
|
local parts = {}
|
||||||
|
for _, key in ipairs(sortedKeys(value)) do
|
||||||
|
parts[#parts + 1] = "[" .. compact(key, depth + 1, seen) .. "]="
|
||||||
|
.. compact(value[key], depth + 1, seen)
|
||||||
|
end
|
||||||
|
seen[value] = nil
|
||||||
|
return "{" .. table.concat(parts, ",") .. "}"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function truncate(value, limit)
|
||||||
|
value, limit = tostring(value), limit or LINE_LIMIT
|
||||||
|
if #value <= limit then return value end
|
||||||
|
return value:sub(1, limit) .. "...<" .. tostring(#value - limit) .. " chars>"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function callRead(method, ...)
|
||||||
|
local result = table.pack(pcall(peripheral.call, bridgeName, method, ...))
|
||||||
|
if not result[1] then return false, tostring(result[2]) end
|
||||||
|
if result.n == 2 then return true, result[2] end
|
||||||
|
local values = { n = result.n - 1 }
|
||||||
|
for index = 2, result.n do values[index - 1] = result[index] end
|
||||||
|
return true, values
|
||||||
|
end
|
||||||
|
|
||||||
|
local function listValues(value)
|
||||||
|
local result = {}
|
||||||
|
if type(value) ~= "table" then return result end
|
||||||
|
for key, entry in pairs(value) do
|
||||||
|
if key ~= "n" and type(entry) == "table" then result[#result + 1] = entry end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function first(value, keys)
|
||||||
|
if type(value) ~= "table" then return nil end
|
||||||
|
for _, key in ipairs(keys) do
|
||||||
|
if value[key] ~= nil then return value[key] end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function itemName(value)
|
||||||
|
if type(value) == "string" then return value end
|
||||||
|
if type(value) ~= "table" then return nil end
|
||||||
|
local name = first(value, { "name", "id", "item", "itemId", "key" })
|
||||||
|
if type(name) == "string" then return name end
|
||||||
|
if type(name) == "table" then return itemName(name) end
|
||||||
|
local what = first(value, { "what", "stack", "resource" })
|
||||||
|
if what ~= nil and what ~= value then return itemName(what) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function itemToken(value)
|
||||||
|
local name = itemName(value)
|
||||||
|
if not name then return truncate(compact(value), 180) end
|
||||||
|
local count = type(value) == "table" and first(value, { "count", "amount", "quantity", "size" }) or nil
|
||||||
|
local token = name .. " x" .. tostring(tonumber(count) or 1)
|
||||||
|
if type(value) == "table" then
|
||||||
|
local components = first(value, { "components", "nbt", "tag" })
|
||||||
|
if components ~= nil then token = token .. " " .. truncate(compact(components), 120) end
|
||||||
|
end
|
||||||
|
return token
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sideEntries(side)
|
||||||
|
if side == nil then return {} end
|
||||||
|
if type(side) ~= "table" or itemName(side) then return { itemToken(side) } end
|
||||||
|
local entries = {}
|
||||||
|
for _, key in ipairs(sortedKeys(side)) do
|
||||||
|
if key ~= "n" then entries[#entries + 1] = tostring(key) .. ":" .. itemToken(side[key]) end
|
||||||
|
end
|
||||||
|
return entries
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sideText(side)
|
||||||
|
local entries = sideEntries(side)
|
||||||
|
return #entries > 0 and table.concat(entries, " + ") or "-"
|
||||||
|
end
|
||||||
|
|
||||||
|
local INPUT_KEYS = { "inputs", "input", "ingredients" }
|
||||||
|
local OUTPUT_KEYS = { "outputs", "output", "results", "result" }
|
||||||
|
local function patternInputs(pattern) return first(pattern, INPUT_KEYS) end
|
||||||
|
local function patternOutputs(pattern) return first(pattern, OUTPUT_KEYS) end
|
||||||
|
|
||||||
|
local function patternType(pattern)
|
||||||
|
local value = first(pattern, { "type", "patternType", "crafting", "processing", "isCrafting" })
|
||||||
|
return value == nil and "unknown" or tostring(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function providerText(pattern)
|
||||||
|
local value = first(pattern, {
|
||||||
|
"provider", "providerName", "patternProvider", "machine", "location", "pos", "position"
|
||||||
|
})
|
||||||
|
return value == nil and "<not exposed>" or truncate(compact(value), 180)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function exactFingerprint(pattern)
|
||||||
|
return patternType(pattern) .. "|IN=" .. table.concat(sideEntries(patternInputs(pattern)), "|")
|
||||||
|
.. "|OUT=" .. table.concat(sideEntries(patternOutputs(pattern)), "|")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function outputFingerprint(pattern)
|
||||||
|
return table.concat(sideEntries(patternOutputs(pattern)), "|")
|
||||||
|
end
|
||||||
|
|
||||||
|
local okPatterns, rawPatterns = callRead("getPatterns")
|
||||||
|
if not okPatterns then okPatterns, rawPatterns = callRead("getPatterns", {}) end
|
||||||
|
if not okPatterns then error("getPatterns fehlgeschlagen: " .. tostring(rawPatterns), 0) end
|
||||||
|
local patterns = listValues(rawPatterns)
|
||||||
|
|
||||||
|
local exactGroups, outputGroups = {}, {}
|
||||||
|
for index, pattern in ipairs(patterns) do
|
||||||
|
local exact, output = exactFingerprint(pattern), outputFingerprint(pattern)
|
||||||
|
exactGroups[exact] = exactGroups[exact] or {}
|
||||||
|
exactGroups[exact][#exactGroups[exact] + 1] = index
|
||||||
|
if output ~= "" then
|
||||||
|
outputGroups[output] = outputGroups[output] or {}
|
||||||
|
outputGroups[output][#outputGroups[output] + 1] = index
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function duplicatesOf(groups)
|
||||||
|
local duplicates = {}
|
||||||
|
for signature, indexes in pairs(groups) do
|
||||||
|
if #indexes > 1 then duplicates[#duplicates + 1] = { signature = signature, indexes = indexes } end
|
||||||
|
end
|
||||||
|
table.sort(duplicates, function(a, b)
|
||||||
|
if #a.indexes == #b.indexes then return a.signature < b.signature end
|
||||||
|
return #a.indexes > #b.indexes
|
||||||
|
end)
|
||||||
|
return duplicates
|
||||||
|
end
|
||||||
|
|
||||||
|
local exactDuplicates = duplicatesOf(exactGroups)
|
||||||
|
local outputDuplicates = duplicatesOf(outputGroups)
|
||||||
|
if fs.exists(OUTPUT_FILE) then fs.delete(OUTPUT_FILE) end
|
||||||
|
local file = assert(fs.open(OUTPUT_FILE, "w"))
|
||||||
|
local function write(value) file.writeLine(tostring(value)) end
|
||||||
|
|
||||||
|
write("ATM10 AE2 COMPACT PATTERN AUDIT V" .. VERSION)
|
||||||
|
write("Computer ID: " .. tostring(os.getComputerID()))
|
||||||
|
write("Computer label: " .. tostring(os.getComputerLabel() or "<not set>"))
|
||||||
|
write("ME Bridge: " .. bridgeName)
|
||||||
|
write("Pattern records: " .. tostring(#patterns))
|
||||||
|
write("Exact duplicate groups: " .. tostring(#exactDuplicates))
|
||||||
|
write("Same-output groups: " .. tostring(#outputDuplicates))
|
||||||
|
|
||||||
|
write("")
|
||||||
|
write("EXACT DUPLICATES")
|
||||||
|
write(string.rep("=", 72))
|
||||||
|
if #exactDuplicates == 0 then write("None") end
|
||||||
|
for groupIndex, group in ipairs(exactDuplicates) do
|
||||||
|
write("GROUP " .. groupIndex .. " | copies=" .. #group.indexes
|
||||||
|
.. " | indexes=" .. table.concat(group.indexes, ","))
|
||||||
|
write(truncate(group.signature))
|
||||||
|
for _, index in ipairs(group.indexes) do write(" #" .. index .. " provider=" .. providerText(patterns[index])) end
|
||||||
|
end
|
||||||
|
|
||||||
|
write("")
|
||||||
|
write("SAME OUTPUT (CHECK MANUALLY; MAY BE INTENTIONAL)")
|
||||||
|
write(string.rep("=", 72))
|
||||||
|
if #outputDuplicates == 0 then write("None") end
|
||||||
|
for groupIndex, group in ipairs(outputDuplicates) do
|
||||||
|
write("GROUP " .. groupIndex .. " | recipes=" .. #group.indexes
|
||||||
|
.. " | indexes=" .. table.concat(group.indexes, ",")
|
||||||
|
.. " | output=" .. truncate(group.signature, 220))
|
||||||
|
end
|
||||||
|
|
||||||
|
write("")
|
||||||
|
write("COMPACT PATTERN INDEX")
|
||||||
|
write(string.rep("=", 72))
|
||||||
|
for index, pattern in ipairs(patterns) do
|
||||||
|
write("#" .. index .. " | " .. patternType(pattern)
|
||||||
|
.. " | IN " .. truncate(sideText(patternInputs(pattern)), 220)
|
||||||
|
.. " | OUT " .. truncate(sideText(patternOutputs(pattern)), 220)
|
||||||
|
.. " | PROVIDER " .. providerText(pattern))
|
||||||
|
end
|
||||||
|
|
||||||
|
write("")
|
||||||
|
write("RAW SCHEMA SAMPLES")
|
||||||
|
write(string.rep("=", 72))
|
||||||
|
for index = 1, math.min(SAMPLE_COUNT, #patterns) do
|
||||||
|
write("SAMPLE #" .. index)
|
||||||
|
write(truncate(compact(patterns[index]), SAMPLE_LIMIT))
|
||||||
|
end
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
print("Kompakter ME Pattern Audit abgeschlossen.")
|
||||||
|
print("Pattern: " .. tostring(#patterns))
|
||||||
|
print("Exakte Duplikat-Gruppen: " .. tostring(#exactDuplicates))
|
||||||
|
print("Gleicher Output: " .. tostring(#outputDuplicates))
|
||||||
|
print("Erstellt: " .. OUTPUT_FILE)
|
||||||
|
print("Bitte danach Codex 'fertig' schreiben.")
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$Remote = "origin",
|
||||||
|
[string]$Branch = "main"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$HostName = "git.peli-server.de"
|
||||||
|
$Repository = $PSScriptRoot
|
||||||
|
|
||||||
|
function Invoke-Git {
|
||||||
|
param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Arguments)
|
||||||
|
|
||||||
|
& git -C $Repository @Arguments
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Git-Befehl fehlgeschlagen: git $($Arguments -join ' ')"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
||||||
|
throw "Git wurde nicht gefunden. Installiere Git for Windows oder fuege es zum PATH hinzu."
|
||||||
|
}
|
||||||
|
|
||||||
|
$remoteUrl = (& git -C $Repository remote get-url $Remote 2>$null)
|
||||||
|
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($remoteUrl)) {
|
||||||
|
throw "Remote '$Remote' wurde im Repository nicht gefunden."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($remoteUrl -notmatch [regex]::Escape($HostName)) {
|
||||||
|
throw "Sicherheitsabbruch: Remote '$Remote' zeigt nicht auf $HostName ($remoteUrl)."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Gitea-Anmeldung fuer $HostName wird erneuert." -ForegroundColor Cyan
|
||||||
|
Write-Host "Der alte Windows-Credential-Manager-Eintrag wird jetzt entfernt."
|
||||||
|
|
||||||
|
$credentialQuery = "protocol=https`nhost=$HostName`n`n"
|
||||||
|
$credentialQuery | & git credential reject
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Der alte Git-Credential-Eintrag konnte nicht entfernt werden."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Beim folgenden Push sollte Git Credential Manager nach der Anmeldung fragen." -ForegroundColor Yellow
|
||||||
|
Write-Host "Benutzername: dein Gitea-Benutzername"
|
||||||
|
Write-Host "Passwort: dein Gitea-Passwort oder besser ein Personal Access Token"
|
||||||
|
Write-Host "Das Passwort/Token niemals hier im Chat einfuegen."
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
$env:GCM_INTERACTIVE = "always"
|
||||||
|
Invoke-Git push --set-upstream $Remote $Branch
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Anmeldung gespeichert und Push erfolgreich." -ForegroundColor Green
|
||||||
|
Write-Host "Kuenftige Pushes aus Codex sollten wieder ohne erneute Anmeldung funktionieren."
|
||||||
Reference in New Issue
Block a user