Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --[[ ================================================================
- Hinweis: rednet.send(id,msg) == modem.transmit(channel=id, replyCh=eigeneID, msg)
- broadcast == Kanal 65535 ; GPS == Kanal 65534
- ================================================================ ]]--
- ------------------------------------------------------------------
- -- Forward-Deklarationen (ein Upvalue je Modul/Helfer)
- ------------------------------------------------------------------
- local CONFIG, STATE
- local radio, recon, attack, guard, ui, input, scout
- local now, logMsg, gameClock, randstr, truncate, cntChannels, safeNum
- local eventLoop, attackLoop, guardLoop, uiLoop, scoutLoop, planLoop
- local setupWizard, main
- ------------------------------------------------------------------
- -- Konfiguration (zur Laufzeit ueber Tasten anpassbar)
- ------------------------------------------------------------------
- CONFIG = {
- useHttp = true,
- timeUrls = {
- "http://worldtimeapi.org/api/timezone/Etc/UTC.txt",
- "http://worldtimeapi.org/api/ip.txt",
- },
- tpsWindow = 15, -- Spiel-Sekunden je TPS-Messfenster
- tpsFloor = 10, -- unter diesem Wert gilt als Lag
- tpsLowDuration = 120, -- Sekunden anhaltend -> drosseln
- throttleRate = 3, -- Nachrichten/s waehrend Drossel
- baseRate = 25, -- Nachrichten/s Standard-Budget
- floodEnabled = true,
- observeSecs = 2, -- kurz beobachten ...
- spoofSecs = 3, -- ... kurz spoofen ...
- fuzzSecs = 6, -- ... dann Fuzzing (Crash-Vektor) + Flood -> schnell zerstoeren
- downSilence = 15, -- s Stille nach Angriff -> Absturz-Verdacht
- baselineMsgs = 3, -- ab so vielen Nachrichten gilt Knoten als etabliert
- attackTick = 0.1,
- uiRefresh = 0.3,
- planInterval = 1, -- s zwischen Governor-Takt/Kanal-Plan (schnelle Reaktion)
- autostart = false, -- Kampagne startet erst per [S]
- -- EMPFANG bewusst sparsam + rate-geregelt halten. Ein Modem, das viele
- -- Kanaele (v.a. Broadcast 65535 / GPS 65534) offen haelt, empfaengt auf
- -- einem vollen Server JEDE Funknachricht -> Event-Flut, die den Server-/
- -- Client-Thread einfrieren kann. Deshalb Slow-Start + Rate-Governor:
- maxOpenChannels = 48, -- Obergrenze gleichzeitig offener Kanaele
- startChannels = 8, -- Slow-Start: so wenige Kanaele zu Beginn
- maxRxPerSec = 120, -- Empfangs-Budget; darueber Kanaele/Broadcast abwerfen
- panicRxPerSec = 350, -- Notbremse: fast alles schliessen
- openGps = false,-- GPS-Kanal 65534 ist eine Dauerflut -> standardmaessig AUS
- autoBroadcast = true, -- Broadcast 65535 bei ruhiger Lage DAUERHAFT offen halten (nur so wird viel entdeckt; Notbremse schuetzt vor Flut). false = Broadcast nur per [B]-Taste
- bcastCooldown = 15, -- s Abkuehlzeit nach Ueberlast, bevor Broadcast wieder oeffnet (verhindert Flattern)
- sweepWindow = 3, -- Kanaele je Sweep-Fenster (klein halten)
- lowFill = 8, -- niedrige IDs 0..lowFill-1 als Catch-all
- probeMax = 40, -- Kanal-Limit-Probe (klein halten)
- rosterCap = 400, -- max. Knoten im Roster (gegen Spoof-ID-Flut)
- panicRxBurst = 200, -- Ereignis-Notbremse: so viele Nachr. seit letztem Governor-Reset -> Broadcast SOFORT zu
- panicFloor = 6, -- Kanal-Budget nach Notbremse
- floodBroadcast = false,-- Flood auch auf Broadcast 65535? (server-weite Fan-out-Last -> Standard AUS)
- }
- ------------------------------------------------------------------
- -- Laufzeit-Zustand
- ------------------------------------------------------------------
- STATE = {
- myId = 0,
- running = false,
- campaignOn = false,
- attacksPaused = false,
- aggression = "adaptive", -- "stealth" | "adaptive" | "aggressive"
- log = {},
- roster = {}, -- id -> node
- nodeIds = {}, -- array aller bekannten ids
- whitelist = {}, -- id -> true (nicht angreifen)
- targets = {}, -- id -> attack-state
- displays = {},
- viewList = {},
- selIndex = 1,
- rr = 0,
- rxCount = 0,
- rxWindow = 0,
- rxGov = 0, -- Zaehler fuer den Empfangs-Governor
- overloaded = false, -- Empfangs-Ueberlast erkannt?
- emergencyTripped = false, -- Ereignis-Notbremse bereits ausgeloest?
- chOpen = 0, -- aktuell offene Kanaele
- sentCount = 0,
- sendErr = 0,
- nodeCount = 0,
- killConfirmed = 0,
- maxDist = nil,
- }
- ------------------------------------------------------------------
- -- Helfer
- ------------------------------------------------------------------
- now = function() return os.clock() end -- tick-basierte Spielzeit-Sekunden
- logMsg = function(s)
- local L = STATE.log
- L[#L + 1] = "[" .. gameClock() .. "] " .. tostring(s)
- while #L > 200 do table.remove(L, 1) end
- end
- gameClock = function()
- local ok, t = pcall(os.time) -- 0..24 (Stunden als Float)
- if not ok or type(t) ~= "number" then return "--:--" end
- local h = math.floor(t)
- local m = math.floor((t - h) * 60)
- if m > 59 then m = 59 end
- return string.format("%02d:%02d", h, m)
- end
- randstr = function(n)
- n = n or math.random(3, 10)
- local chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
- local t = {}
- for i = 1, n do
- local k = math.random(#chars)
- t[i] = string.sub(chars, k, k)
- end
- return table.concat(t)
- end
- truncate = function(s, n)
- s = tostring(s)
- if #s > n then return string.sub(s, 1, n - 1) .. "~" end
- return s
- end
- cntChannels = function(nd)
- local c = 0
- for _ in pairs(nd.channels) do c = c + 1 end
- return c
- end
- safeNum = function(v, d)
- if type(v) == "number" then return v end
- return d
- end
- ------------------------------------------------------------------
- -- GUARD : Rate-Limiter (Token-Bucket) + TPS-Messung
- ------------------------------------------------------------------
- guard = {
- rate = CONFIG.baseRate,
- baseRate = CONFIG.baseRate,
- cap = CONFIG.baseRate,
- tokens = CONFIG.baseRate,
- last = 0,
- mode = "init", -- "http" | "selflimit"
- tps = nil,
- lowElapsedReal = 0, -- ECHT-Sekunden mit TPS unter Grenze (nur HTTP-Modus)
- stableWindows = 0, -- stabile Fenster (Selbst-Limit-Freigabe)
- throttled = false,
- baseline = nil,
- }
- guard.refill = function()
- local t = now()
- local dt = t - guard.last
- if dt < 0 then dt = 0 end
- guard.cap = math.max(2, guard.rate)
- guard.tokens = math.min(guard.cap, guard.tokens + dt * guard.rate)
- guard.last = t
- end
- -- blockiert bis ein Token frei ist (respektiert Pause)
- guard.acquire = function()
- while STATE.running do
- guard.refill()
- if not STATE.attacksPaused and guard.tokens >= 1 then
- guard.tokens = guard.tokens - 1
- return true
- end
- local wait
- if STATE.attacksPaused then
- wait = 0.2
- else
- local deficit = 1 - guard.tokens
- wait = deficit / math.max(guard.rate, 0.01)
- if wait < 0.05 then wait = 0.05 end
- if wait > 1 then wait = 1 end
- end
- sleep(wait)
- end
- return false
- end
- guard.fetchRealTime = function()
- if not http then return nil end
- for _, url in ipairs(CONFIG.timeUrls) do
- local ok, h = pcall(http.get, url)
- if ok and h then
- local body
- pcall(function() body = h.readAll() end)
- pcall(function() h.close() end)
- if type(body) == "string" then
- local n = string.match(body, "unixtime[\"'%s:]*(%d+)")
- if not n then n = string.match(body, "(%d%d%d%d%d%d%d%d%d%d+)") end
- n = tonumber(n)
- if n and n > 1400000000 and n < 4000000000 then return n end
- end
- end
- end
- return nil
- end
- guard.detect = function()
- if http and CONFIG.useHttp then
- local t = guard.fetchRealTime()
- if t then
- guard.mode = "http"
- logMsg("Guard: HTTP TPS-Modus aktiv")
- return
- end
- end
- guard.mode = "selflimit"
- logMsg("Guard: Selbst-Limit-Modus (kein/kein-nutzbares HTTP)")
- end
- -- realEl = echte Sekunden seit der letzten Messung (aus HTTP-Zeitdifferenz).
- -- Wichtig: os.clock ist tick-basiert und laeuft unter Lag langsamer als die
- -- Echtzeit; die "120s"-Regel MUSS daher in Echt-Sekunden zaehlen, sonst
- -- triggert die Drossel bei starkem Lag viel zu spaet.
- guard.applyRule = function(tps, realEl)
- if tps < CONFIG.tpsFloor then
- guard.lowElapsedReal = guard.lowElapsedReal + math.max(0, realEl or 0)
- if guard.lowElapsedReal >= CONFIG.tpsLowDuration and not guard.throttled then
- guard.throttled = true
- guard.rate = CONFIG.throttleRate
- logMsg(string.format("TPS<%.0f seit >%ds (echt) -> DROSSEL (rate=%.0f/s)",
- CONFIG.tpsFloor, CONFIG.tpsLowDuration, guard.rate))
- end
- else
- guard.lowElapsedReal = 0
- if guard.throttled then
- guard.throttled = false
- guard.rate = guard.baseRate
- logMsg("TPS stabil -> Drossel geloest")
- end
- end
- end
- -- Fallback ohne HTTP: keine echte TPS. Wir beobachten stattdessen, ob der
- -- gesamte Funkverkehr einbricht (indirektes Lag-/Ausfall-Symptom). Freigabe
- -- ueber eine Zahl stabiler Fenster statt einer (unter Lag verzerrten) Uhr.
- guard.selfHeuristic = function()
- local rate = STATE.rxWindow / math.max(CONFIG.tpsWindow, 1)
- STATE.rxWindow = 0
- if not guard.baseline then
- guard.baseline = rate
- else
- guard.baseline = guard.baseline * 0.7 + rate * 0.3
- end
- if STATE.campaignOn and not STATE.attacksPaused
- and guard.baseline > 1 and rate < guard.baseline * 0.3 then
- guard.stableWindows = 0
- if not guard.throttled then
- guard.throttled = true
- guard.rate = CONFIG.throttleRate
- logMsg("Selbst-Limit: Funkverkehr eingebrochen -> vorsorglich drosseln")
- end
- else
- guard.stableWindows = guard.stableWindows + 1
- if guard.throttled and guard.stableWindows >= 4 then
- guard.throttled = false
- guard.rate = guard.baseRate
- logMsg("Selbst-Limit: Drossel geloest")
- end
- end
- end
- guard.tick = function()
- if guard.mode == "http" then
- -- Spieluhr eng um jeden HTTP-Fetch klammern und den Mittelpunkt nehmen,
- -- damit Spiel- und Echtzeit dasselbe Intervall messen (sonst verzerrt die
- -- Anfrage-Latenz die TPS nach unten).
- local g0a = now()
- local t0 = guard.fetchRealTime()
- local g0b = now()
- if not t0 then
- guard.mode = "selflimit"
- logMsg("Guard: HTTP verloren -> Selbst-Limit")
- return
- end
- local c0 = (g0a + g0b) / 2
- sleep(CONFIG.tpsWindow)
- local g1a = now()
- local t1 = guard.fetchRealTime()
- local g1b = now()
- if not t1 then
- guard.mode = "selflimit"
- return
- end
- local c1 = (g1a + g1b) / 2
- local gameEl = c1 - c0
- local realEl = t1 - t0
- if realEl > 0.5 then
- guard.tps = 20 * gameEl / realEl
- guard.applyRule(guard.tps, realEl)
- end
- else
- guard.tps = nil
- sleep(CONFIG.tpsWindow)
- guard.selfHeuristic()
- end
- end
- ------------------------------------------------------------------
- -- RADIO : Modem-Discovery, Kanal-Management, Senden
- ------------------------------------------------------------------
- radio = {
- side = nil,
- dev = nil,
- wireless = true, -- ist das gefundene Modem drahtlos? (Wired hoert nichts ueber Funk)
- hwLimit = 48, -- vom Modem erlaubtes Maximum (per Probe ermittelt)
- budget = CONFIG.startChannels, -- aktuell erlaubte offene Kanaele (Governor)
- holdBroadcast = false, -- Broadcast 65535 gerade offen halten?
- open = {},
- sweepPos = 0,
- rxRate = 0, -- gemessene Empfangsrate (Nachr./s)
- bcastCooldownUntil = 0, -- bis dahin Broadcast NICHT wieder oeffnen (nach Ueberlast)
- }
- radio.findModem = function()
- -- Bevorzugt ein DRAHTLOSES Modem waehlen; ein Wired Modem hoert keinen Funk.
- local firstAny = nil
- for _, side in ipairs(rs.getSides()) do
- if peripheral.isPresent(side) and peripheral.getType(side) == "modem" then
- local wl = true
- local dev = peripheral.wrap(side)
- pcall(function() if dev.isWireless then wl = dev.isWireless() end end)
- if wl then
- radio.side = side; radio.dev = dev; radio.wireless = true
- return side
- elseif not firstAny then
- firstAny = { side = side, dev = dev }
- end
- end
- end
- if firstAny then -- nur ein Wired Modem gefunden -> nutzen, aber warnen
- radio.side = firstAny.side; radio.dev = firstAny.dev; radio.wireless = false
- return firstAny.side
- end
- return nil
- end
- radio.probeLimit = function()
- if not radio.dev then return 2 end
- pcall(radio.dev.closeAll)
- radio.open = {}
- local count = 0
- for ch = 0, CONFIG.probeMax do
- local ok = pcall(radio.dev.open, ch)
- if not ok then break end
- local isopen = false
- pcall(function() isopen = radio.dev.isOpen(ch) end)
- pcall(radio.dev.close, ch) -- sofort wieder zu -> nie viele Kanaele gleichzeitig offen
- if not isopen then break end
- count = count + 1
- end
- pcall(radio.dev.closeAll)
- radio.open = {}
- radio.hwLimit = math.max(count, 2)
- -- Start bewusst klein (Slow-Start); der Governor waechst nur bei ruhiger Lage.
- radio.budget = math.min(radio.hwLimit, CONFIG.startChannels)
- return radio.hwLimit
- end
- -- Reihenfolge = Prioritaet: applyPlan kappt die Liste bei radio.budget.
- -- Priorisiert werden entdeckte Gegner-IDs (wenig Volumen, hoher Wert).
- -- Broadcast (65535) ist die groesste Datenflut und wird NUR kurz/periodisch
- -- vom Governor freigegeben; GPS (65534) bleibt standardmaessig ganz aus.
- radio.buildDesired = function()
- local list = {}
- -- 1) entdeckte Knoten-IDs pinnen (gezielter Verkehr an bekannte Gegner) --
- -- ABER nur so viele, dass ein Rest des Budgets fuer Broadcast-Stichprobe,
- -- Sweep und Low-Fill frei bleibt. Sonst wuerden bei vielen Knoten diese
- -- (in der Prioritaet spaeteren) Eintraege komplett verdraengt -> die
- -- Broadcast-Erkennung und [B] wuerden still nichts mehr oeffnen.
- local reserve = CONFIG.sweepWindow + 2
- local nodeCap = math.max(1, radio.budget - reserve)
- local cnt = 0
- for _, nd in ipairs(recon.recentNodes()) do
- if nd.id >= 0 and nd.id < 65534 then
- list[#list + 1] = nd.id
- cnt = cnt + 1
- if cnt >= nodeCap then break end
- end
- end
- -- 2) Broadcast nur wenn der Governor es gerade erlaubt (Stichprobe)
- if radio.holdBroadcast then list[#list + 1] = 65535 end
- if CONFIG.openGps then list[#list + 1] = 65534 end
- -- 3) kleiner rotierender Sweep fuer Nicht-Rednet-Kanaele
- for i = 0, CONFIG.sweepWindow - 1 do
- list[#list + 1] = (radio.sweepPos + i) % 65534
- end
- radio.sweepPos = (radio.sweepPos + CONFIG.sweepWindow) % 65534
- -- 4) wenige niedrige IDs als Catch-all
- for ch = 0, math.max(0, CONFIG.lowFill - 1) do list[#list + 1] = ch end
- return list
- end
- radio.applyPlan = function(desiredList)
- if not radio.dev then return end
- local desired = {}
- local n = 0
- for _, ch in ipairs(desiredList) do
- if not desired[ch] then
- n = n + 1
- if n > radio.budget then break end -- harte Kappung am aktuellen Budget
- desired[ch] = true
- end
- end
- for ch in pairs(radio.open) do
- if not desired[ch] then
- pcall(radio.dev.close, ch)
- radio.open[ch] = nil
- end
- end
- local count = 0
- for ch in pairs(desired) do
- if not radio.open[ch] then
- if pcall(radio.dev.open, ch) then radio.open[ch] = true end
- end
- if radio.open[ch] then count = count + 1 end
- end
- STATE.chOpen = count
- end
- -- Empfangs-Governor: misst die Inbound-Rate und regelt Budget + Broadcast so,
- -- dass wir den Server NIE mit einer Kanal-Flut einfrieren. Slow-Start: klein
- -- anfangen und nur bei ruhiger Lage vorsichtig mehr Kanaele oeffnen; bei hoher
- -- Rate sofort Broadcast schliessen und Budget senken (Notbremse bei Panik).
- radio.govern = function()
- local dt = math.max(CONFIG.planInterval, 0.1)
- radio.rxRate = STATE.rxGov / dt
- STATE.rxGov = 0
- local ceil = math.min(radio.hwLimit, CONFIG.maxOpenChannels)
- local t = now()
- if radio.rxRate > CONFIG.panicRxPerSec then
- radio.holdBroadcast = false
- radio.bcastCooldownUntil = t + CONFIG.bcastCooldown
- radio.budget = math.max(4, math.min(radio.budget, 6))
- STATE.overloaded = true
- logMsg(string.format("!! UEBERLAST %.0f Nachr/s -> Notbremse (Broadcast zu, Budget %d)",
- radio.rxRate, radio.budget))
- elseif radio.rxRate > CONFIG.maxRxPerSec then
- radio.holdBroadcast = false
- radio.bcastCooldownUntil = t + CONFIG.bcastCooldown
- radio.budget = math.max(4, radio.budget - 2)
- STATE.overloaded = true
- else
- STATE.overloaded = false
- STATE.emergencyTripped = false -- Ruhe: Notbremse wieder scharf schalten
- if radio.budget < ceil then
- local step = (radio.rxRate < CONFIG.maxRxPerSec * 0.25) and 4 or 1
- radio.budget = math.min(ceil, radio.budget + step)
- end
- -- Broadcast bei ruhiger Lage OFFEN halten (Haupt-Entdeckungsweg fuer Knoten).
- -- Nach einer Ueberlast erst nach der Abkuehlzeit wieder, sonst Flattern.
- if CONFIG.autoBroadcast and t >= radio.bcastCooldownUntil then
- radio.holdBroadcast = true
- else
- radio.holdBroadcast = false
- end
- end
- end
- -- Ereignis-Notbremse: wird DIREKT im Empfangspfad (onModemMessage) aufgerufen,
- -- also unabhaengig vom tick-getakteten Governor. Schliesst sofort die groessten
- -- Datenschleudern (Broadcast 65535 + GPS 65534). Das genuegt, weil der Rest
- -- (gezielte Kanaele) wenig Volumen hat; den Rest raeumt der naechste applyPlan.
- radio.emergencyBrake = function()
- STATE.overloaded = true
- STATE.emergencyTripped = true
- radio.holdBroadcast = false
- radio.bcastCooldownUntil = now() + CONFIG.bcastCooldown
- radio.budget = math.min(radio.budget, CONFIG.panicFloor)
- if radio.dev then
- pcall(radio.dev.close, 65535); radio.open[65535] = nil
- pcall(radio.dev.close, 65534); radio.open[65534] = nil
- end
- logMsg("!! NOTBREMSE: Broadcast/GPS sofort geschlossen (Empfangs-Sturm)")
- end
- -- rate-limitiertes Senden (jeder Sendeweg laeuft hier durch)
- radio.send = function(channel, replyChannel, msg)
- if not guard.acquire() then return false end
- local ok = pcall(radio.dev.transmit, channel, replyChannel, msg)
- if ok then
- STATE.sentCount = STATE.sentCount + 1
- else
- STATE.sendErr = STATE.sendErr + 1
- end
- return ok
- end
- ------------------------------------------------------------------
- -- RECON : passives Aufklaeren, Roster
- ------------------------------------------------------------------
- recon = {}
- recon.node = function(id)
- local nd = STATE.roster[id]
- if not nd then
- -- Roster-Deckel: schuetzt gegen eine Flut gefaelschter Sender-IDs (Gegner
- -- spoofen ggf. tausende IDs) und begrenzt Speicher/Sortierkosten.
- if STATE.nodeCount >= CONFIG.rosterCap then return nil end
- nd = {
- id = id, first = now(), last = now(), count = 0,
- asSender = 0, asTarget = 0, channels = {}, samples = {},
- dist = nil, mtype = nil, status = "active",
- downSince = nil, baselineSeen = false, lastTargeted = nil,
- }
- STATE.roster[id] = nd
- STATE.nodeIds[#STATE.nodeIds + 1] = id
- STATE.nodeCount = STATE.nodeCount + 1
- if not STATE.overloaded then logMsg("Neuer Knoten: ID " .. id) end
- end
- return nd
- end
- recon.pushSample = function(nd, message)
- local s = nd.samples
- s[#s + 1] = message
- while #s > 5 do table.remove(s, 1) end
- end
- recon.onModemMessage = function(side, channel, replyChannel, message, distance)
- STATE.rxCount = STATE.rxCount + 1
- STATE.rxWindow = STATE.rxWindow + 1
- STATE.rxGov = STATE.rxGov + 1
- -- Ereignis-Notbremse: laeuft im Empfangspfad selbst und greift daher auch,
- -- wenn der tick-getaktete Governor wegen Lag gerade nicht drankommt. rxGov
- -- wird vom Governor jede Sekunde genullt; ueberschreitet es hier den Burst-
- -- Schwellwert, ist der Empfang aus dem Ruder -> Broadcast/GPS sofort zu.
- if STATE.rxGov >= CONFIG.panicRxBurst and not STATE.emergencyTripped then
- radio.emergencyBrake()
- end
- -- Unter Ueberlast nur Stichproben verarbeiten, damit die Event-Queue schnell
- -- leerlaeuft (der Governor/die Notbremse schliesst parallel Kanaele).
- if STATE.overloaded and (STATE.rxCount % 8) ~= 0 then return end
- local t = now()
- -- Sender = replyChannel (falls plausible Computer-ID)
- if type(replyChannel) == "number" and replyChannel >= 0 and replyChannel < 65534 then
- local nd = recon.node(replyChannel)
- if nd then
- nd.last = t
- nd.count = nd.count + 1
- nd.asSender = nd.asSender + 1
- if type(channel) == "number" then
- nd.channels[channel] = (nd.channels[channel] or 0) + 1
- end
- if type(distance) == "number" then
- nd.dist = distance
- STATE.maxDist = math.max(STATE.maxDist or 0, distance)
- end
- nd.mtype = type(message)
- recon.pushSample(nd, message)
- if nd.count >= CONFIG.baselineMsgs then nd.baselineSeen = true end
- if nd.status == "down?" then
- nd.status = "active"
- nd.downSince = nil
- logMsg("Knoten " .. replyChannel .. " wieder aktiv (Reboot?)")
- elseif nd.status == "DEAD" and nd.killedAt and (t - nd.killedAt) > 10 then
- -- bestaetigt getoeteter Gegner sendet wieder -> hat rebootet, erneut angreifen
- nd.status = "active"
- nd.downSince = nil
- logMsg("Knoten " .. replyChannel .. " ONLINE nach bestaetigtem Kill -> erneut im Visier!")
- end
- end
- end
- -- Ziel = channel (falls plausible Computer-ID, nicht Broadcast/GPS)
- if type(channel) == "number" and channel >= 0 and channel < 65534 then
- local tn = recon.node(channel)
- if tn then
- tn.asTarget = tn.asTarget + 1
- tn.lastTargeted = t
- end
- end
- end
- -- Knoten nach letzter Aktivitaet (neueste zuerst). Ergebnis kurz gecacht, da
- -- die Funktion aus mehreren Schleifen mehrmals/s aufgerufen wird und die volle
- -- Sortierung (bis rosterCap=400) sonst unnoetig CPU frisst - gerade unter Last.
- recon.recentNodes = function()
- local t = now()
- if STATE.nodeCacheList and (t - (STATE.nodeCacheAt or -1)) < 0.5 then
- return STATE.nodeCacheList
- end
- local arr = {}
- for _, nd in pairs(STATE.roster) do arr[#arr + 1] = nd end
- table.sort(arr, function(a, b) return a.last > b.last end)
- STATE.nodeCacheList = arr
- STATE.nodeCacheAt = t
- return arr
- end
- ------------------------------------------------------------------
- -- ATTACK : Spoof / Fuzz / Flood + Kampagnen-Scheduler
- ------------------------------------------------------------------
- attack = {}
- attack.stateFor = function(id)
- local s = STATE.targets[id]
- if not s then
- s = { id = id, phase = "observe", phaseStart = now(), pokes = 0 }
- STATE.targets[id] = s
- end
- return s
- end
- attack.randomSample = function()
- local pool = {}
- for _, nd in pairs(STATE.roster) do
- for _, sm in ipairs(nd.samples) do pool[#pool + 1] = sm end
- end
- if #pool == 0 then return nil end
- return pool[math.random(#pool)]
- end
- -- eine fremde ID vortaeuschen (vom Ziel als Peer "vertraut")
- attack.spoofSource = function(targetId)
- local ids = STATE.nodeIds
- if #ids > 0 then
- for _ = 1, 6 do
- local cand = ids[math.random(#ids)]
- if cand ~= targetId and cand ~= STATE.myId then return cand end
- end
- end
- return math.random(0, 64)
- end
- attack.FUZZ = {
- function() return "" end,
- function() return string.rep("A", math.random(200, 4000)) end,
- function() return math.random(-1000000000, 1000000000) end,
- function() return {} end,
- function() return { cmd = randstr(), id = math.random(0, 999), data = randstr() } end,
- function() return "{" .. randstr() end,
- function() return "}" .. randstr() .. "=" end,
- function() return true end,
- function()
- local s = attack.randomSample()
- if type(s) == "string" and #s > 1 then return string.sub(s, 1, #s - 1) end
- return randstr()
- end,
- function() return string.char(math.random(1, 31)) .. randstr() end,
- function()
- if textutils and textutils.serialize then
- local ok, r = pcall(textutils.serialize, { a = 1, b = { 2, 3 } })
- if ok then return r end
- end
- return randstr()
- end,
- }
- attack.fuzzPayload = function()
- return attack.FUZZ[math.random(#attack.FUZZ)]()
- end
- attack.spoofPayload = function()
- local s = attack.randomSample()
- if s ~= nil then return s end
- return "ping"
- end
- attack.candidates = function()
- local arr = {}
- for _, nd in ipairs(recon.recentNodes()) do
- if nd.id ~= STATE.myId
- and not STATE.whitelist[nd.id]
- and nd.status ~= "DEAD"
- and (nd.baselineSeen or nd.asTarget > 0) then
- arr[#arr + 1] = nd.id
- end
- end
- return arr
- end
- attack.advancePhase = function(s)
- local mode = STATE.aggression
- local el = now() - s.phaseStart
- if mode == "stealth" then
- s.phase = "spoof"
- elseif mode == "aggressive" then
- if s.phase == "observe" and el > 2 then s.phase = "fuzz"; s.phaseStart = now()
- elseif s.phase == "fuzz" and el > 5 then s.phase = "flood"; s.phaseStart = now() end
- else -- adaptive
- if s.phase == "observe" and el > CONFIG.observeSecs then s.phase = "spoof"; s.phaseStart = now()
- elseif s.phase == "spoof" and el > CONFIG.spoofSecs then s.phase = "fuzz"; s.phaseStart = now()
- elseif s.phase == "fuzz" and el > CONFIG.fuzzSecs then s.phase = "flood"; s.phaseStart = now() end
- end
- end
- attack.assess = function(nd)
- if nd.baselineSeen and nd.status == "active" and (now() - nd.last) > CONFIG.downSilence then
- nd.status = "down?"
- nd.downSince = now()
- logMsg(string.format("ZIEL %d still seit %.0fs -> VERDACHT Absturz. Sichtbaren Crash beim SL pruefen! [K]=bestaetigen",
- nd.id, now() - nd.last))
- end
- end
- attack.batchFor = function(s)
- if s.phase == "observe" then return 0
- elseif s.phase == "spoof" then return 2
- elseif s.phase == "fuzz" then return 4
- else return CONFIG.floodEnabled and 20 or 4 end
- end
- attack.emit = function(nd, s)
- local src = attack.spoofSource(nd.id)
- local payload
- if s.phase == "spoof" then
- payload = attack.spoofPayload()
- else
- payload = attack.fuzzPayload()
- end
- radio.send(nd.id, src, payload)
- -- Broadcast-Stoerung nur mit explizitem Opt-in: ein Send auf 65535 wird vom
- -- Server an JEDEN Computer mit offenem Broadcast-Kanal verteilt (Fan-out ->
- -- server-weite Last, genau die Flut, die wir vermeiden wollen).
- if s.phase == "flood" and CONFIG.floodBroadcast and math.random() < 0.5 then
- radio.send(65535, src, payload)
- end
- s.pokes = s.pokes + 1
- end
- attack.step = function()
- if not STATE.campaignOn or STATE.attacksPaused then return end
- local ids = attack.candidates()
- if #ids == 0 then return end
- STATE.rr = (STATE.rr % #ids) + 1
- local id = ids[STATE.rr]
- local nd = STATE.roster[id]
- if not nd then return end
- local s = attack.stateFor(id)
- attack.advancePhase(s)
- attack.assess(nd)
- local batch = attack.batchFor(s)
- if nd.status == "down?" then batch = math.min(batch, 2) end -- nur nachfassen
- for i = 1, batch do
- if not STATE.running then return end
- attack.emit(nd, s)
- if i % 30 == 0 then sleep(0) end
- end
- end
- ------------------------------------------------------------------
- -- UI : Dashboard (Computer-Schirm + optional Monitor)
- ------------------------------------------------------------------
- ui = { monSide = nil }
- ui.initDisplays = function()
- STATE.displays = {}
- local termColour = false
- pcall(function() termColour = term.isColour() end)
- STATE.displays[1] = { dev = term, colour = termColour, name = "term" }
- if ui.monSide and peripheral.isPresent(ui.monSide) then
- local m = peripheral.wrap(ui.monSide)
- pcall(function() m.setTextScale(0.5) end)
- local c = false
- pcall(function() c = m.isColour() end)
- STATE.displays[#STATE.displays + 1] = { dev = m, colour = c, name = "mon" }
- end
- end
- ui.viewList = function()
- return recon.recentNodes()
- end
- ui.draw = function(d)
- local dev = d.dev
- local ok, w, h = pcall(dev.getSize)
- if not ok or not w then return end
- if d.colour then
- pcall(dev.setBackgroundColour, colours.black)
- pcall(dev.setTextColour, colours.white)
- end
- pcall(dev.clear)
- local function put(x, y, txt, fg)
- if y < 1 or y > h then return end
- if x > w then return end
- txt = tostring(txt)
- if d.colour then pcall(dev.setTextColour, fg or colours.white) end
- pcall(dev.setCursorPos, x, y)
- pcall(dev.write, string.sub(txt, 1, w - x + 1))
- end
- local tps = guard.tps and string.format("%.1f", guard.tps) or "--"
- local camp = STATE.campaignOn and (STATE.attacksPaused and "PAUSE" or "AKTIV") or "AUS"
- put(1, 1, "== REDNET EW == ID:" .. STATE.myId .. " " .. gameClock(), colours.cyan)
- put(1, 2, "Kampagne:" .. camp .. " Modus:" .. STATE.aggression
- .. " Flood:" .. (CONFIG.floodEnabled and "an" or "aus"),
- STATE.campaignOn and colours.lime or colours.grey)
- put(1, 3, "TPS:" .. tps .. "(" .. guard.mode .. ")"
- .. (guard.throttled and " DROSSEL" or "")
- .. " Rate:" .. string.format("%.0f", guard.rate) .. "/s",
- guard.throttled and colours.orange or colours.white)
- put(1, 4, "Knoten:" .. STATE.nodeCount .. " RX:" .. STATE.rxCount
- .. " TX:" .. STATE.sentCount .. " Reichw~:"
- .. (STATE.maxDist and (math.floor(STATE.maxDist) .. "m") or "?")
- .. " Kills:" .. STATE.killConfirmed, colours.white)
- put(1, 5, "Kanaele:" .. (STATE.chOpen or 0) .. "/" .. radio.budget
- .. " rx/s:" .. math.floor(radio.rxRate or 0)
- .. (radio.holdBroadcast and " [BC]" or "")
- .. (STATE.overloaded and " !!UEBERLAST-DROSSEL!!" or ""),
- STATE.overloaded and colours.red or colours.lightGrey)
- put(1, 6, " ID alt msgs ch typ status", colours.lightGrey)
- local list = ui.viewList()
- STATE.viewList = list
- if STATE.selIndex < 1 then STATE.selIndex = 1 end
- if STATE.selIndex > #list and #list > 0 then STATE.selIndex = #list end
- local rowStart = 7
- -- Log-Hoehe an die verfuegbare Hoehe anpassen, damit auf kleinen Monitoren
- -- Roster-Zeilen sichtbar bleiben und der Log nicht die Kopfzeile ueberschreibt.
- local logLines = math.max(1, math.min(5, h - rowStart - 1))
- local rowEnd = h - logLines - 1
- local maxRows = rowEnd - rowStart + 1
- if maxRows < 0 then maxRows = 0 end
- -- Scroll-Fenster um die Auswahl
- local first = 1
- if #list > maxRows and maxRows > 0 then
- first = math.max(1, math.min(STATE.selIndex - math.floor(maxRows / 2), #list - maxRows + 1))
- end
- local y = rowStart
- for i = first, math.min(#list, first + maxRows - 1) do
- local nd = list[i]
- local st = nd.status
- if STATE.whitelist[nd.id] then st = "WL" end
- local col = colours.white
- if st == "down?" then col = colours.yellow
- elseif st == "DEAD" then col = colours.lime
- elseif st == "WL" then col = colours.grey end
- local marker = (i == STATE.selIndex) and ">" or " "
- local line = string.format("%s %-5d %3ds %5d %2d %-6s %s",
- marker, nd.id, math.floor(now() - nd.last), nd.count,
- cntChannels(nd), truncate(nd.mtype or "?", 6), st)
- put(1, y, line, (i == STATE.selIndex) and colours.yellow or col)
- y = y + 1
- end
- -- Log unten
- local ly = h - logLines
- put(1, ly, "-- Log --------------------------------------------", colours.grey)
- local L = STATE.log
- for i = 1, logLines - 1 do
- local idx = #L - (logLines - 1) + i
- if idx >= 1 then put(1, ly + i, L[idx], colours.lightGrey) end
- end
- local help
- if w >= 78 then
- help = "[S]tart [Spc]Pause [A]Modus [F]lood [B]cast [+/-]Rate [W]L [K]ill [R]escan [Q]uit"
- else
- help = "S:Start Spc:Pause A:Mode F:Fl B:BC W:WL K:Kill Q"
- end
- put(1, h, help, colours.cyan)
- end
- ui.render = function()
- for _, d in ipairs(STATE.displays) do pcall(ui.draw, d) end
- end
- ------------------------------------------------------------------
- -- INPUT : Tastensteuerung (nur am Computer)
- ------------------------------------------------------------------
- input = {}
- input.selectedNode = function()
- local l = STATE.viewList
- if l and l[STATE.selIndex] then return l[STATE.selIndex] end
- return nil
- end
- input.onChar = function(c)
- c = string.lower(c)
- if c == "q" then
- STATE.running = false
- elseif c == " " then
- STATE.attacksPaused = not STATE.attacksPaused
- logMsg("Angriffe " .. (STATE.attacksPaused and "pausiert" or "fortgesetzt"))
- elseif c == "s" then
- STATE.campaignOn = not STATE.campaignOn
- logMsg("Kampagne " .. (STATE.campaignOn and "GESTARTET" or "gestoppt"))
- elseif c == "a" then
- local order = { stealth = "adaptive", adaptive = "aggressive", aggressive = "stealth" }
- STATE.aggression = order[STATE.aggression] or "adaptive"
- logMsg("Angriffs-Modus: " .. STATE.aggression)
- elseif c == "f" then
- CONFIG.floodEnabled = not CONFIG.floodEnabled
- logMsg("Flooding " .. (CONFIG.floodEnabled and "an" or "aus"))
- elseif c == "b" then
- -- Broadcast sofort oeffnen (Abkuehlzeit aufheben). Die Notbremse kann ihn
- -- bei echter Flut trotzdem wieder schliessen.
- radio.bcastCooldownUntil = 0
- radio.holdBroadcast = true
- logMsg("Broadcast manuell geoeffnet")
- elseif c == "]" or c == "+" or c == "=" then
- guard.baseRate = math.min(guard.baseRate + 5, 200)
- if not guard.throttled then guard.rate = guard.baseRate end
- logMsg("Basis-Rate: " .. guard.baseRate .. "/s")
- elseif c == "[" or c == "-" then
- guard.baseRate = math.max(guard.baseRate - 5, 1)
- if not guard.throttled then guard.rate = guard.baseRate end
- logMsg("Basis-Rate: " .. guard.baseRate .. "/s")
- elseif c == "w" then
- local nd = input.selectedNode()
- if nd then
- STATE.whitelist[nd.id] = not STATE.whitelist[nd.id] or nil
- logMsg("Whitelist " .. nd.id .. ": " .. (STATE.whitelist[nd.id] and "an" or "aus"))
- end
- elseif c == "k" then
- local nd = input.selectedNode()
- if nd and nd.status ~= "DEAD" then
- nd.status = "DEAD"
- nd.killedAt = now()
- STATE.killConfirmed = STATE.killConfirmed + 1
- logMsg("KILL bestaetigt: Knoten " .. nd.id .. " (sichtbarer Crash)")
- end
- elseif c == "r" then
- logMsg("Rescan: Kanal-Limit + Plan neu")
- pcall(function()
- radio.probeLimit()
- radio.applyPlan(radio.buildDesired())
- end)
- end
- end
- input.onKey = function(code)
- if code == keys.up then
- STATE.selIndex = math.max(1, STATE.selIndex - 1)
- elseif code == keys.down then
- STATE.selIndex = STATE.selIndex + 1
- elseif code == keys.right then
- guard.baseRate = math.min(guard.baseRate + 5, 200)
- if not guard.throttled then guard.rate = guard.baseRate end
- elseif code == keys.left then
- guard.baseRate = math.max(guard.baseRate - 5, 1)
- if not guard.throttled then guard.rate = guard.baseRate end
- end
- end
- ------------------------------------------------------------------
- -- SCOUT : optionale OpenCCSensors-Ortung (best effort)
- ------------------------------------------------------------------
- scout = { side = nil, hasTargets = false }
- scout.find = function()
- for _, side in ipairs(rs.getSides()) do
- if peripheral.isPresent(side) and peripheral.getType(side) == "sensor" then
- scout.side = side
- local methods = {}
- pcall(function() methods = peripheral.getMethods(side) end)
- for _, m in ipairs(methods or {}) do
- if m == "getTargets" then scout.hasTargets = true end
- end
- return side
- end
- end
- return nil
- end
- scout.poll = function()
- if not scout.side or not scout.hasTargets then return end
- local ok, targets = pcall(peripheral.call, scout.side, "getTargets")
- if ok and type(targets) == "table" then
- local n = 0
- for _ in pairs(targets) do n = n + 1 end
- STATE.scoutCount = n
- end
- end
- ------------------------------------------------------------------
- -- Nebenlaeufige Schleifen
- ------------------------------------------------------------------
- eventLoop = function()
- while STATE.running do
- local ev = { os.pullEventRaw() }
- local e = ev[1]
- if e == "modem_message" then
- pcall(recon.onModemMessage, ev[2], ev[3], ev[4], ev[5], ev[6])
- elseif e == "char" then
- pcall(input.onChar, ev[2])
- elseif e == "key" then
- pcall(input.onKey, ev[2])
- elseif e == "terminate" then
- STATE.running = false
- end
- if not STATE.running then return end
- end
- end
- attackLoop = function()
- while STATE.running do
- local ok, err = pcall(attack.step)
- if not ok then logMsg("attack-Fehler: " .. tostring(err)) end
- sleep(CONFIG.attackTick)
- end
- end
- guardLoop = function()
- pcall(guard.detect)
- while STATE.running do
- local ok, err = pcall(guard.tick)
- if not ok then
- logMsg("guard-Fehler: " .. tostring(err))
- sleep(2)
- end
- end
- end
- uiLoop = function()
- while STATE.running do
- pcall(ui.render)
- sleep(CONFIG.uiRefresh)
- end
- end
- scoutLoop = function()
- while STATE.running do
- pcall(scout.poll)
- sleep(3)
- end
- end
- planLoop = function()
- while STATE.running do
- pcall(function()
- radio.govern() -- Empfangsrate messen + Budget/Broadcast regeln
- radio.applyPlan(radio.buildDesired()) -- Kanaele entsprechend oeffnen/schliessen
- end)
- sleep(CONFIG.planInterval)
- end
- end
- ------------------------------------------------------------------
- -- SETUP-ASSISTENT
- ------------------------------------------------------------------
- -- gibt true zurueck (Taste) oder false (terminate/Ctrl+T). os.pullEventRaw
- -- wirft bei terminate nicht, damit wir sauber aufraeumen koennen.
- local function pressAnyKey()
- print("")
- print(" [ Taste druecken zum Fortfahren ]")
- while true do
- local e = os.pullEventRaw()
- if e == "key" then return true end
- if e == "terminate" then return false end
- end
- end
- setupWizard = function()
- term.clear(); term.setCursorPos(1, 1)
- local function line(s) print(s) end
- local function abort() -- bei Ctrl+T: offene Kanaele schliessen, sauber raus
- pcall(function() if radio.dev then radio.dev.closeAll() end end)
- return false
- end
- line("=====================================================")
- line(" REDNET ELECTRONIC-WARFARE - AUFBAU / SETUP")
- line("=====================================================")
- line("")
- line("Platziere folgende Bloecke an den Computer:")
- line("")
- line(" 1) WIRELESS MODEM (Pflicht) an eine beliebige Seite.")
- line(" Fuer maximale Reichweite den Computer moeglichst")
- line(" HOCH bauen - Reichweite steigt mit der Hoehe.")
- line("")
- line(" 2) ADVANCED MONITOR (optional, empfohlen) an eine")
- line(" andere Seite - grosses Live-Dashboard. Mehrere")
- line(" Bloecke breit/hoch = mehr Uebersicht.")
- line("")
- line(" 3) OPENCCSENSORS-SENSOR (optional) fuer Gegner-Ortung.")
- line("")
- line(" Hinweis: HTTP wird fuer 'pastebin get' und die echte")
- line(" TPS-Messung gebraucht. Ohne HTTP laeuft der Guard im")
- line(" Selbst-Limit-Modus (Fallback).")
- line("")
- line(" Empfang: bei ruhiger Lage bleibt Broadcast offen")
- line(" (findet Knoten). Steigt die Empfangsrate zu stark,")
- line(" schliesst eine Notbremse Broadcast/GPS sofort -")
- line(" so friert die Funk-Flut den Server nicht ein.")
- if not pressAnyKey() then return abort() end
- term.clear(); term.setCursorPos(1, 1)
- line("== Peripherie-Erkennung ==")
- line("")
- local modem = radio.findModem()
- local monSide = nil
- local sensor = nil
- for _, side in ipairs(rs.getSides()) do
- if peripheral.isPresent(side) then
- local t = peripheral.getType(side)
- if t == "monitor" and not monSide then monSide = side end
- if t == "sensor" and not sensor then sensor = side end
- end
- end
- ui.monSide = monSide
- if sensor then scout.find() end
- line(" Modem : " .. (modem and ("[OK] " .. modem) or "[FEHLT]"))
- line(" Monitor : " .. (monSide and ("[OK] " .. monSide) or "[-- kein]"))
- line(" Sensor : " .. (sensor and ("[OK] " .. sensor) or "[-- kein]"))
- line(" HTTP : " .. (http and "[verfuegbar]" or "[aus]"))
- line("")
- if not modem then
- line("FEHLER: Kein Wireless Modem gefunden!")
- line("Setze ein Wireless Modem an eine Seite des Computers")
- line("und starte 'ew' erneut.")
- pressAnyKey()
- return false
- end
- line("Kalibriere Kanal-Limit ...")
- local maxc = radio.probeLimit()
- line(" -> Modem-Limit ~" .. maxc .. "; Empfang rate-geregelt")
- line(" (Start " .. CONFIG.startChannels .. " Kanaele, max " .. CONFIG.maxOpenChannels
- .. ", Budget " .. CONFIG.maxRxPerSec .. " Nachr/s)")
- radio.holdBroadcast = true -- fuer den Hoertest Broadcast mitlauschen
- radio.applyPlan(radio.buildDesired())
- line("Hoere 6s auf Funkverkehr (Reichweiten-/Aktivitaetstest) ...")
- local timer = os.startTimer(6)
- while true do
- local ev = { os.pullEventRaw() }
- if ev[1] == "modem_message" then
- pcall(recon.onModemMessage, ev[2], ev[3], ev[4], ev[5], ev[6])
- elseif ev[1] == "timer" and ev[2] == timer then
- break
- elseif ev[1] == "terminate" then
- return abort()
- end
- end
- line(" -> Knoten gehoert: " .. STATE.nodeCount .. " Nachrichten: " .. STATE.rxCount)
- if STATE.nodeCount == 0 then
- line(" Noch nichts gehoert - normal: im Setup lauschen wir nur")
- line(" auf wenige Kanaele. Im Betrieb tastet [B]cast/Governor")
- line(" den Broadcast ab. Sonst hoeher bauen / [R]escan.")
- else
- line(" Max. Distanz bisher: " ..
- (STATE.maxDist and (math.floor(STATE.maxDist) .. "m") or "?"))
- end
- line("")
- line("WICHTIG: Setze deine EIGENEN Netz-IDs spaeter mit [W]")
- line("auf die Whitelist, damit du sie nicht selbst angreifst!")
- line("")
- line("Kampagne startet NICHT automatisch - mit [S] beginnen.")
- if not pressAnyKey() then return abort() end
- return true
- end
- ------------------------------------------------------------------
- -- MAIN
- ------------------------------------------------------------------
- main = function()
- STATE.myId = os.getComputerID()
- STATE.whitelist[STATE.myId] = true
- local seed = 1
- pcall(function() seed = math.floor((os.time() * 10000) + STATE.myId + (os.clock() * 1000)) end)
- math.randomseed(seed)
- guard.last = now()
- if not setupWizard() then return end
- ui.initDisplays()
- STATE.running = true
- STATE.campaignOn = CONFIG.autostart
- -- pcall, damit ein unerwarteter Fehler in einer Schleife trotzdem ins
- -- Aufraeumen faellt (Kanaele schliessen) statt roh zu crashen.
- local ok, err = pcall(parallel.waitForAny,
- eventLoop, attackLoop, guardLoop, uiLoop, scoutLoop, planLoop)
- -- Aufraeumen
- pcall(function() if radio.dev then radio.dev.closeAll() end end)
- pcall(function()
- if term.isColour() then
- term.setBackgroundColour(colours.black)
- term.setTextColour(colours.white)
- end
- end)
- term.clear(); term.setCursorPos(1, 1)
- print("ew beendet. Kanaele geschlossen.")
- print("Kills bestaetigt: " .. STATE.killConfirmed)
- if not ok and err and err ~= "" then
- printError("Fehler: " .. tostring(err))
- end
- end
- main()
Advertisement
Add Comment
Please, Sign In to add comment