v3.1.0: Banner-Töne als kleine Melodien (App und Shop) statt Piepen; HOME-Menü-Design wieder entfernt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
17c4c7a074
commit
59e082e7d5
@@ -17,7 +17,7 @@ SOURCES := source
|
||||
INCLUDES := source
|
||||
|
||||
# Bei jedem Release erhöhen - das Auto-Update vergleicht diese Nummer
|
||||
VERSION := 3.0.9
|
||||
VERSION := 3.1.0
|
||||
|
||||
APP_TITLE := MBMNET
|
||||
APP_DESCRIPTION := Dein eigenes 3DS-Netzwerk
|
||||
|
||||
Binary file not shown.
+3
-17
@@ -33,24 +33,13 @@ if [ "$NUR_RELEASE" = 0 ]; then
|
||||
(cd "$RELEASE" && git add mbmnet.3dsx mbmnet.cia mbmnet-shop.3dsx mbmnet-shop.cia mbmnet-installieren.gm9 version.txt && git commit -m "MBMNET-App v$VERSION" && git push)
|
||||
fi
|
||||
|
||||
# --- MBMNET-Design fürs HOME-Menü (eigene Töne, Standard-Aussehen; für Anemone3DS) --------------
|
||||
THEME_TMP=$(mktemp -d)
|
||||
node theme/build-theme.js "$THEME_TMP" >/dev/null
|
||||
docker run --rm -v "$PWD":/app -v "$THEME_TMP":/out -w /app mbmnet-3ds-build \
|
||||
bannertool makesmdh -s "MBMNET" -l "MBMNET-Design: eigene Töne im HOME-Menü" -p "MBMNET" -i icon.png -o /out/MBMNET/info.smdh >/dev/null
|
||||
DESIGNZIP="$(cd "$RELEASE" && pwd)/mbmnet-design.zip"
|
||||
rm -f "$DESIGNZIP"
|
||||
(cd "$THEME_TMP/MBMNET" && zip -q "$DESIGNZIP" body_LZ.bin info.smdh preview.png)
|
||||
|
||||
# --- SD-Karten-Paket (wie bei MSET9/Hacks-Guide: am PC auf die SD entpacken, dann GM9-Skript starten) ---
|
||||
SDZIP="$(cd "$RELEASE" && pwd)/mbmnet-sd.zip"
|
||||
SDTMP=$(mktemp -d)
|
||||
mkdir -p "$SDTMP/cias" "$SDTMP/gm9/scripts" "$SDTMP/3ds" "$SDTMP/Themes"
|
||||
mkdir -p "$SDTMP/cias" "$SDTMP/gm9/scripts" "$SDTMP/3ds"
|
||||
cp "$RELEASE/mbmnet.cia" "$RELEASE/mbmnet-shop.cia" "$SDTMP/cias/"
|
||||
cp "$RELEASE/mbmnet-installieren.gm9" "$SDTMP/gm9/scripts/"
|
||||
cp "$RELEASE/mbmnet.3dsx" "$SDTMP/3ds/"
|
||||
cp "$DESIGNZIP" "$SDTMP/Themes/MBMNET-Design.zip"
|
||||
rm -rf "$THEME_TMP"
|
||||
cat > "$SDTMP/LIESMICH.txt" <<'TXT'
|
||||
MBMNET - Installation
|
||||
|
||||
@@ -63,12 +52,9 @@ MBMNET - Installation
|
||||
|
||||
Ohne GodMode9: mbmnet.cia mit FBI installieren, oder 3ds/mbmnet.3dsx
|
||||
über den Homebrew Launcher starten.
|
||||
|
||||
Freiwillig: Themes/MBMNET-Design.zip ist ein HOME-Menü-Design mit eigenen Tönen
|
||||
(Aussehen bleibt Standard). Mit Anemone3DS installieren.
|
||||
TXT
|
||||
rm -f "$SDZIP"
|
||||
(cd "$SDTMP" && zip -qr "$SDZIP" LIESMICH.txt cias gm9 3ds Themes)
|
||||
(cd "$SDTMP" && zip -qr "$SDZIP" LIESMICH.txt cias gm9 3ds)
|
||||
rm -rf "$SDTMP"
|
||||
echo "SD-Paket: $(du -h "$SDZIP" | cut -f1)"
|
||||
|
||||
@@ -96,7 +82,7 @@ else
|
||||
fi
|
||||
|
||||
# Vorhandene Dateien gleichen Namens löschen (bei erneutem Lauf), dann hochladen
|
||||
for f in mbmnet-sd.zip mbmnet-design.zip mbmnet.cia mbmnet.3dsx mbmnet-shop.cia mbmnet-shop.3dsx mbmnet-installieren.gm9; do
|
||||
for f in mbmnet-sd.zip mbmnet.cia mbmnet.3dsx mbmnet-shop.cia mbmnet-shop.3dsx mbmnet-installieren.gm9; do
|
||||
[ -f "$RELEASE/$f" ] || continue
|
||||
for old in $(api GET "/releases/$ID/assets" | json "' '.join(str(a['id']) for a in d if a['name']=='$f')"); do
|
||||
api DELETE "/releases/$ID/assets/$old" >/dev/null
|
||||
|
||||
Binary file not shown.
@@ -1,203 +0,0 @@
|
||||
// Baut das MBMNET-Design für das HOME-Menü (Anemone3DS): Standard-Aussehen, eigene Töne.
|
||||
// node theme/build-theme.js <Ausgabeordner>
|
||||
// Ergebnis: <Ausgabe>/MBMNET/{body_LZ.bin, info.smdh, preview.png} (info.smdh baut bannertool, siehe release.sh)
|
||||
//
|
||||
// Aufbau der Design-Datei (body) und des Ton-Abschnitts wie im Usagi 3DS Theme Editor (MIT,
|
||||
// github.com/usagirei/3DS-Theme-Editor): Kopf 0xD0 Bytes, Ton-Abschnitt = 2, 100, dann 8 Töne
|
||||
// (Cursor, App starten, Ordner, Schließen, Bildschirm 0-2, Deckel auf) mit Größe und Lautstärke.
|
||||
// Die Töne sind CWAV (PCM16, 22 kHz, mono), Aufbau wie 3dbrew/BCWAV bzw. libcwav.
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const zlib = require('node:zlib');
|
||||
|
||||
const RATE = 22050;
|
||||
|
||||
// --- Töne erzeugen ---------------------------------------------------------------------------
|
||||
|
||||
// Ton aus Teiltönen: [{ f: Frequenz, t0: Start (s), d: Dauer (s), a: Lautstärke }], weiche Hüllkurve
|
||||
function synth(parts, total, { noise = 0 } = {}) {
|
||||
const n = Math.round(total * RATE);
|
||||
const out = new Float64Array(n);
|
||||
let seed = 12345;
|
||||
for (const p of parts) {
|
||||
const s0 = Math.round(p.t0 * RATE), len = Math.round(p.d * RATE);
|
||||
for (let i = 0; i < len && s0 + i < n; i++) {
|
||||
const t = i / RATE;
|
||||
const attack = Math.min(1, i / (0.004 * RATE)); // 4 ms Einschwingen (kein Knacken)
|
||||
const decay = Math.exp(-t / (p.d / 4)); // ausklingen
|
||||
const f = p.f1 ? p.f + (p.f1 - p.f) * (i / len) : p.f; // optional Gleiten
|
||||
out[s0 + i] += p.a * attack * decay * Math.sin(2 * Math.PI * f * t)
|
||||
+ (p.a * 0.25) * attack * decay * Math.sin(4 * Math.PI * f * t); // etwas Oberton: weicher Klang
|
||||
}
|
||||
}
|
||||
if (noise) { // gefiltertes Rauschen für "Wisch"-Töne
|
||||
let lp = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
||||
const white = seed / 0x3fffffff - 1;
|
||||
lp += 0.08 * (white - lp);
|
||||
const env = Math.sin(Math.PI * i / n) ** 2;
|
||||
out[i] += noise * lp * env;
|
||||
}
|
||||
}
|
||||
// Ende weich ausblenden (letzte 10 ms)
|
||||
const fade = Math.round(0.01 * RATE);
|
||||
for (let i = 0; i < fade; i++) out[n - 1 - i] *= i / fade;
|
||||
const pcm = Buffer.alloc(n * 2);
|
||||
for (let i = 0; i < n; i++) pcm.writeInt16LE(Math.max(-32767, Math.min(32767, Math.round(out[i] * 32767))), i * 2);
|
||||
return pcm;
|
||||
}
|
||||
|
||||
const SOUNDS = [
|
||||
// 0 Cursor: weicher, kurzer Klick (statt Piepen)
|
||||
synth([{ f: 1650, t0: 0, d: 0.035, a: 0.22 }], 0.045),
|
||||
// 1 App starten: aufsteigender Zweiklang (MBMNET-Rot-Feeling: hell, freundlich)
|
||||
synth([{ f: 659.3, t0: 0, d: 0.16, a: 0.30 }, { f: 987.8, t0: 0.08, d: 0.22, a: 0.30 }], 0.32),
|
||||
// 2 Ordner: kurzer "Pop"
|
||||
synth([{ f: 520, f1: 760, t0: 0, d: 0.07, a: 0.30 }], 0.08),
|
||||
// 3 Schließen / zurück: absteigender Zweiklang
|
||||
synth([{ f: 880, t0: 0, d: 0.10, a: 0.26 }, { f: 587.3, t0: 0.06, d: 0.14, a: 0.26 }], 0.22),
|
||||
// 4-6 Bildschirm blättern: sanfter Wisch
|
||||
synth([], 0.12, { noise: 0.35 }),
|
||||
synth([], 0.12, { noise: 0.35 }),
|
||||
synth([{ f: 440, f1: 660, t0: 0, d: 0.10, a: 0.10 }], 0.12, { noise: 0.30 }),
|
||||
// 7 Deckel auf: kleiner Gong (drei Töne)
|
||||
synth([{ f: 523.3, t0: 0, d: 0.30, a: 0.22 }, { f: 659.3, t0: 0.09, d: 0.30, a: 0.22 }, { f: 1046.5, t0: 0.18, d: 0.40, a: 0.22 }], 0.6),
|
||||
];
|
||||
|
||||
// --- CWAV (PCM16, mono) ----------------------------------------------------------------------
|
||||
const align = (n, a) => (n + a - 1) & ~(a - 1);
|
||||
|
||||
function cwav(pcm) {
|
||||
const samples = pcm.length / 2;
|
||||
const infoSize = 0x40; // 8 Kopf + 0x14 Werte + 4 Anzahl + 8 Verweis + 0x14 Kanal = 0x3C -> 0x40
|
||||
const dataOff = 0x40 + infoSize;
|
||||
const dataSize = align(0x20 + pcm.length, 0x20);
|
||||
const file = Buffer.alloc(dataOff + dataSize);
|
||||
// Kopf
|
||||
file.write('CWAV', 0, 'latin1');
|
||||
file.writeUInt16LE(0xFEFF, 4);
|
||||
file.writeUInt16LE(0x40, 6);
|
||||
file.writeUInt32LE(0x02010000, 8);
|
||||
file.writeUInt32LE(file.length, 0x0C);
|
||||
file.writeUInt16LE(2, 0x10);
|
||||
file.writeUInt16LE(0x7000, 0x14); file.writeUInt32LE(0x40, 0x18); file.writeUInt32LE(infoSize, 0x1C);
|
||||
file.writeUInt16LE(0x7001, 0x20); file.writeUInt32LE(dataOff, 0x24); file.writeUInt32LE(dataSize, 0x28);
|
||||
// INFO
|
||||
const I = 0x40;
|
||||
file.write('INFO', I, 'latin1');
|
||||
file.writeUInt32LE(infoSize, I + 4);
|
||||
file.writeUInt8(1, I + 8); // PCM16
|
||||
file.writeUInt8(0, I + 9); // keine Schleife
|
||||
file.writeUInt32LE(RATE, I + 0x0C);
|
||||
file.writeUInt32LE(0, I + 0x10); // Schleifenanfang
|
||||
file.writeUInt32LE(samples, I + 0x14); // Ende = Anzahl Samples
|
||||
file.writeUInt32LE(0, I + 0x18);
|
||||
const table = I + 0x1C; // Verweistabelle der Kanäle
|
||||
file.writeUInt32LE(1, table);
|
||||
file.writeUInt16LE(0x7100, table + 4); file.writeUInt32LE(0x0C, table + 8); // Kanal-Info relativ zur Tabelle
|
||||
const ch = table + 0x0C;
|
||||
file.writeUInt16LE(0x1F00, ch); file.writeUInt32LE(0x18, ch + 4); // Samples relativ zu DATA+8
|
||||
file.writeUInt16LE(0, ch + 8); file.writeUInt32LE(0xFFFFFFFF, ch + 0x0C); // keine ADPCM-Infos
|
||||
file.writeUInt32LE(0, ch + 0x10);
|
||||
// DATA
|
||||
file.write('DATA', dataOff, 'latin1');
|
||||
file.writeUInt32LE(dataSize, dataOff + 4);
|
||||
pcm.copy(file, dataOff + 0x20);
|
||||
return file;
|
||||
}
|
||||
|
||||
// --- Ton-Abschnitt (wie Usagi CwavBlock.Generate) ------------------------------------------
|
||||
function sfxBlock(files) {
|
||||
const parts = [];
|
||||
const u32 = (v) => { const b = Buffer.alloc(4); b.writeUInt32LE(v >>> 0); parts.push(b); };
|
||||
u32(0x02); u32(0x64);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
if (i === 4) for (const v of [0x00, 0x64, 0x00, 0x64, 0x64, 0x00, 0x64, 0x64, 0x00, 0x00, 0x00]) u32(v);
|
||||
const f = files[i];
|
||||
u32(f.length);
|
||||
u32(f.length ? 0x50 : 1); // Lautstärke 80
|
||||
parts.push(f);
|
||||
}
|
||||
return Buffer.concat(parts);
|
||||
}
|
||||
|
||||
// --- Design-Datei (body) ---------------------------------------------------------------------
|
||||
function body(sfx) {
|
||||
const header = Buffer.alloc(0xD0);
|
||||
header.writeUInt32LE(1, 0); // Version
|
||||
// 0x5 Hintergrundmusik aus, alle Zeichen-Arten 0 (Standard-Aussehen), alle Farb-Flags 0
|
||||
header.writeUInt32LE(1, 0xB8); // Töne an
|
||||
header.writeUInt32LE(sfx.length, 0xBC);
|
||||
header.writeUInt32LE(0xD0, 0xC0);
|
||||
if (sfx.length > 0x2DC00) throw new Error(`Töne zu groß: ${sfx.length} Bytes`);
|
||||
const raw = Buffer.concat([header, sfx]);
|
||||
return Buffer.concat([raw, Buffer.alloc(align(raw.length, 0x10) - raw.length)]);
|
||||
}
|
||||
|
||||
// --- LZ11 (nur Literale: immer gültig, die Datei ist ohnehin klein) ---------------------------
|
||||
function lz11(data) {
|
||||
const out = [Buffer.from([0x11, data.length & 0xFF, (data.length >> 8) & 0xFF, (data.length >> 16) & 0xFF])];
|
||||
for (let i = 0; i < data.length; i += 8) {
|
||||
out.push(Buffer.from([0x00]));
|
||||
out.push(data.subarray(i, i + 8));
|
||||
}
|
||||
return Buffer.concat(out);
|
||||
}
|
||||
|
||||
function unlz11(buf) { // zur Prüfung
|
||||
const size = buf[1] | (buf[2] << 8) | (buf[3] << 16);
|
||||
const out = Buffer.alloc(size);
|
||||
let i = 4, o = 0;
|
||||
while (o < size) {
|
||||
const flags = buf[i++];
|
||||
for (let b = 7; b >= 0 && o < size; b--) {
|
||||
if (!(flags & (1 << b))) { out[o++] = buf[i++]; continue; }
|
||||
throw new Error('komprimierte Blöcke nicht erwartet');
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// --- Vorschau (412x480: oben 400x240 + unten 320x240), schlicht in MBMNET-Farben ---------------
|
||||
function preview() {
|
||||
const W = 412, H = 480;
|
||||
const rows = [];
|
||||
for (let y = 0; y < H; y++) {
|
||||
const row = Buffer.alloc(1 + W * 3);
|
||||
for (let x = 0; x < W; x++) {
|
||||
const top = y < 240;
|
||||
const inScreen = top ? x < 400 : x >= 46 && x < 366;
|
||||
let c = inScreen ? (top ? [0x22, 0x26, 0x2E] : [0x16, 0x19, 0x1F]) : [0, 0, 0];
|
||||
// Logo-Quadrat in der Mitte oben
|
||||
if (top && Math.abs(x - 200) < 40 && Math.abs(y - 110) < 40) c = [0xE4, 0x00, 0x2B];
|
||||
row.set(c, 1 + x * 3);
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
const chunk = (t, d) => {
|
||||
const len = Buffer.alloc(4); len.writeUInt32BE(d.length);
|
||||
const crc = Buffer.alloc(4); crc.writeUInt32BE(zlib.crc32 ? zlib.crc32(Buffer.concat([Buffer.from(t), d])) >>> 0 : crc32(Buffer.concat([Buffer.from(t), d])));
|
||||
return Buffer.concat([len, Buffer.from(t), d, crc]);
|
||||
};
|
||||
const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(W, 0); ihdr.writeUInt32BE(H, 4); ihdr[8] = 8; ihdr[9] = 2;
|
||||
return Buffer.concat([Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]), chunk('IHDR', ihdr),
|
||||
chunk('IDAT', zlib.deflateSync(Buffer.concat(rows))), chunk('IEND', Buffer.alloc(0))]);
|
||||
}
|
||||
function crc32(buf) {
|
||||
let c, crc = 0xFFFFFFFF;
|
||||
for (const b of buf) { c = (crc ^ b) & 0xFF; for (let k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1; crc = (crc >>> 8) ^ c; }
|
||||
return (crc ^ 0xFFFFFFFF) >>> 0;
|
||||
}
|
||||
|
||||
const outDir = path.join(process.argv[2] || '.', 'MBMNET');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const files = SOUNDS.map(cwav);
|
||||
const sfx = sfxBlock(files);
|
||||
const b = body(sfx);
|
||||
const packed = lz11(b);
|
||||
if (!unlz11(packed).equals(b)) throw new Error('LZ11-Prüfung fehlgeschlagen');
|
||||
fs.writeFileSync(path.join(outDir, 'body_LZ.bin'), packed);
|
||||
fs.writeFileSync(path.join(outDir, 'preview.png'), preview());
|
||||
for (let i = 0; i < files.length; i++) fs.writeFileSync(path.join(outDir, `..`, `ton-${i}.bcwav`), files[i]);
|
||||
console.log(`MBMNET-Design: body ${b.length} Bytes (Töne ${sfx.length}), gepackt ${packed.length} Bytes -> ${outDir}`);
|
||||
Reference in New Issue
Block a user