// AeroMC-Plugin für das Stream Deck. // Spricht direkt das Stream-Deck-Protokoll (WebSocket) und mit dem Launcher über // dessen lokale Schnittstelle (Einstellung "Stream Deck erlauben" in AeroMC). // Port und Schlüssel stehen in %APPDATA%\AeroMC\streamdeck.json. import WebSocket from 'ws'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const A = (n) => 'com.aeromc.launcher.' + n; const AKTION_INSTANZ = A('instanz'); const AKTION_CLIP = A('clip'); const AKTION_AUFNAHME = A('aufnahme'); const AKTION_SERVER = A('server'); const AKTION_ZULETZT = A('zuletzt'); const AKTION_FENSTER = A('fenster'); const AKTION_CLIPMODUS = A('clipmodus'); const AKTION_NEUSTART = A('neustart'); const AKTION_ORDNER = A('ordner'); const AKTION_SPIELZEIT = A('spielzeit'); const AKTION_SERVERSTATUS = A('serverstatus'); const AKTION_SCREENSHOT = A('screenshot'); const AKTION_DREH = A('dreh'); const AKTION_FREUNDE = A('freunde'); const AKTION_KONTO = A('konto'); const AKTION_SCHNELL = A('schnellstart'); const AKTION_BACKUP = A('backup'); const AKTION_LOGS = A('logs'); const AKTION_KONSOLE = A('konsole'); const AKTION_ABSTURZ = A('absturz'); const CRASH_MS = 30000; const ABFRAGE_MS = 2000; const PING_MS = 30000; const args = process.argv.slice(2); const arg = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : null; }; const port = arg('-port'); const pluginUUID = arg('-pluginUUID'); const registerEvent = arg('-registerEvent'); const tasten = new Map(); // context -> { action, settings } let stand = null; // letzte Antwort von /state, null = Launcher nicht erreichbar let meldung = null; // kurze Rückmeldung nach einem Tastendruck { context, text, bis } const ws = new WebSocket(`ws://127.0.0.1:${port}`); const sende = (obj) => { if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(obj)); }; // ---------- Launcher ---------- function verbindung() { const datei = path.join(process.env.APPDATA || '', 'AeroMC', 'streamdeck.json'); try { return JSON.parse(fs.readFileSync(datei, 'utf8')); } catch { return null; } } async function anfrage(methode, pfad, body) { const v = verbindung(); if (!v) return null; try { const res = await fetch(`http://127.0.0.1:${v.port}${pfad}`, { method: methode, headers: { Authorization: `Bearer ${v.token}`, 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(3000), }); return await res.json(); } catch { return null; } } async function holeStand() { // Freunde nur anfordern, wenn eine Freunde-Taste existiert (der Launcher fragt dafür den Server) const mitFreunden = [...tasten.values()].some((t) => t.action === AKTION_FREUNDE); const s = await anfrage('GET', '/state' + (mitFreunden ? '?freunde=1' : '')); stand = s && s.ok ? s : null; } // ---------- Tastenbild ---------- const esc = (t) => String(t).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); function zeilen(text, breite = 9) { const woerter = String(text || '').split(/\s+/).filter(Boolean); const out = []; let zeile = ''; for (const w of woerter) { if ((zeile + ' ' + w).trim().length <= breite) zeile = (zeile + ' ' + w).trim(); else { if (zeile) out.push(zeile); zeile = w; } } if (zeile) out.push(zeile); return out.slice(0, 2).map((z) => (z.length > breite ? z.slice(0, breite - 1) + '…' : z)); } function dauer(seit) { const s = Math.max(0, Math.floor((Date.now() - seit) / 1000)); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); return h ? `${h}:${String(m).padStart(2, '0')} h` : `${m} min`; } // AeroMC-Logo einmal einlesen und in jedes Tastenbild einbetten let logo = ''; try { const datei = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'imgs', 'actions', 'key@2x.png'); logo = 'data:image/png;base64,' + fs.readFileSync(datei).toString('base64'); } catch { /* ohne Logo geht es auch */ } function bild({ farbe, kopf, titel, fuss, symbol }) { const sym = symbol && symbol.bild ? symbol : (logo ? { bild: logo, pixel: false } : null); const t = zeilen(titel); const texte = t.map((z, i) => `${esc(z)}`).join(''); const svg = `` + `` + (sym ? `` : '') + `${esc(kopf)}` + texte + `${esc(fuss)}` + ``; return 'data:image/svg+xml;charset=utf8,' + encodeURIComponent(svg); } const GRAU = '#6b7280', GRUEN = '#34d399', ROT = '#f87171', GELB = '#fbbf24', BLAU = '#60a5fa'; const symbole = new Map(); // Instanz-ID -> { bildId, bild, pixel } const koepfe = new Map(); // Konto-UUID -> { bild } const abstuerze = new Map(); // Instanz-ID -> { zeit(Abfrage), bericht, frisch, berichtZeit } function symbolKonto(uuid) { const x = uuid && koepfe.get(uuid); return x && x.bild ? x : null; } // Instanz, die gerade läuft, sonst die zuletzt gespielte - für Tasten ohne feste Auswahl function standardInstanz() { const l = (stand && stand.instanzen) || []; const lauf = l.find((x) => x.laeuft); return lauf ? lauf.id : (stand && stand.zuletzt) || null; } function vor(ms) { const min = Math.max(0, Math.floor((Date.now() - ms) / 60000)); if (min < 1) return 'gerade eben'; if (min < 60) return 'vor ' + min + ' min'; if (min < 1440) return 'vor ' + Math.floor(min / 60) + ' h'; return 'vor ' + Math.floor(min / 1440) + ' Tg.'; } async function pollAbstuerze(erzwingen) { const ids = new Set(); for (const t of tasten.values()) if (t.action === AKTION_ABSTURZ) { const id = instanzId(t); if (id) ids.add(id); } for (const id of ids) { const alt = abstuerze.get(id); if (!erzwingen && alt && Date.now() - alt.zeit < CRASH_MS) continue; const r = await anfrage('GET', '/crash?id=' + encodeURIComponent(id)); abstuerze.set(id, { zeit: Date.now(), bericht: !!(r && r.bericht), frisch: !!(r && r.frisch), berichtZeit: r && r.zeit }); } } function symbolFuer(id) { const x = id && symbole.get(id); return x && x.bild ? x : null; } // Instanz-Bilder (eigenes Bild oder Minecraft-Icon) nachladen, sobald sie gebraucht werden oder sich ändern async function holeSymbole() { if (!stand) return; const ids = new Set(); for (const t of tasten.values()) { const id = instanzId(t); if (id) ids.add(id); } for (const t of tasten.values()) { const u = t.action === AKTION_KONTO && t.settings.kontoId; if (u && !koepfe.has(u)) { const r = await anfrage('GET', '/icon?konto=' + encodeURIComponent(u)); koepfe.set(u, { bild: r && r.ok ? r.bild : null }); } } for (const id of ids) { const inst = (stand.instanzen || []).find((x) => x.id === id); if (!inst) continue; const alt = symbole.get(id); if (alt && alt.bildId === inst.bildId) continue; const r = await anfrage('GET', '/icon?id=' + encodeURIComponent(id)); symbole.set(id, { bildId: inst.bildId, bild: r && r.ok ? r.bild : null, pixel: !!(r && r.pixel) }); } } // ---------- Drehregler (Stream Deck+) ---------- function dialListe() { return (stand && stand.instanzen) || []; } function dialIndex(t) { const l = dialListe(); let i = l.findIndex((x) => x.id === t.settings.instanceId); if (i < 0) i = l.findIndex((x) => x.id === (stand && stand.zuletzt)); return i < 0 ? 0 : i; } function zeichneDial(context) { const t = tasten.get(context); if (!t) return; const l = dialListe(); let fb; if (meldung && meldung.context === context && meldung.bis > Date.now()) { fb = { title: 'HINWEIS', value: meldung.text, icon: logo || '', indicator: { value: 0 } }; } else if (!stand) { fb = { title: 'AeroMC', value: 'Launcher aus', icon: logo || '', indicator: { value: 0 } }; } else if (!l.length) { fb = { title: 'AeroMC', value: 'Keine Instanz', icon: logo || '', indicator: { value: 0 } }; } else { const i = dialIndex(t); const x = l[i]; const sym = symbolFuer(x.id); fb = { title: x.name, value: x.laeuft ? '● ' + dauer(x.seit || Date.now()) : '▶ bereit', icon: (sym && sym.bild) || logo || '', indicator: { value: Math.round(((i + 1) / l.length) * 100) }, }; } sende({ event: 'setFeedback', context, payload: fb }); } const pings = new Map(); // Adresse -> { ok, online, max, latencyMs, zeit } function stunden(sek) { if (sek < 3600) return Math.floor(sek / 60) + ' min'; const h = sek / 3600; return (h < 100 ? h.toFixed(1).replace('.', ',') : Math.round(h)) + ' h'; } // Welche Instanz gehört zu dieser Taste? function instanzId(t) { if (t.action === AKTION_ZULETZT) return stand && stand.zuletzt; if (t.action === AKTION_SCHNELL) return (stand && stand.schnellstart && stand.schnellstart[t.settings.slot || '1']) || null; if (t.action === AKTION_LOGS || t.action === AKTION_ABSTURZ) return t.settings.instanceId || standardInstanz(); if (t.action === AKTION_DREH) return stand && dialListe().length ? dialListe()[dialIndex(t)].id : null; return t.settings.instanceId || null; } function adresse(t) { return String(t.settings.adresse || t.settings.server || '').trim(); } function zeichne(context) { const t = tasten.get(context); if (!t) return; if (t.action === AKTION_DREH) { zeichneDial(context); return; } const id = instanzId(t); const sym = t.action === AKTION_KONTO ? symbolKonto(t.settings.kontoId) : symbolFuer(id); const i = stand && id ? (stand.instanzen || []).find((x) => x.id === id) : null; if (i) t.name = i.name; // merken, damit die Taste auch bei ausgeschaltetem Launcher ihren Namen zeigt const aus = !stand; const name = (i && i.name) || t.name || 'Instanz'; let b; if (meldung && meldung.context === context && meldung.bis > Date.now()) { b = bild({ symbol: sym, farbe: GELB, kopf: 'HINWEIS', titel: meldung.text, fuss: '' }); } else switch (t.action) { case AKTION_INSTANZ: case AKTION_ZULETZT: { const kopfLeer = t.action === AKTION_ZULETZT ? 'ZULETZT' : 'INSTANZ'; if (t.action === AKTION_INSTANZ && !id) b = bild({ symbol: sym, farbe: GRAU, kopf: kopfLeer, titel: 'Bitte wählen', fuss: '' }); else if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'AUS', titel: name, fuss: 'Launcher aus' }); else if (!i) b = bild({ symbol: sym, farbe: GRAU, kopf: kopfLeer, titel: t.action === AKTION_ZULETZT ? 'noch nichts gespielt' : 'nicht gefunden', fuss: '' }); else if (i.laeuft) b = bild({ symbol: sym, farbe: GRUEN, kopf: '● LÄUFT', titel: i.name, fuss: i.seit ? dauer(i.seit) : '■ beenden' }); else b = bild({ symbol: sym, farbe: BLAU, kopf: t.action === AKTION_ZULETZT ? '▶ ZULETZT' : '▶ START', titel: i.name, fuss: '' }); break; } case AKTION_SERVER: { const adr = adresse(t); if (!id || !adr) b = bild({ symbol: sym, farbe: GRAU, kopf: 'SERVER', titel: 'Bitte wählen', fuss: '' }); else if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'AUS', titel: adr, fuss: 'Launcher aus' }); else if (i && i.laeuft) b = bild({ symbol: sym, farbe: GRUEN, kopf: '● LÄUFT', titel: adr, fuss: name }); else b = bild({ symbol: sym, farbe: BLAU, kopf: '▶ BEITRETEN', titel: t.settings.serverName || adr, fuss: name }); break; } case AKTION_NEUSTART: if (!id) b = bild({ symbol: sym, farbe: GRAU, kopf: 'NEUSTART', titel: 'Bitte wählen', fuss: '' }); else if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'NEUSTART', titel: name, fuss: 'Launcher aus' }); else if (i && i.laeuft) b = bild({ symbol: sym, farbe: GELB, kopf: '↻ NEUSTART', titel: name, fuss: dauer(i.seit || Date.now()) }); else b = bild({ symbol: sym, farbe: GRAU, kopf: 'NEUSTART', titel: name, fuss: 'läuft nicht' }); break; case AKTION_FENSTER: b = bild({ symbol: sym, farbe: aus ? GRAU : BLAU, kopf: 'LAUNCHER', titel: 'Fenster', fuss: aus ? 'Launcher aus' : 'zeigen / weg' }); break; case AKTION_CLIPMODUS: if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'CLIP-MODUS', titel: 'Puffer', fuss: 'Launcher aus' }); else if (stand.clipStartet) b = bild({ symbol: sym, farbe: GELB, kopf: 'CLIP-MODUS', titel: 'Puffer', fuss: 'startet …' }); else b = stand.clip ? bild({ symbol: sym, farbe: GRUEN, kopf: 'CLIP-MODUS', titel: 'Puffer', fuss: '● an' }) : bild({ symbol: sym, farbe: GRAU, kopf: 'CLIP-MODUS', titel: 'Puffer', fuss: 'aus' }); break; case AKTION_ORDNER: b = bild({ symbol: sym, farbe: aus ? GRAU : BLAU, kopf: 'ORDNER', titel: 'Aufnahmen', fuss: aus ? 'Launcher aus' : (id ? name : '') }); break; case AKTION_SPIELZEIT: { if (aus) { b = bild({ symbol: sym, farbe: GRAU, kopf: 'SPIELZEIT', titel: '–', fuss: 'Launcher aus' }); break; } const liste = id ? (i ? [i] : []) : stand.instanzen || []; const summe = liste.reduce((n, x) => n + (x.gesamt || 0), 0); const lauf = liste.find((x) => x.laeuft && x.seit); b = lauf ? bild({ symbol: sym, farbe: GRUEN, kopf: '● SITZUNG', titel: dauer(lauf.seit), fuss: 'gesamt ' + stunden(summe + Math.floor((Date.now() - lauf.seit) / 1000)) }) : bild({ symbol: sym, farbe: BLAU, kopf: 'SPIELZEIT', titel: stunden(summe), fuss: id ? name : 'alle Instanzen' }); break; } case AKTION_SERVERSTATUS: { const adr = adresse(t); const p = adr && pings.get(adr); if (!adr) b = bild({ symbol: sym, farbe: GRAU, kopf: 'SERVER', titel: 'Bitte wählen', fuss: '' }); else if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'AUS', titel: adr, fuss: 'Launcher aus' }); else if (!p) b = bild({ symbol: sym, farbe: GRAU, kopf: 'SERVER', titel: adr, fuss: 'prüfe …' }); else if (!p.ok) b = bild({ symbol: sym, farbe: ROT, kopf: '● OFFLINE', titel: t.settings.serverName || adr, fuss: '' }); else b = bild({ symbol: sym, farbe: GRUEN, kopf: '● ONLINE', titel: p.online != null ? p.online + ' Spieler' : 'online', fuss: (p.max != null ? 'max ' + p.max + ' · ' : '') + p.latencyMs + ' ms' }); break; } case AKTION_SCHNELL: { const slot = t.settings.slot || '1'; if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'AUS', titel: name, fuss: 'Launcher aus' }); else if (!id || !i) b = bild({ symbol: sym, farbe: GRAU, kopf: 'STRG+' + slot, titel: 'nicht belegt', fuss: '' }); else if (i.laeuft) b = bild({ symbol: sym, farbe: GRUEN, kopf: '● LÄUFT', titel: i.name, fuss: i.seit ? dauer(i.seit) : '■ beenden' }); else b = bild({ symbol: sym, farbe: BLAU, kopf: '▶ STRG+' + slot, titel: i.name, fuss: '' }); break; } case AKTION_KONTO: { const k = stand && (stand.konten || []).find((x) => x.id === t.settings.kontoId); if (!t.settings.kontoId) b = bild({ symbol: sym, farbe: GRAU, kopf: 'KONTO', titel: 'Bitte wählen', fuss: '' }); else if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'AUS', titel: t.name || 'Konto', fuss: 'Launcher aus' }); else if (!k) b = bild({ symbol: sym, farbe: ROT, kopf: 'KONTO', titel: 'nicht gefunden', fuss: '' }); else { t.name = k.name; b = bild({ symbol: sym, farbe: k.aktiv ? GRUEN : BLAU, kopf: k.aktiv ? '● AKTIV' : 'KONTO', titel: k.name, fuss: k.spielt ? 'spielt gerade' : (k.aktiv ? '' : 'wechseln') }); } break; } case AKTION_FREUNDE: { const f = stand && stand.freunde; if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'FREUNDE', titel: '–', fuss: 'Launcher aus' }); else if (!f) b = bild({ symbol: sym, farbe: GRAU, kopf: 'FREUNDE', titel: 'Keine Daten', fuss: 'aus / lädt' }); else b = bild({ symbol: sym, farbe: f.online ? GRUEN : GRAU, kopf: f.online ? '● ONLINE' : 'FREUNDE', titel: f.online + ' online', fuss: 'von ' + f.gesamt }); break; } case AKTION_BACKUP: { const bk = stand && stand.backup; const frisch = bk && bk.zeit && Date.now() - bk.zeit < 600000; if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'BACKUP', titel: 'Jetzt sichern', fuss: 'Launcher aus' }); else if (bk && bk.laeuft) b = bild({ symbol: sym, farbe: GELB, kopf: '● LÄUFT', titel: 'Backup', fuss: 'bitte warten' }); else if (frisch && bk.ok) b = bild({ symbol: sym, farbe: GRUEN, kopf: '✔ FERTIG', titel: 'Backup', fuss: vor(bk.zeit) }); else if (frisch) b = bild({ symbol: sym, farbe: ROT, kopf: 'FEHLER', titel: 'Backup', fuss: vor(bk.zeit) }); else b = bild({ symbol: sym, farbe: BLAU, kopf: 'BACKUP', titel: 'Jetzt sichern', fuss: '' }); break; } case AKTION_LOGS: b = bild({ symbol: sym, farbe: aus ? GRAU : BLAU, kopf: 'LOGS', titel: 'Logs öffnen', fuss: aus ? 'Launcher aus' : (i ? i.name : '') }); break; case AKTION_KONSOLE: b = bild({ symbol: sym, farbe: aus ? GRAU : BLAU, kopf: 'KONSOLE', titel: 'Konsole', fuss: aus ? 'Launcher aus' : '' }); break; case AKTION_ABSTURZ: { const a = id && abstuerze.get(id); if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'ABSTURZ', titel: 'Bericht', fuss: 'Launcher aus' }); else if (!a) b = bild({ symbol: sym, farbe: GRAU, kopf: 'ABSTURZ', titel: 'Bericht', fuss: id ? 'prüfe …' : 'keine Instanz' }); else if (!a.bericht) b = bild({ symbol: sym, farbe: GRAU, kopf: 'ABSTURZ', titel: 'keiner', fuss: i ? i.name : '' }); else if (a.frisch) b = bild({ symbol: sym, farbe: ROT, kopf: '⚠ ABSTURZ', titel: 'Bericht zeigen', fuss: a.berichtZeit ? vor(a.berichtZeit) : '' }); else b = bild({ symbol: sym, farbe: GRAU, kopf: 'ABSTURZ', titel: 'kein neuer', fuss: a.berichtZeit ? 'letzter ' + vor(a.berichtZeit) : '' }); break; } case AKTION_SCREENSHOT: b = bild({ symbol: sym, farbe: aus ? GRAU : BLAU, kopf: 'SCREENSHOT', titel: 'Spielfenster', fuss: aus ? 'Launcher aus' : '' }); break; case AKTION_CLIP: if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'CLIP', titel: 'Clip speichern', fuss: 'Launcher aus' }); else if (stand.clipStartet) b = bild({ symbol: sym, farbe: GELB, kopf: 'CLIP', titel: 'Clip speichern', fuss: 'startet …' }); else b = stand.clip ? bild({ symbol: sym, farbe: GRUEN, kopf: 'CLIP', titel: 'Clip speichern', fuss: 'bereit' }) : bild({ symbol: sym, farbe: GRAU, kopf: 'CLIP', titel: 'Clip speichern', fuss: 'Modus aus' }); break; default: if (aus) b = bild({ symbol: sym, farbe: GRAU, kopf: 'AUFNAHME', titel: 'Aufnahme starten', fuss: 'Launcher aus' }); else b = stand.aufnahme ? bild({ symbol: sym, farbe: ROT, kopf: '● REC', titel: 'Aufnahme stoppen', fuss: '' }) : bild({ symbol: sym, farbe: BLAU, kopf: 'AUFNAHME', titel: 'Aufnahme starten', fuss: '' }); } sende({ event: 'setImage', context, payload: { image: b, target: 0 } }); } const zeichneAlle = () => { for (const c of tasten.keys()) zeichne(c); }; async function pingeFaellige(erzwingen) { const adressen = new Set(); for (const t of tasten.values()) if (t.action === AKTION_SERVERSTATUS && adresse(t)) adressen.add(adresse(t)); for (const adr of adressen) { const alt = pings.get(adr); if (!erzwingen && alt && Date.now() - alt.zeit < PING_MS) continue; const r = await anfrage('GET', '/ping?ip=' + encodeURIComponent(adr)); pings.set(adr, Object.assign({ ok: false }, r || {}, { zeit: Date.now() })); } } async function abfrage() { if (!tasten.size) return; await holeStand(); if (stand) { await holeSymbole(); await pingeFaellige(false); await pollAbstuerze(false); } zeichneAlle(); } // ---------- Stream-Deck-Ereignisse ---------- function zeigeMeldung(context, text) { meldung = { context, text, bis: Date.now() + 2500 }; zeichne(context); setTimeout(() => zeichne(context), 2600); } const FEHLERTEXT = { [AKTION_CLIP]: 'Clip-Modus aus', [AKTION_AUFNAHME]: 'Spiel läuft nicht', [AKTION_CLIPMODUS]: 'Spiel läuft nicht', [AKTION_NEUSTART]: 'Läuft nicht', [AKTION_SERVER]: 'Läuft schon', [AKTION_SCREENSHOT]: 'Kein Spiel', [AKTION_ABSTURZ]: 'Kein Bericht', [AKTION_BACKUP]: 'Läuft schon', [AKTION_SCHNELL]: 'Nicht belegt', }; async function tastendruck(context) { const t = tasten.get(context); if (!t) return; const id = instanzId(t); const i = stand && id ? (stand.instanzen || []).find((x) => x.id === id) : null; let erg; switch (t.action) { case AKTION_INSTANZ: case AKTION_ZULETZT: case AKTION_SCHNELL: case AKTION_DREH: if (!id) { sende({ event: 'showAlert', context }); return; } erg = await anfrage('POST', i && i.laeuft ? '/stop' : '/launch', { id }); break; case AKTION_SCREENSHOT: erg = await anfrage('POST', '/screenshot'); break; case AKTION_FREUNDE: erg = await anfrage('POST', '/open', { art: 'friends' }); break; case AKTION_KONSOLE: erg = await anfrage('POST', '/open', { art: 'console' }); break; case AKTION_LOGS: erg = await anfrage('POST', '/open', { art: 'logs', id: id || '' }); break; case AKTION_ABSTURZ: erg = await anfrage('POST', '/open', { art: 'crash', id: id || '' }); break; case AKTION_BACKUP: erg = await anfrage('POST', '/backup'); break; case AKTION_KONTO: if (!t.settings.kontoId) { sende({ event: 'showAlert', context }); return; } erg = await anfrage('POST', '/account', { id: t.settings.kontoId }); break; case AKTION_SERVER: if (!id || !adresse(t)) { sende({ event: 'showAlert', context }); return; } erg = await anfrage('POST', '/launch', { id, server: adresse(t) }); break; case AKTION_NEUSTART: if (!id) { sende({ event: 'showAlert', context }); return; } erg = await anfrage('POST', '/restart', { id }); break; case AKTION_FENSTER: erg = await anfrage('POST', '/window'); break; case AKTION_CLIPMODUS: erg = await anfrage('POST', '/clipmode'); break; case AKTION_ORDNER: erg = await anfrage('POST', '/folder', id ? { id } : {}); break; case AKTION_SPIELZEIT: zeichne(context); return; case AKTION_SERVERSTATUS: pings.delete(adresse(t)); await pingeFaellige(true); zeichne(context); return; case AKTION_CLIP: if (stand && stand.clipStartet) { zeigeMeldung(context, 'Startet noch …'); return; } erg = await anfrage('POST', '/clip'); break; default: erg = await anfrage('POST', '/record'); } if (erg && erg.ok) sende({ event: 'showOk', context }); else { sende({ event: 'showAlert', context }); zeigeMeldung(context, !erg ? 'Launcher aus' : FEHLERTEXT[t.action] || 'Nicht möglich'); } setTimeout(abfrage, 600); } ws.on('open', () => sende({ event: registerEvent, uuid: pluginUUID })); ws.on('message', async (roh) => { let m; try { m = JSON.parse(roh.toString()); } catch { return; } switch (m.event) { case 'willAppear': tasten.set(m.context, { action: m.action, settings: (m.payload && m.payload.settings) || {} }); await holeStand(); await holeSymbole(); zeichneAlle(); break; case 'willDisappear': tasten.delete(m.context); break; case 'didReceiveSettings': { const t = tasten.get(m.context); if (t) { t.settings = (m.payload && m.payload.settings) || {}; zeichne(m.context); holeSymbole().then(() => pollAbstuerze(true)).then(() => zeichne(m.context)); } break; } case 'keyDown': case 'dialDown': tastendruck(m.context); break; case 'dialRotate': { const t = tasten.get(m.context); const l = dialListe(); if (!t || !l.length) break; const n = l.length; const neu = (((dialIndex(t) + (m.payload.ticks || 0)) % n) + n) % n; t.settings.instanceId = l[neu].id; sende({ event: 'setSettings', context: m.context, payload: t.settings }); zeichneDial(m.context); holeSymbole().then(() => zeichneDial(m.context)); break; } case 'touchTap': // Tippen auf den Touch-Streifen: Launcher-Fenster zeigen bzw. wegminimieren anfrage('POST', '/window'); break; case 'sendToPlugin': { // Eigenschaften-Fenster fragt nach der Instanzliste bzw. den Servern einer Instanz await holeStand(); const payload = { erreichbar: !!stand, instanzen: stand ? stand.instanzen.map((i) => ({ id: i.id, name: i.name })) : [], konten: stand ? (stand.konten || []) : [], schnellstart: stand ? (stand.schnellstart || {}) : {}, }; const frage = m.payload || {}; if (frage.frage === 'server' && frage.id && stand) { const r = await anfrage('GET', '/servers?id=' + encodeURIComponent(frage.id)); payload.server = (r && r.server) || []; } sende({ event: 'sendToPropertyInspector', action: m.action, context: m.context, payload }); break; } default: } }); ws.on('close', () => process.exit(0)); setInterval(abfrage, ABFRAGE_MS);