Upload via GUI (138 Dateien)
This commit is contained in:
+171
@@ -1864,6 +1864,176 @@ ipcMain.handle('skin:currentData', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- Umhänge (offiziell von Mojang + eigene AeroMC-Umhänge) ----------
|
||||
// Bilder immer als data:-Adresse in die Oberfläche (fremde Server schicken
|
||||
// keine CORS-Kopfzeilen – die 3D-Ansicht dürfte sie sonst nicht laden)
|
||||
async function bildAlsDataUri(url) {
|
||||
const res = await fetch(url, { headers: { 'User-Agent': 'AeroMC-Launcher' } });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
const daten = Buffer.from(await res.arrayBuffer());
|
||||
if (daten.length > 6 * 1024 * 1024) throw new Error('Bild zu groß');
|
||||
return 'data:image/png;base64,' + daten.toString('base64');
|
||||
}
|
||||
|
||||
// PNG mit Seitenverhältnis 2:1 (64×32 … 1024×512) – wie der Server es verlangt
|
||||
function umhangPngAusBase64(base64) {
|
||||
const daten = Buffer.from(String(base64 || ''), 'base64');
|
||||
if (daten.length < 33 || !daten.subarray(0, 8).equals(PNG_KENNUNG)) return null;
|
||||
const breite = daten.readUInt32BE(16);
|
||||
const hoehe = daten.readUInt32BE(20);
|
||||
if (![64, 128, 256, 512, 1024].includes(breite) || breite !== 2 * hoehe) return null;
|
||||
return daten;
|
||||
}
|
||||
|
||||
// Umhang eines beliebigen Spielers fürs Anzeigen: eigener AeroMC-Umhang hat
|
||||
// Vorrang, sonst der offizielle aus Mojangs Profil. 5 Minuten gemerkt.
|
||||
const umhangCache = new Map(); // uuid -> { dataUri, quelle, zeit }
|
||||
async function umhangFuer(uuid) {
|
||||
const id = String(uuid || '').replace(/-/g, '').toLowerCase();
|
||||
if (!/^[0-9a-f]{32}$/.test(id)) return null;
|
||||
const gemerkt = umhangCache.get(id);
|
||||
if (gemerkt && Date.now() - gemerkt.zeit < 5 * 60 * 1000) return gemerkt;
|
||||
let ergebnis = { dataUri: null, quelle: null, zeit: Date.now() };
|
||||
try {
|
||||
const aero = await friends.getCapes([id]);
|
||||
const eintrag = aero.ok && aero.capes && aero.capes[id];
|
||||
if (eintrag && eintrag.url) ergebnis = { dataUri: await bildAlsDataUri(eintrag.url), quelle: 'aeromc', zeit: Date.now() };
|
||||
} catch { /* weiter mit Mojang */ }
|
||||
if (!ergebnis.dataUri) {
|
||||
try {
|
||||
const res = await fetch('https://sessionserver.mojang.com/session/minecraft/profile/' + id, { headers: { 'User-Agent': 'AeroMC-Launcher' } });
|
||||
if (res.ok) {
|
||||
const profil = await res.json();
|
||||
const eig = (profil.properties || []).find((p) => p.name === 'textures');
|
||||
const tex = eig ? JSON.parse(Buffer.from(eig.value, 'base64').toString('utf8')) : {};
|
||||
const cape = tex.textures && tex.textures.CAPE;
|
||||
if (cape && cape.url) ergebnis = { dataUri: await bildAlsDataUri(skins.aufHttps(cape.url)), quelle: 'mojang', zeit: Date.now() };
|
||||
}
|
||||
} catch { /* kein Umhang */ }
|
||||
}
|
||||
umhangCache.set(id, ergebnis);
|
||||
return ergebnis;
|
||||
}
|
||||
ipcMain.handle('cape:forUuid', async (_e, uuid) => {
|
||||
const r = await umhangFuer(uuid);
|
||||
return r && r.dataUri ? { dataUri: r.dataUri, quelle: r.quelle } : null;
|
||||
});
|
||||
|
||||
// Übersicht fürs Skin-Fenster: offizielle Umhänge + eigener AeroMC-Umhang
|
||||
ipcMain.handle('cape:info', async () => {
|
||||
const konto = aktivesFreundeKonto();
|
||||
const token = await aktivesToken();
|
||||
if (!konto || !token) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||
const ergebnis = { ok: true, name: konto.username, offiziell: [], aeromc: null, aeromcFehler: '' };
|
||||
try {
|
||||
const p = await skins.holeProfil(token);
|
||||
if (p.ok) ergebnis.offiziell = p.capes || [];
|
||||
} catch { /* ohne offizielle Liste weiter */ }
|
||||
const aero = await friends.getCapes([konto.uuid]);
|
||||
if (aero.ok) ergebnis.aeromc = aero.capes[String(konto.uuid).replace(/-/g, '').toLowerCase()] || null;
|
||||
else { ergebnis.aeromcFehler = aero.message; ergebnis.aeromcFehlt = !!aero.fehlt; }
|
||||
return ergebnis;
|
||||
});
|
||||
|
||||
ipcMain.handle('cape:setOfficial', async (_e, capeId) => {
|
||||
const token = await aktivesToken();
|
||||
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||
try {
|
||||
const res = await skins.setzeUmhang(token, capeId || null);
|
||||
if (res.ok) { umhangCache.clear(); broadcast('skin:changed', { quelle: 'umhang' }); }
|
||||
return res;
|
||||
} catch (err) { return { ok: false, message: 'Umhang konnte nicht gesetzt werden: ' + err.message }; }
|
||||
});
|
||||
|
||||
// Der Umhang-Agent muss als echte Datei vorliegen (Java liest nicht aus app.asar):
|
||||
// einmal nach userData kopieren, Dateiname mit Größe, damit ein Update ihn ersetzt.
|
||||
// Rückgabe: "pfad=serveradresse" für -javaagent, oder '' wenn es nicht klappt.
|
||||
function umhangAgent() {
|
||||
try {
|
||||
const quelle = path.join(__dirname, 'assets', 'agent', 'aeromc-capes.jar');
|
||||
const groesse = fs.statSync(quelle).size;
|
||||
const ordner = path.join(app.getPath('userData'), 'agent');
|
||||
const ziel = path.join(ordner, `aeromc-capes-${groesse}.jar`);
|
||||
if (!fs.existsSync(ziel) || fs.statSync(ziel).size !== groesse) {
|
||||
fs.mkdirSync(ordner, { recursive: true });
|
||||
fs.writeFileSync(ziel, fs.readFileSync(quelle));
|
||||
for (const alt of fs.readdirSync(ordner)) {
|
||||
if (alt !== path.basename(ziel) && /^aeromc-capes-\d+\.jar$/.test(alt)) { try { fs.unlinkSync(path.join(ordner, alt)); } catch { /* noch in Benutzung */ } }
|
||||
}
|
||||
}
|
||||
return `${ziel}=${friends.BASIS_URL}`;
|
||||
} catch (err) {
|
||||
console.error('Umhang-Agent nicht verfügbar:', err.message);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// verständliche Meldung, wenn der Server das neue Plugin noch nicht hat
|
||||
function umhangFehler(res) {
|
||||
if (res && res.code === 'rest_no_route') return { ok: false, message: 'Der Server kennt noch keine Umhänge (Plugin „AeroMC Freunde“ 1.5.0 nötig).' };
|
||||
return res;
|
||||
}
|
||||
|
||||
ipcMain.handle('cape:upload', async (_e, base64) => {
|
||||
const konto = aktivesFreundeKonto();
|
||||
if (!konto) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||
if (!umhangPngAusBase64(base64)) return { ok: false, message: 'Ungültiger Umhang (PNG mit 64×32 bis 1024×512 Pixeln nötig).' };
|
||||
const res = await friends.uploadCape(konto.uuid, base64);
|
||||
if (res.ok) { umhangCache.clear(); broadcast('skin:changed', { quelle: 'umhang' }); }
|
||||
return res.ok ? { ok: true, cape: res.data && res.data.cape } : umhangFehler(res);
|
||||
});
|
||||
|
||||
ipcMain.handle('cape:remove', async () => {
|
||||
const konto = aktivesFreundeKonto();
|
||||
if (!konto) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||
const res = await friends.removeCape(konto.uuid);
|
||||
if (res.ok) { umhangCache.clear(); broadcast('skin:changed', { quelle: 'umhang' }); }
|
||||
return res.ok ? res : umhangFehler(res);
|
||||
});
|
||||
|
||||
// für den Editor: eigener Umhang, sonst der getragene offizielle
|
||||
ipcMain.handle('cape:currentData', async () => {
|
||||
const konto = aktivesFreundeKonto();
|
||||
if (!konto) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||
umhangCache.delete(String(konto.uuid).replace(/-/g, '').toLowerCase());
|
||||
const r = await umhangFuer(konto.uuid);
|
||||
if (!r || !r.dataUri) return { ok: false, keiner: true, message: 'Dein Konto hat noch keinen Umhang.' };
|
||||
return { ok: true, dataUri: r.dataUri, quelle: r.quelle };
|
||||
});
|
||||
|
||||
// Bild wählen: art 'umhang' = fertige Umhang-Datei (PNG), 'bild' = beliebiges Motiv zum Einsetzen
|
||||
ipcMain.handle('cape:chooseFile', async (e, art) => {
|
||||
const motiv = art === 'bild';
|
||||
const res = await dialog.showOpenDialog(BrowserWindow.fromWebContents(e.sender) || mainWindow, {
|
||||
title: motiv ? 'Bild für den Umhang wählen' : 'Umhang-Datei öffnen',
|
||||
properties: ['openFile'],
|
||||
filters: motiv
|
||||
? [{ name: 'Bilder', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'] }]
|
||||
: [{ name: 'PNG-Bild', extensions: ['png'] }],
|
||||
});
|
||||
if (res.canceled || !res.filePaths.length) return { ok: false, canceled: true };
|
||||
const pfad = res.filePaths[0];
|
||||
try {
|
||||
const st = fs.statSync(pfad);
|
||||
if (st.size > 15 * 1024 * 1024) return { ok: false, message: 'Die Datei ist zu groß (über 15 MB).' };
|
||||
const endung = path.extname(pfad).slice(1).toLowerCase().replace('jpg', 'jpeg');
|
||||
return { ok: true, pfad, dataUri: `data:image/${endung || 'png'};base64,` + fs.readFileSync(pfad).toString('base64') };
|
||||
} catch (err) { return { ok: false, message: 'Datei nicht lesbar: ' + err.message }; }
|
||||
});
|
||||
|
||||
ipcMain.handle('cape:saveData', async (e, base64) => {
|
||||
const daten = umhangPngAusBase64(base64);
|
||||
if (!daten) return { ok: false, message: 'Ungültige Umhang-Daten.' };
|
||||
const res = await dialog.showSaveDialog(BrowserWindow.fromWebContents(e.sender) || mainWindow, {
|
||||
title: 'Umhang speichern',
|
||||
defaultPath: path.join(app.getPath('pictures'), 'umhang.png'),
|
||||
filters: [{ name: 'PNG-Bild', extensions: ['png'] }],
|
||||
});
|
||||
if (res.canceled || !res.filePath) return { ok: false, canceled: true };
|
||||
try { fs.writeFileSync(res.filePath, daten); return { ok: true, pfad: res.filePath }; }
|
||||
catch (err) { return { ok: false, message: 'Speichern fehlgeschlagen: ' + err.message }; }
|
||||
});
|
||||
|
||||
ipcMain.handle('skin:readData', (_e, dateiPfad) => {
|
||||
const geprueft = skins.pruefeDatei(dateiPfad);
|
||||
if (!geprueft.ok) return geprueft;
|
||||
@@ -3602,6 +3772,7 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => {
|
||||
if (settings.autoModUpdates) modUpdates = await modUpdatesVorDemStart(id).catch(() => null);
|
||||
|
||||
const opts = {
|
||||
javaAgent: settings.aeromcCapes === false ? '' : umhangAgent(),
|
||||
versionId: inst.minecraft.version,
|
||||
loader: inst.minecraft.loader || 'vanilla',
|
||||
loaderVersion: inst.minecraft.loaderVersion || '',
|
||||
|
||||
Reference in New Issue
Block a user