Upload via GUI (194 Dateien)

This commit is contained in:
2026-10-07 07:32:02 +00:00
parent 7e7a2021ed
commit 3ac5a6c14d
20 changed files with 2196 additions and 489 deletions
+359
View File
@@ -0,0 +1,359 @@
'use strict';
/*
* spieldesign.js – AeroMC-Look im Spiel (Minecraft 1.14 … 26.x)
* --------------------------------------------------------------
* Zwei Teile:
*
* 1. Ressourcenpaket "AeroMC-Design" im Ordner resourcepacks der Instanz:
* Titel-Logo mit Untertitel, Menü-Hintergrund (Panorama), eigene Sprüche,
* Knöpfe im AeroMC-Stil und Symbole vor den Menü-Beschriftungen (als eigene
* Bitmap-Schrift über die Sprachdateien). Bis 1.20.1 stecken die Knöpfe im
* gemeinsamen widgets.png der Version, ab 1.20.2 sind es eigene Sprites.
* Das Paket wird in options.txt ganz unten eingereiht – eigene
* Ressourcenpakete des Spielers haben also immer Vorrang.
* 2. Grafiken für den Agenten (Ladebildschirm-Logo, Fenstersymbol): Java kann
* nicht aus app.asar lesen, darum werden sie nach userData kopiert.
*
* Für den Mod-Schalter (siehe modschalter.js) bringt das Paket außerdem die
* Beschriftung „Mods verwalten“ mit – auch wenn der AeroMC-Look aus ist.
* Ist beides aus, wird das Paket wieder entfernt.
*/
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const unzipper = require('unzipper');
const PAKET = 'AeroMC-Design';
const PAKET_ID = 'file/' + PAKET;
const STAND = 8; // erhöhen, wenn sich die Grafiken ändern -> Paket wird neu gebaut
const ASSETS = path.join(__dirname, 'assets', 'spieldesign');
// ---------- Versionen ----------
// "1.21.9-rc1" -> [1, 21, 9]; Snapshots wie "25w31a" -> null
function zahlen(version) {
const m = /^(\d+)\.(\d+)(?:\.(\d+))?/.exec(String(version || ''));
return m ? [Number(m[1]), Number(m[2]), Number(m[3] || 0)] : null;
}
function ab(version, grenze) {
const a = zahlen(version);
if (!a) return true; // Snapshots u. Ä. gelten als neu
const b = zahlen(grenze);
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
return true;
}
// Ressourcenpaket-Format je Version (minecraft.wiki, "Pack format")
const FORMATE = [
['1.14', 4], ['1.15', 5], ['1.16.2', 6], ['1.17', 7], ['1.18', 8], ['1.19', 9], ['1.19.3', 12], ['1.19.4', 13],
['1.20', 15], ['1.20.2', 18], ['1.20.3', 22], ['1.20.5', 32], ['1.21', 34], ['1.21.2', 42], ['1.21.4', 46],
['1.21.5', 55], ['1.21.6', 63], ['1.21.7', 64], ['1.21.9', 69], ['1.21.11', 75], ['26.1', 84], ['26.2', 88], ['26.3', 97],
];
function paketFormat(version) {
let f = FORMATE[FORMATE.length - 1][1];
if (zahlen(version)) for (const [v, n] of FORMATE) if (ab(version, v)) f = n;
return f;
}
function mcmeta(version) {
const f = paketFormat(version);
const neuste = f === FORMATE[FORMATE.length - 1][1];
const pack = { description: 'AeroMC – Menü, Logo und Knöpfe' };
if (f >= 65) {
// ab 1.21.9: min/max statt pack_format; die neuste bekannte Version offen nach oben
pack.min_format = f;
pack.max_format = neuste ? 999 : f;
} else {
pack.pack_format = f;
}
return JSON.stringify({ pack }, null, 2);
}
/** Spieldesign gibt es ab 1.14 (vorher andere Menüs und LWJGL 2). */
function unterstuetzt(version) {
return ab(version, '1.14');
}
// ---------- PNG (verlustfrei, ohne Zusatzbibliothek) ----------
function pngLesen(buf) {
if (buf.readUInt32BE(0) !== 0x89504e47) throw new Error('kein PNG');
let pos = 8, w = 0, h = 0, tiefe = 0, typ = 0, palette = null, transparenz = null;
const idat = [];
while (pos < buf.length) {
const laenge = buf.readUInt32BE(pos);
const art = buf.toString('ascii', pos + 4, pos + 8);
const daten = buf.subarray(pos + 8, pos + 8 + laenge);
if (art === 'IHDR') {
w = daten.readUInt32BE(0); h = daten.readUInt32BE(4); tiefe = daten[8]; typ = daten[9];
if (daten[12] !== 0) throw new Error('Interlacing nicht unterstützt');
} else if (art === 'PLTE') palette = daten;
else if (art === 'tRNS') transparenz = daten;
else if (art === 'IDAT') idat.push(daten);
else if (art === 'IEND') break;
pos += 12 + laenge;
}
const kanaele = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[typ];
if (!kanaele || (tiefe !== 8 && !(typ === 3 && tiefe < 8) && !(typ === 0 && tiefe < 8))) throw new Error(`PNG-Format ${typ}/${tiefe} nicht unterstützt`);
const roh = zlib.inflateSync(Buffer.concat(idat));
const bpp = Math.max(1, (kanaele * tiefe) >> 3); // Bytes je Pixel für die Filter
const zeile = Math.ceil((w * kanaele * tiefe) / 8);
const pixel = Buffer.alloc(zeile * h);
for (let y = 0; y < h; y++) {
const f = roh[y * (zeile + 1)];
const ein = roh.subarray(y * (zeile + 1) + 1, (y + 1) * (zeile + 1));
const aus = pixel.subarray(y * zeile, (y + 1) * zeile);
const oben = y ? pixel.subarray((y - 1) * zeile, y * zeile) : null;
for (let i = 0; i < zeile; i++) {
const a = i >= bpp ? aus[i - bpp] : 0, b = oben ? oben[i] : 0, c = oben && i >= bpp ? oben[i - bpp] : 0;
let v = ein[i];
if (f === 1) v += a;
else if (f === 2) v += b;
else if (f === 3) v += (a + b) >> 1;
else if (f === 4) { const p = a + b - c, pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c); v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c; }
aus[i] = v & 255;
}
}
const rgba = Buffer.alloc(w * h * 4);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const o = (y * w + x) * 4;
let wert;
if (tiefe < 8) {
const bit = x * tiefe;
wert = (pixel[y * zeile + (bit >> 3)] >> (8 - tiefe - (bit & 7))) & ((1 << tiefe) - 1);
} else wert = null;
const p = y * zeile + x * kanaele;
if (typ === 6) { pixel.copy(rgba, o, p, p + 4); continue; }
if (typ === 2) { rgba[o] = pixel[p]; rgba[o + 1] = pixel[p + 1]; rgba[o + 2] = pixel[p + 2]; rgba[o + 3] = 255; continue; }
if (typ === 4) { rgba[o] = rgba[o + 1] = rgba[o + 2] = pixel[p]; rgba[o + 3] = pixel[p + 1]; continue; }
if (typ === 0) {
const g = tiefe < 8 ? Math.round((wert * 255) / ((1 << tiefe) - 1)) : pixel[p];
rgba[o] = rgba[o + 1] = rgba[o + 2] = g; rgba[o + 3] = 255; continue;
}
const index = tiefe < 8 ? wert : pixel[p]; // Palette
rgba[o] = palette[index * 3]; rgba[o + 1] = palette[index * 3 + 1]; rgba[o + 2] = palette[index * 3 + 2];
rgba[o + 3] = transparenz && index < transparenz.length ? transparenz[index] : 255;
}
}
return { w, h, rgba };
}
const CRC = (() => {
const t = new Int32Array(256);
for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; t[n] = c; }
return t;
})();
function crc32(buf) {
let c = -1;
for (let i = 0; i < buf.length; i++) c = CRC[(c ^ buf[i]) & 255] ^ (c >>> 8);
return (c ^ -1) >>> 0;
}
function block(art, daten) {
const kopf = Buffer.alloc(8);
kopf.writeUInt32BE(daten.length, 0);
kopf.write(art, 4, 'ascii');
const pruef = Buffer.alloc(4);
pruef.writeUInt32BE(crc32(Buffer.concat([kopf.subarray(4), daten])), 0);
return Buffer.concat([kopf, daten, pruef]);
}
function pngSchreiben(w, h, rgba) {
const roh = Buffer.alloc((w * 4 + 1) * h);
for (let y = 0; y < h; y++) rgba.copy(roh, y * (w * 4 + 1) + 1, y * w * 4, (y + 1) * w * 4);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr[8] = 8; ihdr[9] = 6;
return Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
block('IHDR', ihdr), block('IDAT', zlib.deflateSync(roh, { level: 9 })), block('IEND', Buffer.alloc(0)),
]);
}
// ---------- Paket bauen ----------
async function ausJar(jar, eintraege) {
const verz = await unzipper.Open.file(jar);
const aus = {};
for (const e of eintraege) {
const f = verz.files.find((d) => d.path === e);
if (f) aus[e] = await f.buffer();
}
return aus;
}
function schreibe(datei, inhalt) {
fs.mkdirSync(path.dirname(datei), { recursive: true });
fs.writeFileSync(datei, inhalt);
}
async function bauePaket(gameDir, version, clientJar, protokoll, teile) {
const { design, mods } = teile;
const ziel = path.join(gameDir, 'resourcepacks', PAKET);
const marke = path.join(ziel, 'aeromc.json');
const kennung = JSON.stringify({ stand: STAND, version, design: !!design, mods: !!mods });
try { if (fs.readFileSync(marke, 'utf8') === kennung) return; } catch { /* noch nicht gebaut */ }
fs.rmSync(ziel, { recursive: true, force: true });
const alt = !ab(version, '1.20'); // Titel-Logo zweigeteilt bis 1.19.4
const gui = path.join(ziel, 'assets', 'minecraft', 'textures', 'gui');
const asset = (n) => fs.readFileSync(path.join(ASSETS, n));
schreibe(path.join(ziel, 'pack.png'), asset('pack.png'));
schreibe(path.join(ziel, 'pack.mcmeta'), mcmeta(version));
// Symbole (Schriftzeichen) und Beschriftungen – für Menü-Knöpfe und/oder „Mods verwalten“
schreibe(path.join(ziel, 'assets', 'aeromc', 'textures', 'font', 'symbole.png'), asset('symbole.png'));
// Die Schriftdatei ersetzt die von Minecraft (zumindest in neueren Versionen) –
// darum die Original-Einträge dieser Version übernehmen und die Symbole davorsetzen
try {
const eintrag = 'assets/minecraft/font/default.json';
const orig = JSON.parse(String((await ausJar(clientJar, [eintrag]))[eintrag] || '{"providers":[]}'));
const eigene = JSON.parse(String(asset('font_default.json'))).providers;
schreibe(path.join(ziel, 'assets', 'minecraft', 'font', 'default.json'),
JSON.stringify({ providers: [...eigene, ...(orig.providers || [])] }, null, 2));
} catch (err) {
if (protokoll) protokoll('[AeroMC] Symbole nicht eingebunden: ' + err.message);
}
for (const sprache of ['de_de', 'en_us']) {
const texte = Object.assign({},
design ? JSON.parse(String(asset(`lang_${sprache}.json`))) : {},
mods ? JSON.parse(String(asset(`lang_mods_${sprache}.json`))) : {});
schreibe(path.join(ziel, 'assets', 'minecraft', 'lang', sprache + '.json'), JSON.stringify(texte, null, 2));
}
// 26.x: „Feedback geben“ ist ein Symbol-Knopf -> Puzzle-Symbol für „Mods verwalten“
if (mods) schreibe(path.join(gui, 'sprites', 'pause_menu', 'social_interactions.png'), asset('mods_knopf.png'));
if (!design) { schreibe(marke, kennung); return; }
const titel = fs.readFileSync(path.join(ASSETS, alt ? 'titel_alt.png' : 'titel_neu.png'));
schreibe(path.join(gui, 'title', 'minecraft.png'), titel);
schreibe(path.join(gui, 'title', 'minceraft.png'), titel);
schreibe(path.join(gui, 'title', 'edition.png'), fs.readFileSync(path.join(ASSETS, alt ? 'edition_alt.png' : 'edition_neu.png')));
// Hintergrund des Hauptmenüs (Minecraft dreht ihn als Würfel um die Kamera)
for (let i = 0; i < 6; i++) {
schreibe(path.join(gui, 'title', 'background', `panorama_${i}.png`), fs.readFileSync(path.join(ASSETS, `panorama_${i}.png`)));
}
schreibe(path.join(ziel, 'assets', 'minecraft', 'texts', 'splashes.txt'), asset('splashes.txt'));
// Knöpfe wie im Entwurf (dunkles Schiefer, hervorgehoben leuchtend blau)
try {
if (ab(version, '1.20.2')) {
// eigene Sprites (doppelte Auflösung) samt Neun-Teile-Skalierung
const w = path.join(gui, 'sprites', 'widget');
schreibe(path.join(w, 'button.png'), asset('knopf.png'));
schreibe(path.join(w, 'button_highlighted.png'), asset('knopf_hell.png'));
schreibe(path.join(w, 'button.png.mcmeta'), asset('knopf.png.mcmeta'));
schreibe(path.join(w, 'button_highlighted.png.mcmeta'), asset('knopf.png.mcmeta'));
} else {
// widgets.png enthält auch Hotbar & Co. – Original 4-fach vergrößern (verlustfrei)
// und nur die Knopf-Zeilen durch die neuen ersetzen (66–86 normal, 86–106 hervorgehoben)
const e = 'assets/minecraft/textures/gui/widgets.png';
const daten = (await ausJar(clientJar, [e]))[e];
if (daten) {
const orig = pngLesen(daten);
const f = Math.max(1, Math.round(1024 / orig.w));
const W = orig.w * f, H = orig.h * f;
const gross = Buffer.alloc(W * H * 4);
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) orig.rgba.copy(gross, (y * W + x) * 4, (((y / f) | 0) * orig.w + ((x / f) | 0)) * 4, (((y / f) | 0) * orig.w + ((x / f) | 0)) * 4 + 4);
}
const s = W / 256;
for (const [datei, zeile] of [['knopf_4x.png', 66], ['knopf_hell_4x.png', 86]]) {
const k = pngLesen(asset(datei));
const kb = Math.round(200 * s), kh = Math.round(20 * s), y0 = Math.round(zeile * s);
for (let y = 0; y < kh; y++) {
for (let x = 0; x < kb; x++) {
const q = (((y * k.h / kh) | 0) * k.w + ((x * k.w / kb) | 0)) * 4;
k.rgba.copy(gross, ((y0 + y) * W + x) * 4, q, q + 4);
}
}
}
schreibe(path.join(gui, 'widgets.png'), pngSchreiben(W, H, gross));
}
}
} catch (err) {
if (protokoll) protokoll('[AeroMC] Knöpfe nicht ersetzt: ' + err.message);
}
schreibe(marke, kennung);
}
// ---------- options.txt ----------
function liesListe(zeilen, schluessel) {
const z = zeilen.find((l) => l.startsWith(schluessel + ':'));
if (!z) return null;
try { const v = JSON.parse(z.slice(schluessel.length + 1)); return Array.isArray(v) ? v : []; } catch { return []; }
}
function setzeListe(zeilen, schluessel, liste) {
const i = zeilen.findIndex((l) => l.startsWith(schluessel + ':'));
const neu = schluessel + ':' + JSON.stringify(liste);
if (i >= 0) zeilen[i] = neu; else zeilen.push(neu);
}
/** Paket in options.txt ein- bzw. austragen. Rückgabe: ob sich etwas geändert hat. */
function trageEin(gameDir, an) {
const datei = path.join(gameDir, 'options.txt');
let text = '';
try { text = fs.readFileSync(datei, 'utf8'); } catch { if (!an) return false; }
const zeilen = text ? text.split(/\r?\n/) : [];
while (zeilen.length && zeilen[zeilen.length - 1] === '') zeilen.pop();
const aktiv = liesListe(zeilen, 'resourcePacks') || ['vanilla'];
const unvertraeglich = liesListe(zeilen, 'incompatibleResourcePacks') || [];
const vorher = JSON.stringify([aktiv, unvertraeglich]);
const ohne = (l) => l.filter((p) => p !== PAKET_ID && p !== PAKET);
let a = ohne(aktiv), u = ohne(unvertraeglich);
if (an) {
// direkt über den eingebauten Paketen, unter allen eigenen Ressourcenpaketen
let pos = a.findIndex((p) => String(p).startsWith('file/'));
if (pos < 0) pos = a.length;
if (!a.includes('vanilla')) { a.unshift('vanilla'); pos++; }
a.splice(Math.max(pos, a.indexOf('vanilla') + 1), 0, PAKET_ID);
// NICHT in incompatibleResourcePacks eintragen: Steht ein passendes Paket dort,
// streicht Minecraft es aus der Liste und lässt es dabei für diesen Start weg
}
if (JSON.stringify([a, u]) === vorher) return false;
if (liesListe(zeilen, 'resourcePacks') || an) setzeListe(zeilen, 'resourcePacks', a);
if (liesListe(zeilen, 'incompatibleResourcePacks')) setzeListe(zeilen, 'incompatibleResourcePacks', u);
fs.writeFileSync(datei, zeilen.join('\n') + '\n');
return true;
}
// ---------- Einstieg ----------
/**
* Vor jedem Spielstart: Paket anlegen und eintragen bzw. (ausgeschaltet)
* wieder entfernen. Fehler brechen den Start nie ab.
*/
async function vorbereiten({ gameDir, version, clientJar, an, mods, protokoll }) {
try {
if ((an || mods) && unterstuetzt(version)) {
await bauePaket(gameDir, version, clientJar, protokoll, { design: !!an, mods: !!mods });
trageEin(gameDir, true);
} else {
if (trageEin(gameDir, false) || fs.existsSync(path.join(gameDir, 'resourcepacks', PAKET))) {
fs.rmSync(path.join(gameDir, 'resourcepacks', PAKET), { recursive: true, force: true });
}
}
} catch (err) {
if (protokoll) protokoll('[AeroMC] Spieldesign nicht vorbereitet: ' + err.message);
}
}
/** Grafiken für den Agenten nach userData kopieren; Rückgabe: Ordner oder '' */
function agentOrdner(userData) {
try {
const ordner = path.join(userData, 'agent', 'spieldesign');
fs.mkdirSync(ordner, { recursive: true });
for (const n of fs.readdirSync(ASSETS)) {
if (!/^(ladelogo(_alt)?\.png|icon_\d+\.rgba)$/.test(n)) continue;
const q = path.join(ASSETS, n), z = path.join(ordner, n);
const daten = fs.readFileSync(q);
let gleich = false;
try { gleich = fs.readFileSync(z).equals(daten); } catch { /* fehlt noch */ }
if (!gleich) fs.writeFileSync(z, daten);
}
return ordner;
} catch {
return '';
}
}
module.exports = { vorbereiten, agentOrdner, unterstuetzt, paketFormat, mcmeta, pngLesen, pngSchreiben, trageEin, PAKET_ID };