Upload via GUI (324 Dateien)

This commit is contained in:
2026-10-30 05:54:28 +00:00
parent e8b5d6b6e0
commit 9d55faa3eb
18 changed files with 1518 additions and 0 deletions
+189
View File
@@ -0,0 +1,189 @@
'use strict';
/*
* streamdeck.js – lokale Schnittstelle für das AeroMC-Plugin des Elgato Stream Deck
* -----------------------------------------------------------------------------
* Ein kleiner HTTP-Server, der AUSSCHLIESSLICH auf 127.0.0.1 lauscht und nur auf
* Wunsch läuft (Einstellung "streamdeckEnabled", standardmäßig aus).
*
* Absicherung gegen fremde Zugriffe:
* - nur Loopback, kein Zugriff aus dem Netzwerk
* - jede Anfrage braucht den Zufalls-Schlüssel im Kopf "Authorization: Bearer …";
* der wird bei jedem Start neu erzeugt und nur in eine Datei im
* Benutzerordner geschrieben, die das Plugin liest (streamdeck.json)
* - Anfragen mit "Origin"-Kopf (also aus einer Webseite im Browser) und mit
* fremdem "Host"-Kopf (DNS-Rebinding) werden abgewiesen
*
* Die eigentlichen Aktionen kommen von main.js (siehe init) – dieses Modul kennt
* weder Instanzen noch Aufnahmen und reicht nur weiter.
*/
const http = require('http');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const STANDARD_PORT = 48615;
const PORT_VERSUCHE = 10;
const MAX_BODY = 4096;
let deps = null;
let server = null;
let schluessel = null;
let verbindungsDatei = null;
/*
* deps: {
* userDataDir,
* zustand() -> { instanzen: [{ id, name, laeuft, seit }], aufnahme, clip }
* starte(id, server) -> { ok, message? } (server optional: direkt verbinden)
* neustart(id) -> { ok, message? }
* fenster() -> { ok } Launcher-Fenster zeigen bzw. wegminimieren
* clipModus() -> { ok, message? }
* ordner(id) -> { ok } Aufnahmen-Ordner öffnen
* serverListe(id) -> { ok, server: [{ name, ip }] }
* ping(ip) -> { ok, online, max, latencyMs }
* screenshot() -> { ok, message? } Bild des Spielfensters in den Screenshot-Ordner
* symbol(id) -> { ok, bild, pixel } Instanz-Bild als data:-URI
* kopf(uuid) -> { ok, bild } Spielerkopf eines Kontos als data:-URI
* absturz(id) -> { ok, bericht, zeit, frisch } neuester Absturzbericht der Instanz
* konto(id) -> { ok } aktives Konto wechseln
* backup() -> { ok, message? } Sicherung aller Instanzen starten
* oeffne(art, id) -> { ok, message? } Fenster öffnen: friends | logs | console | crash
* beende(id) -> { ok, message? }
* clipSpeichern() -> { ok, message? }
* aufnahmeUmschalten() -> { ok, message? }
* }
*/
function init(d) {
deps = d;
verbindungsDatei = path.join(d.userDataDir, 'streamdeck.json');
}
function istSchluesselGleich(angabe) {
if (!schluessel || typeof angabe !== 'string') return false;
const a = Buffer.from(angabe);
const b = Buffer.from(schluessel);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
function antworte(res, status, daten) {
const text = JSON.stringify(daten);
res.writeHead(status, {
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': Buffer.byteLength(text),
'Cache-Control': 'no-store',
});
res.end(text);
}
function leseBody(req) {
return new Promise((resolve, reject) => {
let groesse = 0;
const teile = [];
req.on('data', (c) => {
groesse += c.length;
if (groesse > MAX_BODY) { reject(new Error('zu groß')); req.destroy(); return; }
teile.push(c);
});
req.on('end', () => {
if (!teile.length) { resolve({}); return; }
try { resolve(JSON.parse(Buffer.concat(teile).toString('utf8')) || {}); } catch { reject(new Error('kein JSON')); }
});
req.on('error', reject);
});
}
async function behandle(req, res, port) {
// Anfragen aus Webseiten (Origin) und DNS-Rebinding (fremder Host) gar nicht erst bedienen
const host = String(req.headers.host || '').toLowerCase();
if (req.headers.origin || (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`)) {
return antworte(res, 403, { ok: false, message: 'Nicht erlaubt.' });
}
const auth = String(req.headers.authorization || '');
if (!auth.startsWith('Bearer ') || !istSchluesselGleich(auth.slice(7))) {
return antworte(res, 401, { ok: false, message: 'Schlüssel fehlt oder ist falsch.' });
}
const url = new URL(req.url || '/', 'http://127.0.0.1');
const pfad = url.pathname;
if (req.method === 'GET' && pfad === '/state') {
return antworte(res, 200, Object.assign({ ok: true }, deps.zustand({ freunde: url.searchParams.get('freunde') === '1' })));
}
if (req.method === 'GET' && pfad === '/servers') {
return antworte(res, 200, await deps.serverListe(url.searchParams.get('id') || ''));
}
if (req.method === 'GET' && pfad === '/icon') {
const konto = url.searchParams.get('konto');
return antworte(res, 200, konto ? await deps.kopf(konto) : await deps.symbol(url.searchParams.get('id') || ''));
}
if (req.method === 'GET' && pfad === '/crash') {
return antworte(res, 200, await deps.absturz(url.searchParams.get('id') || ''));
}
if (req.method === 'GET' && pfad === '/ping') {
return antworte(res, 200, await deps.ping(url.searchParams.get('ip') || ''));
}
if (req.method !== 'POST') return antworte(res, 405, { ok: false, message: 'Nur GET/POST.' });
let body;
try { body = await leseBody(req); } catch (err) { return antworte(res, 400, { ok: false, message: err.message }); }
const id = typeof body.id === 'string' ? body.id : '';
let erg;
if (pfad === '/launch' && id) erg = await deps.starte(id, typeof body.server === 'string' ? body.server.trim().slice(0, 253) : '');
else if (pfad === '/restart' && id) erg = await deps.neustart(id);
else if (pfad === '/window') erg = await deps.fenster();
else if (pfad === '/clipmode') erg = await deps.clipModus();
else if (pfad === '/folder') erg = await deps.ordner(id);
else if (pfad === '/screenshot') erg = await deps.screenshot();
else if (pfad === '/account' && id) erg = await deps.konto(id);
else if (pfad === '/backup') erg = await deps.backup();
else if (pfad === '/open' && typeof body.art === 'string') erg = await deps.oeffne(body.art, id);
else if (pfad === '/stop' && id) erg = await deps.beende(id);
else if (pfad === '/clip') erg = await deps.clipSpeichern();
else if (pfad === '/record') erg = await deps.aufnahmeUmschalten();
else return antworte(res, 404, { ok: false, message: 'Unbekannte Aktion.' });
return antworte(res, erg && erg.ok ? 200 : 409, erg || { ok: false });
}
function schreibeVerbindung(port) {
try {
fs.writeFileSync(verbindungsDatei, JSON.stringify({ port, token: schluessel }), 'utf8');
} catch { /* ohne Datei findet das Plugin den Launcher nicht – mehr geht nicht */ }
}
function loescheVerbindung() {
try { fs.rmSync(verbindungsDatei, { force: true }); } catch { /* egal */ }
}
function lausche(port, versuche) {
const s = http.createServer((req, res) => {
behandle(req, res, port).catch(() => { try { antworte(res, 500, { ok: false }); } catch { /* egal */ } });
});
s.on('error', (err) => {
if (err.code === 'EADDRINUSE' && versuche > 1 && server === s) { lausche(port + 1, versuche - 1); return; }
if (server === s) server = null;
});
server = s;
s.listen(port, '127.0.0.1', () => {
if (server === s) schreibeVerbindung(port);
});
}
function start() {
if (server || !deps) return;
schluessel = crypto.randomBytes(32).toString('hex');
lausche(STANDARD_PORT, PORT_VERSUCHE);
}
function stop() {
if (server) { try { server.close(); } catch { /* egal */ } server = null; }
schluessel = null;
if (verbindungsDatei) loescheVerbindung();
}
function configure(settings) {
if (settings && settings.streamdeckEnabled === true) start(); else stop();
}
module.exports = { init, configure, shutdown: stop };
+82
View File
@@ -0,0 +1,82 @@
'use strict';
/*
* streamdeckplugin.js – das AeroMC-Plugin in die Stream-Deck-Software kopieren
* -----------------------------------------------------------------------------
* Das Plugin (Ordner streamdeck-plugin/com.aeromc.launcher.sdPlugin) liegt im
* Launcher mit dabei. Schaltet jemand "Stream Deck erlauben" ein – und bei jedem
* Start, solange es an ist – legt AeroMC es in den Plugin-Ordner der Stream-Deck-
* Software, falls es dort fehlt oder von einer anderen Fassung stammt. Nach dem
* Kopieren muss die Stream-Deck-Software einmal neu gestartet werden, damit sie
* das Plugin lädt.
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const PLUGIN_NAME = 'com.aeromc.launcher.sdPlugin';
// Ordner mit dem mitgelieferten Plugin; null, wenn er in dieser Fassung fehlt
function quelle(istPaketiert, ressourcenOrdner) {
const kandidat = istPaketiert
? path.join(ressourcenOrdner, 'streamdeck-plugin', PLUGIN_NAME)
: path.join(__dirname, '..', 'streamdeck-plugin', PLUGIN_NAME);
return fs.existsSync(path.join(kandidat, 'manifest.json')) ? kandidat : null;
}
// Plugin-Ordner der Stream-Deck-Software; null, wenn sie nie installiert war
function pluginOrdner() {
const appdata = process.env.APPDATA;
if (!appdata) return null;
const ordner = path.join(appdata, 'Elgato', 'StreamDeck', 'Plugins');
return fs.existsSync(ordner) ? ordner : null;
}
// Was die Stream-Deck-Software selbst im Plugin-Ordner anlegt (Protokolle), zählt nicht mit
const IGNORIERT = new Set(['logs']);
function dateien(basis, rel = '') {
const ergebnis = [];
for (const e of fs.readdirSync(path.join(basis, rel), { withFileTypes: true })) {
if (!rel && IGNORIERT.has(e.name)) continue;
const r = rel ? rel + '/' + e.name : e.name;
if (e.isDirectory()) ergebnis.push(...dateien(basis, r));
else ergebnis.push(r);
}
return ergebnis.sort();
}
// Prüfsumme über Dateinamen und -inhalt - gleiche Prüfsumme = gleicher Stand
function fingerabdruck(ordner) {
const h = crypto.createHash('sha1');
for (const rel of dateien(ordner)) {
h.update(rel + '\0');
h.update(fs.readFileSync(path.join(ordner, ...rel.split('/'))));
}
return h.digest('hex');
}
/*
* -> { ok: true, status: 'installiert' | 'aktualisiert' | 'aktuell', neustart }
* { ok: false, grund: 'quelle-fehlt' | 'kein-streamdeck' | 'kopieren', message }
*/
function installiere(opt) {
const o = opt || {};
if (!o.quelle) return { ok: false, grund: 'quelle-fehlt', message: 'Das Plugin ist in dieser Fassung nicht enthalten.' };
const plugins = o.pluginOrdner !== undefined ? o.pluginOrdner : pluginOrdner();
if (!plugins || !fs.existsSync(plugins)) {
return { ok: false, grund: 'kein-streamdeck', message: 'Die Stream-Deck-Software wurde nicht gefunden.' };
}
const ziel = path.join(plugins, PLUGIN_NAME);
try {
const vorhanden = fs.existsSync(ziel);
if (vorhanden && fingerabdruck(ziel) === fingerabdruck(o.quelle)) return { ok: true, status: 'aktuell', neustart: false };
fs.cpSync(o.quelle, ziel, { recursive: true, force: true, filter: (src) => !IGNORIERT.has(path.basename(src)) || src === o.quelle });
return { ok: true, status: vorhanden ? 'aktualisiert' : 'installiert', neustart: true };
} catch (err) {
return { ok: false, grund: 'kopieren', message: 'Das Plugin konnte nicht kopiert werden (' + err.message + '). Bitte die Stream-Deck-Software beenden und es erneut versuchen.' };
}
}
module.exports = { PLUGIN_NAME, quelle, pluginOrdner, installiere, fingerabdruck };
@@ -0,0 +1,33 @@
{
"name": "bin",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"ws": "8.18.0"
}
},
"node_modules/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}
@@ -0,0 +1 @@
{ "type": "module", "dependencies": { "ws": "8.18.0" } }
@@ -0,0 +1,536 @@
// 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);
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

@@ -0,0 +1,365 @@
{
"Name": "AeroMC",
"Version": "1.1.0.0",
"Author": "M_Viper",
"Description": "Steuert den AeroMC-Launcher: Instanzen starten und beenden, Clips speichern und die Aufnahme umschalten - mit Live-Status auf den Tasten.",
"Category": "AeroMC",
"CategoryIcon": "imgs/plugin/category-icon",
"Icon": "imgs/plugin/marketplace",
"URL": "https://aeromc.viper.ipv64.net",
"SDKVersion": 2,
"UUID": "com.aeromc.launcher",
"Nodejs": {
"Version": "20"
},
"Software": {
"MinimumVersion": "6.5"
},
"OS": [
{
"Platform": "windows",
"MinimumVersion": "10"
}
],
"CodePath": "bin/plugin.js",
"Actions": [
{
"Name": "Instanz starten/beenden",
"UUID": "com.aeromc.launcher.instanz",
"Icon": "imgs/actions/icon",
"Tooltip": "Startet die gewählte Instanz; läuft sie schon, beendet die Taste sie wieder. Zeigt den Zustand live an.",
"PropertyInspectorPath": "ui/instanz.html",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Clip speichern",
"UUID": "com.aeromc.launcher.clip",
"Icon": "imgs/actions/icon",
"Tooltip": "Speichert die letzten Sekunden als Clip. Der Clip-Modus muss laufen (Taste „Clip-Modus ein/aus“).",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Aufnahme starten/stoppen",
"UUID": "com.aeromc.launcher.aufnahme",
"Icon": "imgs/actions/icon",
"Tooltip": "Startet bzw. stoppt die Aufnahme des laufenden Spiels. Das Aufnahme-Fenster öffnet sich dabei unsichtbar von selbst.",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Server beitreten",
"UUID": "com.aeromc.launcher.server",
"Icon": "imgs/actions/icon",
"Tooltip": "Startet die gewählte Instanz und verbindet direkt mit dem gewählten Server.",
"PropertyInspectorPath": "ui/server.html",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Zuletzt gespielte Instanz",
"UUID": "com.aeromc.launcher.zuletzt",
"Icon": "imgs/actions/icon",
"Tooltip": "Startet (bzw. beendet) die Instanz, die du zuletzt gespielt hast – ohne Auswahl.",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Launcher-Fenster",
"UUID": "com.aeromc.launcher.fenster",
"Icon": "imgs/actions/icon",
"Tooltip": "Holt das AeroMC-Fenster nach vorn; ist es schon vorn, wird es minimiert.",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Clip-Modus ein/aus",
"UUID": "com.aeromc.launcher.clipmodus",
"Icon": "imgs/actions/icon",
"Tooltip": "Schaltet den Clip-Puffer des laufenden Spiels ein oder aus. Das Aufnahme-Fenster öffnet sich dabei unsichtbar von selbst.",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Instanz neu starten",
"UUID": "com.aeromc.launcher.neustart",
"Icon": "imgs/actions/icon",
"Tooltip": "Beendet die laufende Instanz sauber und startet sie wieder (mit demselben Server).",
"PropertyInspectorPath": "ui/instanz.html",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Aufnahmen-Ordner",
"UUID": "com.aeromc.launcher.ordner",
"Icon": "imgs/actions/icon",
"Tooltip": "Öffnet den Ordner mit Aufnahmen und Clips im Explorer.",
"PropertyInspectorPath": "ui/instanz.html",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Spielzeit",
"UUID": "com.aeromc.launcher.spielzeit",
"Icon": "imgs/actions/icon",
"Tooltip": "Zeigt die Gesamtspielzeit (einer Instanz oder aller) und während des Spielens die aktuelle Sitzung.",
"PropertyInspectorPath": "ui/instanz.html",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Server-Status",
"UUID": "com.aeromc.launcher.serverstatus",
"Icon": "imgs/actions/icon",
"Tooltip": "Zeigt, ob ein Server online ist, wie viele Spieler drauf sind und den Ping. Aktualisiert alle 30 Sekunden.",
"PropertyInspectorPath": "ui/server.html",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Screenshot",
"UUID": "com.aeromc.launcher.screenshot",
"Icon": "imgs/actions/icon",
"Tooltip": "Speichert ein Bild des Spielfensters im Screenshot-Ordner der laufenden Instanz. Überlagernde Fenster (z. B. das Stream Deck) erscheinen mit auf dem Bild.",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Instanz-Drehregler",
"UUID": "com.aeromc.launcher.dreh",
"Icon": "imgs/actions/icon",
"Tooltip": "Für das Stream Deck+: Drehen wählt die Instanz, Drücken startet bzw. beendet sie, Tippen auf den Streifen zeigt oder minimiert den Launcher.",
"Controllers": [
"Encoder"
],
"Encoder": {
"layout": "$B1",
"TriggerDescription": {
"Rotate": "Instanz wählen",
"Push": "Starten / Beenden",
"Touch": "Launcher-Fenster"
}
},
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Freunde online",
"UUID": "com.aeromc.launcher.freunde",
"Icon": "imgs/actions/icon",
"Tooltip": "Zeigt, wie viele deiner AeroMC-Freunde gerade online sind. Ein Druck öffnet das Freunde-Fenster.",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Konto wechseln",
"UUID": "com.aeromc.launcher.konto",
"Icon": "imgs/actions/icon",
"Tooltip": "Setzt das gewählte Konto als aktives Konto in AeroMC. Zeigt den Spielerkopf; grün = gerade aktiv.",
"PropertyInspectorPath": "ui/konto.html",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Schnellstart 1–5",
"UUID": "com.aeromc.launcher.schnellstart",
"Icon": "imgs/actions/icon",
"Tooltip": "Startet die Instanz, die du in AeroMC auf Strg+1 bis Strg+5 gelegt hast (bzw. beendet sie, wenn sie läuft).",
"PropertyInspectorPath": "ui/schnellstart.html",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Backup jetzt",
"UUID": "com.aeromc.launcher.backup",
"Icon": "imgs/actions/icon",
"Tooltip": "Sichert alle Instanzen. Zeigt „läuft“ und nach dem Ende „fertig“ bzw. „Fehler“.",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Logdateien",
"UUID": "com.aeromc.launcher.logs",
"Icon": "imgs/actions/icon",
"Tooltip": "Öffnet die Logdateien der gewählten (sonst der laufenden oder zuletzt gespielten) Instanz.",
"PropertyInspectorPath": "ui/instanz.html",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Konsole",
"UUID": "com.aeromc.launcher.konsole",
"Icon": "imgs/actions/icon",
"Tooltip": "Öffnet die Konsole von AeroMC.",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
},
{
"Name": "Letzter Absturz",
"UUID": "com.aeromc.launcher.absturz",
"Icon": "imgs/actions/icon",
"Tooltip": "Öffnet den Absturzbericht der gewählten (sonst laufenden/zuletzt gespielten) Instanz. Leuchtet rot, wenn der letzte Start abgestürzt ist.",
"PropertyInspectorPath": "ui/instanz.html",
"Controllers": [
"Keypad"
],
"States": [
{
"Image": "imgs/actions/key",
"TitleAlignment": "bottom",
"ShowTitle": false
}
]
}
]
}
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>AeroMC-Instanz</title>
<style>
body { font-family: "Segoe UI", sans-serif; font-size: 13px; color: #d8d8d8; background: #2d2d2d; margin: 0; padding: 12px; }
label { display: block; margin-bottom: 4px; color: #a8a8a8; }
select { width: 100%; padding: 6px; background: #3d3d3d; color: #fff; border: 1px solid #555; border-radius: 4px; }
.hint { margin-top: 10px; color: #8a8a8a; font-size: 12px; }
</style>
</head>
<body>
<label for="instanz">Instanz</label>
<select id="instanz"><option value="">Lade …</option></select>
<p class="hint" id="hinweis"></p>
<script>
// bei diesen Tasten darf die Instanz leer bleiben (= alle Instanzen)
const OPTIONAL = ['com.aeromc.launcher.spielzeit', 'com.aeromc.launcher.ordner', 'com.aeromc.launcher.logs', 'com.aeromc.launcher.absturz'];
const OPTIONAL_TEXT = { 'com.aeromc.launcher.logs': 'Laufende / zuletzt gespielte', 'com.aeromc.launcher.absturz': 'Laufende / zuletzt gespielte' };
let ws, uuid, aktion, einstellungen = {}, wiederholung = null;
function frageInstanzen() {
if (ws.readyState === 1) ws.send(JSON.stringify({ event: 'sendToPlugin', action: aktion, context: uuid, payload: { frage: 'instanzen' } }));
}
function fuelle(p) {
// solange AeroMC nicht erreichbar ist, alle 2 s neu fragen - ein Neuauswählen der Taste ist dann nicht nötig
if (p.erreichbar) { clearInterval(wiederholung); wiederholung = null; }
else if (!wiederholung) wiederholung = setInterval(frageInstanzen, 2000);
const sel = document.getElementById('instanz');
const hinweis = document.getElementById('hinweis');
sel.innerHTML = '';
if (!p.erreichbar) {
sel.innerHTML = '<option value="">AeroMC nicht erreichbar</option>';
hinweis.textContent = 'AeroMC starten und unter Optionen → Allgemein „Stream Deck erlauben“ einschalten. Dieses Fenster aktualisiert sich dann selbst.';
return;
}
hinweis.textContent = '';
const leer = document.createElement('option');
leer.value = ''; leer.textContent = OPTIONAL.includes(aktion) ? (OPTIONAL_TEXT[aktion] || 'Alle Instanzen') : '– bitte wählen –';
sel.appendChild(leer);
for (const i of p.instanzen) {
const o = document.createElement('option');
o.value = i.id; o.textContent = i.name;
sel.appendChild(o);
}
sel.value = einstellungen.instanceId || '';
}
function connectElgatoStreamDeckSocket(port, inUUID, registerEvent, info, actionInfo) {
uuid = inUUID;
const a = JSON.parse(actionInfo);
aktion = a.action;
einstellungen = (a.payload && a.payload.settings) || {};
ws = new WebSocket('ws://127.0.0.1:' + port);
ws.onopen = () => {
ws.send(JSON.stringify({ event: registerEvent, uuid }));
frageInstanzen();
};
ws.onmessage = (e) => {
const m = JSON.parse(e.data);
if (m.event === 'sendToPropertyInspector') fuelle(m.payload || {});
};
document.getElementById('instanz').addEventListener('change', (ev) => {
einstellungen.instanceId = ev.target.value;
ws.send(JSON.stringify({ event: 'setSettings', context: uuid, payload: einstellungen }));
});
}
</script>
</body>
</html>
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>AeroMC-Konto</title>
<style>
body { font-family: "Segoe UI", sans-serif; font-size: 13px; color: #d8d8d8; background: #2d2d2d; margin: 0; padding: 12px; }
label { display: block; margin-bottom: 4px; color: #a8a8a8; }
select { width: 100%; padding: 6px; background: #3d3d3d; color: #fff; border: 1px solid #555; border-radius: 4px; }
.hint { margin-top: 10px; color: #8a8a8a; font-size: 12px; }
</style>
</head>
<body>
<label for="wahl">Konto</label>
<select id="wahl"><option value="">Lade …</option></select>
<p class="hint" id="hinweis"></p>
<script>
let ws, uuid, aktion, einstellungen = {}, wiederholung = null;
function frage() {
if (ws.readyState === 1) ws.send(JSON.stringify({ event: 'sendToPlugin', action: aktion, context: uuid, payload: { frage: 'liste' } }));
}
function fuelle(p) {
if (p.erreichbar) { clearInterval(wiederholung); wiederholung = null; }
else if (!wiederholung) wiederholung = setInterval(frage, 2000);
const sel = document.getElementById('wahl');
const hinweis = document.getElementById('hinweis');
sel.innerHTML = '';
if (!p.erreichbar) {
sel.innerHTML = '<option value="">AeroMC nicht erreichbar</option>';
hinweis.textContent = 'AeroMC starten und unter Optionen → Allgemein „Stream Deck erlauben“ einschalten. Dieses Fenster aktualisiert sich dann selbst.';
return;
}
hinweis.textContent = '';
const leer = document.createElement('option');
leer.value = ''; leer.textContent = '– bitte wählen –';
sel.appendChild(leer);
for (const k of p.konten) {
const o = document.createElement('option');
o.value = k.id; o.textContent = k.name + (k.aktiv ? ' (aktiv)' : '');
sel.appendChild(o);
}
sel.value = einstellungen.kontoId || '';
}
function connectElgatoStreamDeckSocket(port, inUUID, registerEvent, info, actionInfo) {
uuid = inUUID;
const a = JSON.parse(actionInfo);
aktion = a.action;
einstellungen = (a.payload && a.payload.settings) || {};
ws = new WebSocket('ws://127.0.0.1:' + port);
ws.onopen = () => { ws.send(JSON.stringify({ event: registerEvent, uuid })); frage(); };
ws.onmessage = (e) => {
const m = JSON.parse(e.data);
if (m.event === 'sendToPropertyInspector') fuelle(m.payload || {});
};
document.getElementById('wahl').addEventListener('change', (ev) => {
einstellungen.kontoId = ev.target.value;
ws.send(JSON.stringify({ event: 'setSettings', context: uuid, payload: einstellungen }));
});
}
</script>
</body>
</html>
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>AeroMC-Schnellstart</title>
<style>
body { font-family: "Segoe UI", sans-serif; font-size: 13px; color: #d8d8d8; background: #2d2d2d; margin: 0; padding: 12px; }
label { display: block; margin-bottom: 4px; color: #a8a8a8; }
select { width: 100%; padding: 6px; background: #3d3d3d; color: #fff; border: 1px solid #555; border-radius: 4px; }
.hint { margin-top: 10px; color: #8a8a8a; font-size: 12px; }
</style>
</head>
<body>
<label for="wahl">Schnellstart-Platz (Strg+Zahl in AeroMC)</label>
<select id="wahl"><option value="">Lade …</option></select>
<p class="hint" id="hinweis"></p>
<script>
let ws, uuid, aktion, einstellungen = {}, wiederholung = null;
function frage() {
if (ws.readyState === 1) ws.send(JSON.stringify({ event: 'sendToPlugin', action: aktion, context: uuid, payload: { frage: 'liste' } }));
}
function fuelle(p) {
if (p.erreichbar) { clearInterval(wiederholung); wiederholung = null; }
else if (!wiederholung) wiederholung = setInterval(frage, 2000);
const sel = document.getElementById('wahl');
const hinweis = document.getElementById('hinweis');
sel.innerHTML = '';
if (!p.erreichbar) {
sel.innerHTML = '<option value="">AeroMC nicht erreichbar</option>';
hinweis.textContent = 'AeroMC starten und unter Optionen → Allgemein „Stream Deck erlauben“ einschalten. Dieses Fenster aktualisiert sich dann selbst.';
return;
}
hinweis.textContent = '';
const namen = Object.fromEntries(p.instanzen.map((i) => [i.id, i.name]));
for (const n of ['1', '2', '3', '4', '5']) {
const o = document.createElement('option');
o.value = n;
o.textContent = 'Strg+' + n + ' – ' + (namen[p.schnellstart[n]] || 'nicht belegt');
sel.appendChild(o);
}
if (!einstellungen.slot) einstellungen.slot = '1';
sel.value = einstellungen.slot || '';
}
function connectElgatoStreamDeckSocket(port, inUUID, registerEvent, info, actionInfo) {
uuid = inUUID;
const a = JSON.parse(actionInfo);
aktion = a.action;
einstellungen = (a.payload && a.payload.settings) || {};
ws = new WebSocket('ws://127.0.0.1:' + port);
ws.onopen = () => { ws.send(JSON.stringify({ event: registerEvent, uuid })); frage(); };
ws.onmessage = (e) => {
const m = JSON.parse(e.data);
if (m.event === 'sendToPropertyInspector') fuelle(m.payload || {});
};
document.getElementById('wahl').addEventListener('change', (ev) => {
einstellungen.slot = ev.target.value;
ws.send(JSON.stringify({ event: 'setSettings', context: uuid, payload: einstellungen }));
});
}
</script>
</body>
</html>
@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>AeroMC-Server</title>
<style>
body { font-family: "Segoe UI", sans-serif; font-size: 13px; color: #d8d8d8; background: #2d2d2d; margin: 0; padding: 12px; }
label { display: block; margin-bottom: 4px; color: #a8a8a8; }
select { width: 100%; padding: 6px; background: #3d3d3d; color: #fff; border: 1px solid #555; border-radius: 4px; }
input { width: 100%; box-sizing: border-box; padding: 6px; background: #3d3d3d; color: #fff; border: 1px solid #555; border-radius: 4px; }
.hint { margin-top: 10px; color: #8a8a8a; font-size: 12px; }
</style>
</head>
<body>
<label for="instanz">Instanz</label>
<select id="instanz"><option value="">Lade …</option></select>
<label for="server" style="margin-top:12px">Server dieser Instanz</label>
<select id="server"><option value="">– Instanz wählen –</option></select>
<label for="adresse" style="margin-top:12px">oder Adresse eingeben</label>
<input id="adresse" type="text" placeholder="z. B. mc.example.net:25565" />
<p class="hint" id="hinweis"></p>
<script>
// bei diesen Tasten darf die Instanz leer bleiben (= alle Instanzen)
const OPTIONAL = [];
let ws, uuid, aktion, einstellungen = {}, wiederholung = null;
function frageInstanzen() {
if (ws.readyState !== 1) return;
const id = einstellungen.instanceId;
ws.send(JSON.stringify({ event: 'sendToPlugin', action: aktion, context: uuid, payload: id ? { frage: 'server', id } : { frage: 'instanzen' } }));
}
function speichere() {
ws.send(JSON.stringify({ event: 'setSettings', context: uuid, payload: einstellungen }));
}
function fuelleServer(liste) {
const sel = document.getElementById('server');
sel.innerHTML = '';
const leer = document.createElement('option');
leer.value = ''; leer.textContent = liste.length ? '– bitte wählen –' : 'Keine Server in dieser Instanz';
sel.appendChild(leer);
for (const x of liste) {
const o = document.createElement('option');
o.value = x.ip; o.textContent = (x.name ? x.name + ' – ' : '') + x.ip; o.dataset.name = x.name || '';
sel.appendChild(o);
}
sel.value = liste.some((x) => x.ip === einstellungen.server) ? einstellungen.server : '';
}
function fuelle(p) {
// solange AeroMC nicht erreichbar ist, alle 2 s neu fragen - ein Neuauswählen der Taste ist dann nicht nötig
if (p.erreichbar) { clearInterval(wiederholung); wiederholung = null; }
else if (!wiederholung) wiederholung = setInterval(frageInstanzen, 2000);
const sel = document.getElementById('instanz');
const hinweis = document.getElementById('hinweis');
sel.innerHTML = '';
if (!p.erreichbar) {
sel.innerHTML = '<option value="">AeroMC nicht erreichbar</option>';
hinweis.textContent = 'AeroMC starten und unter Optionen → Allgemein „Stream Deck erlauben“ einschalten. Dieses Fenster aktualisiert sich dann selbst.';
return;
}
hinweis.textContent = '';
const leer = document.createElement('option');
leer.value = ''; leer.textContent = OPTIONAL.includes(aktion) ? 'Alle Instanzen' : '– bitte wählen –';
sel.appendChild(leer);
for (const i of p.instanzen) {
const o = document.createElement('option');
o.value = i.id; o.textContent = i.name;
sel.appendChild(o);
}
sel.value = einstellungen.instanceId || '';
if (p.server) fuelleServer(p.server);
}
function connectElgatoStreamDeckSocket(port, inUUID, registerEvent, info, actionInfo) {
uuid = inUUID;
const a = JSON.parse(actionInfo);
aktion = a.action;
einstellungen = (a.payload && a.payload.settings) || {};
ws = new WebSocket('ws://127.0.0.1:' + port);
ws.onopen = () => {
ws.send(JSON.stringify({ event: registerEvent, uuid }));
frageInstanzen();
};
ws.onmessage = (e) => {
const m = JSON.parse(e.data);
if (m.event === 'sendToPropertyInspector') fuelle(m.payload || {});
};
document.getElementById('adresse').value = einstellungen.adresse || '';
document.getElementById('instanz').addEventListener('change', (ev) => {
einstellungen.instanceId = ev.target.value;
einstellungen.server = ''; einstellungen.serverName = '';
speichere();
fuelleServer([]);
frageInstanzen();
});
document.getElementById('server').addEventListener('change', (ev) => {
einstellungen.server = ev.target.value;
einstellungen.serverName = ev.target.selectedOptions[0] ? ev.target.selectedOptions[0].dataset.name || '' : '';
speichere();
});
document.getElementById('adresse').addEventListener('input', (ev) => {
einstellungen.adresse = ev.target.value.trim();
speichere();
});
}
</script>
</body>
</html>