Files
AeroMc-Launcher/streamdeck-plugin/com.aeromc.launcher.sdPlugin/bin/plugin.js
T
2026-10-30 05:54:28 +00:00

537 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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);