537 lines
25 KiB
JavaScript
537 lines
25 KiB
JavaScript
// 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) => `<text x="72" y="${92 + i * 20}" font-size="18" font-weight="700" fill="#fff" text-anchor="middle">${esc(z)}</text>`).join('');
|
||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="144" height="144" viewBox="0 0 144 144" font-family="Segoe UI, Arial, sans-serif">`
|
||
+ `<rect width="144" height="144" rx="14" fill="#14161f"/>`
|
||
+ (sym ? `<image href="${sym.bild}" x="48" y="4" width="48" height="48" preserveAspectRatio="xMidYMid meet"${sym.pixel ? ' style="image-rendering:pixelated"' : ''}/>` : '')
|
||
+ `<text x="72" y="68" font-size="14" font-weight="700" fill="${farbe}" text-anchor="middle">${esc(kopf)}</text>`
|
||
+ texte
|
||
+ `<text x="72" y="134" font-size="14" fill="#aab" text-anchor="middle">${esc(fuss)}</text>`
|
||
+ `<rect x="0" y="138" width="144" height="6" fill="${farbe}"/></svg>`;
|
||
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);
|