Compare commits

...

37 Commits

Author SHA1 Message Date
Dystroyer8 60cbaccb8e Automate Blaze Cake keep stock 2026-07-20 19:07:35 +02:00
Dystroyer8 2ccf008f7f Make ME pattern audit compact 2026-07-20 18:23:41 +02:00
Dystroyer8 b6f49cb6bb Add read-only ME pattern duplicate audit 2026-07-20 17:51:57 +02:00
Dystroyer8 408ced35ea Limit Blaze Cake batches to egg stack size 2026-07-20 17:22:20 +02:00
Dystroyer8 c780a4ec92 Add Blaze Cake basin production chain 2026-07-20 17:03:32 +02:00
Dystroyer8 9e8fbd17ed Add greenhouse drawer inventory diagnostic 2026-07-19 22:26:17 +02:00
Dystroyer8 76e734f6ba Add tag based ore substitution 2026-07-19 01:14:00 +02:00
Dystroyer8 49e4363cd9 Fix unified ATM10 brass ingot ID 2026-07-19 00:57:47 +02:00
Dystroyer8 1480119c8e Add protected ME fallback for Create inputs 2026-07-18 22:37:19 +02:00
Dystroyer8 090fec482a Synchronize dependency output handoff 2026-07-18 21:37:39 +02:00
Dystroyer8 5c3b092727 Let Precision use its local component buffers 2026-07-18 21:23:19 +02:00
Dystroyer8 eee51ee05a Add recursive factory dependencies and press pool 2026-07-18 21:03:43 +02:00
Dystroyer8 c963557649 Add casing recipes and automatic component stock 2026-07-18 20:26:24 +02:00
Dystroyer8 a349ca92f0 Add multi-input Andesite Alloy mixer jobs 2026-07-18 17:45:41 +02:00
Dystroyer8 756957ece6 Advance Zinc jobs early and show vault capacity 2026-07-18 04:07:51 +02:00
Dystroyer8 2476e7b78f Track actual scheduler output arrivals 2026-07-18 03:42:14 +02:00
Dystroyer8 10d96bba81 Add persistent Create factory scheduler V1 2026-07-18 03:31:27 +02:00
Dystroyer8 8a14c96534 Add Zinc washing test and large stock page controls 2026-07-18 02:36:22 +02:00
Dystroyer8 d4f18113a6 Show dynamic paged Stock Ticker inventory 2026-07-18 02:18:31 +02:00
Dystroyer8 d23a39dbfa Lock Create jobs until their outputs return 2026-07-18 01:49:08 +02:00
Dystroyer8 b0e9b3ed23 Add first Create logistics test controller 2026-07-18 01:19:42 +02:00
Dystroyer8 3245afa156 Handle unlabeled CC computers in factory debug 2026-07-18 01:05:05 +02:00
Dystroyer8 741fad1f22 Keep factory debug output off the monitor 2026-07-18 00:57:20 +02:00
Dystroyer8 1f61140498 Add Create factory peripheral discovery dump 2026-07-18 00:54:46 +02:00
Dystroyer8 661becfb45 Scope Andesite display to local production data 2026-07-17 01:52:27 +02:00
Dystroyer8 03a88fb2c5 Synchronize stress readings after clutch changes 2026-07-17 01:43:07 +02:00
Dystroyer8 5a24e96217 Fix direct clutch output polarity 2026-07-17 01:38:49 +02:00
Dystroyer8 17ed31c60c Fix Andesite vault discovery and monitor scaling 2026-07-17 01:25:34 +02:00
Dystroyer8 8ea978196e Add Andesite Alloy factory controller 2026-07-17 01:19:19 +02:00
Dystroyer8 a0302d764d Update relocated third boiler position 2026-07-16 23:26:15 +02:00
Dystroyer8 28b3dc0494 Persist and resume Create production state 2026-07-16 21:46:33 +02:00
Dystroyer8 b347855349 Stabilize boiler mapping and clutch fail-safe 2026-07-16 21:45:00 +02:00
Dystroyer8 484b6a5117 Add touch control for test clutch 2026-07-16 21:24:51 +02:00
Dystroyer8 2eea78980e Detect idle Create boilers without water 2026-07-16 21:18:36 +02:00
Dystroyer8 543385a880 Read Create gauges through block readers 2026-07-16 21:05:47 +02:00
Dystroyer8 d586491a07 Improve Create peripheral detection and add diagnostics 2026-07-16 21:00:29 +02:00
Dystroyer8 aac7e4f52d Adapt boiler display for shared main shaft 2026-07-16 20:50:42 +02:00
12 changed files with 3073 additions and 119 deletions
+192 -8
View File
@@ -2,6 +2,136 @@
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
- zentrale Leitwarte fuer Status, Energie, Alarme und Sektor-Hauptschalter
@@ -21,20 +151,74 @@ werden ergaenzt, sobald die Peripherie- und Methodennamen der gebauten Anlagen f
## Einfache Create-Boiler-Anzeige
Fuer die erste Kraftwerksanzeige werden ein Monitor, pro Boiler ein Create-Speedometer,
ein Create-Stressometer an der Hauptwelle und optional pro Boiler ein Block Reader aus
Advanced Peripherals ueber Wired Modems mit dem Computer verbunden. Installation und Start:
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 zeigt pro Boiler Online-Status, RPM und - mit Block Reader - den Heizmodus.
Zusaetzlich zeigt es Kapazitaet, Verbrauch und Auslastung der Hauptwelle. Die Zuordnung erfolgt
nach den internen Peripheral-Namen; Speedometer und Block Reader sollten deshalb jeweils in der
Reihenfolge Boiler 1 bis 3 an das Wired Network angeschlossen werden. Jeder Block Reader muss
auf den Controller-Tank des zugeordneten Boiler-Multiblocks zeigen.
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
+344
View File
@@ -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
+8
View File
@@ -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")
+184 -106
View File
@@ -1,25 +1,63 @@
-- Create boiler status board for CC:Tweaked.
-- Hardware:
-- * one monitor
-- * one Create Speedometer per boiler
-- * one Create Stressometer on the main shaft
-- * optionally one Advanced Peripherals Block Reader per boiler controller
-- Everything must be connected to the computer, preferably using Wired Modems.
-- All Create values are read through Advanced Peripherals Block Readers.
local EXPECTED_BOILERS = 3
local REFRESH_SECONDS = 1
local MONITOR_SCALE = 0.5
local SPEEDOMETER_TYPE = "Create_Speedometer"
local STRESSOMETER_TYPE = "Create_Stressometer"
local BLOCK_READER_TYPE = "block_reader"
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 anschliessen oder per Wired Modem verbinden.", 0)
error("Kein Monitor gefunden. Monitor direkt oder per Wired Modem verbinden.", 0)
end
monitor.setTextScale(MONITOR_SCALE)
monitor.setCursorBlink(false)
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()
@@ -34,17 +72,6 @@ local function center(y, text, foreground, background)
monitor.write(text)
end
local function getPeripheralsByType(wantedType)
local names = {}
for _, name in ipairs(peripheral.getNames()) do
if peripheral.getType(name) == wantedType then
names[#names + 1] = name
end
end
table.sort(names)
return names
end
local function getValueIgnoringCase(data, wantedKeys)
if type(data) ~= "table" then return nil end
for key, value in pairs(data) do
@@ -56,65 +83,84 @@ local function getValueIgnoringCase(data, wantedKeys)
return nil
end
local function findBoilerData(data, visited)
if type(data) ~= "table" then return nil end
visited = visited or {}
if visited[data] then return nil end
visited[data] = true
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" })
if type(boiler) == "table" then return boiler end
local network = getValueIgnoringCase(data, { "Network" })
local speed = getValueIgnoringCase(data, { "Speed" })
local heat = getValueIgnoringCase(data, { "Heat", "activeHeat", "actualHeat" })
local passive = getValueIgnoringCase(data, { "PassiveHeat", "passive" })
if heat ~= nil or passive ~= nil then return data end
for _, value in pairs(data) do
local found = findBoilerData(value, visited)
if found then return found end
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
return nil
end
end
end
return boilers, networks
end
local function readHeatMode(readerName)
if not readerName then return "UNKNOWN" end
local ok, data = pcall(peripheral.call, readerName, "getBlockData")
if not ok then return "ERROR" end
local function readBoiler(entry)
if not entry then return "MISSING", "NO READER", colors.red end
local boiler = findBoilerData(data)
if not boiler then return "UNKNOWN" 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 passive = getValueIgnoringCase(boiler, { "PassiveHeat", "passive" })
local heat = tonumber(getValueIgnoringCase(boiler,
{ "Heat", "activeHeat", "actualHeat", "boilerLevel", "level" }))
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
if passive == true or passive == 1 then return "CAMPFIRE" end
if heat and heat >= 18 then return "SUPERHEATED" end
if heat and heat > 0 then return "HEATED" end
return "COLD"
-- 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 readBoiler(name, readerName)
if not name then
return "MISSING", 0, "UNKNOWN", colors.red
end
local ok, speed = pcall(peripheral.call, name, "getSpeed")
if not ok or type(speed) ~= "number" then
return "ERROR", 0, "UNKNOWN", colors.red
end
speed = math.abs(speed)
if speed < 0.5 then
return "STANDBY", 0, readHeatMode(readerName), colors.orange
end
return "ONLINE", speed, readHeatMode(readerName), colors.lime
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 = tostring(value)
local parts = {}
local digits, parts = tostring(value), {}
while #digits > 3 do
table.insert(parts, 1, digits:sub(-3))
digits = digits:sub(1, -4)
@@ -123,61 +169,69 @@ local function formatInteger(value)
return table.concat(parts, ".")
end
local function readMainShaft(stressometerName)
if not stressometerName then return nil, nil, nil end
local okCapacity, capacity = pcall(peripheral.call,
stressometerName, "getNetworkCapacity")
local okUsage, usage = pcall(peripheral.call,
stressometerName, "getNetworkStress")
if not okCapacity or not okUsage then return nil, nil, nil end
capacity = tonumber(capacity) or 0
usage = tonumber(usage) or 0
local load = capacity > 0 and usage / capacity * 100 or 0
return capacity, usage, load
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 speedometers = getPeripheralsByType(SPEEDOMETER_TYPE)
local stressometers = getPeripheralsByType(STRESSOMETER_TYPE)
local readers = getPeripheralsByType(BLOCK_READER_TYPE)
local boilers, networks = discoverReaders()
local speed, capacity, usage, load = readMainShaft(networks[1])
monitor.setBackgroundColor(colors.black)
monitor.clear()
center(1, "CREATE KRAFTWERK", colors.cyan)
local firstRow = 3
for index = 1, EXPECTED_BOILERS do
local state, speed, heatMode, color = readBoiler(speedometers[index], readers[index])
local row = firstRow + index - 1
if row <= height then
center(row, ("BOILER %d %-7s %5.1f RPM %s")
:format(index, state, speed, heatMode), color)
end
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
local capacity, usage, load = readMainShaft(stressometers[1])
if height >= 7 then center(7, "MAIN SHAFT", colors.cyan) end
if capacity then
if height >= 8 then center(8, "Capacity: " .. formatInteger(capacity) .. " SU", colors.white) end
if height >= 9 then center(9, "Usage: " .. formatInteger(usage) .. " SU", colors.white) end
if height >= 10 then
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(10, loadText, load >= 90 and colors.red or load >= 70 and colors.orange or colors.lime)
end
center(11, loadText, load >= 90 and colors.red or load >= 70 and colors.orange or colors.lime)
else
if height >= 8 then center(8, "Stressometer nicht gefunden", colors.red) end
end
if height >= 12 then
center(height, "Nur Anzeige - keine Steuerung", colors.gray)
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
while true do
local function safeRender()
local ok, reason = pcall(render)
if not ok then
monitor.setBackgroundColor(colors.black)
@@ -185,5 +239,29 @@ while true do
center(1, "ANZEIGEFEHLER", colors.red)
center(3, tostring(reason), colors.orange)
end
sleep(REFRESH_SECONDS)
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
+10
View File
@@ -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")
+170
View File
@@ -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
+509
View File
@@ -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
+52
View File
@@ -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)
+184
View File
@@ -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>")
+3
View File
@@ -5,6 +5,9 @@ return {
files = {
"startup.lua",
"create_boiler_display.lua",
"create_andesite_display.lua",
"create_andesite_startup.lua",
"create_peripheral_debug.lua",
"core/constants.lua",
"core/log.lua",
"core/protocol.lua",
+225
View File
@@ -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.")