diff --git a/bin/DEMO.BIN b/bin/DEMO.BIN
new file mode 100644
index 0000000..bf797c6
Binary files /dev/null and b/bin/DEMO.BIN differ
diff --git a/bin/GAME.BIN b/bin/GAME.BIN
new file mode 100644
index 0000000..a4908a7
Binary files /dev/null and b/bin/GAME.BIN differ
diff --git a/bin/LOGO.BIN b/bin/LOGO.BIN
new file mode 100644
index 0000000..4625fe5
Binary files /dev/null and b/bin/LOGO.BIN differ
diff --git a/bin/SUBSCN.BIN b/bin/SUBSCN.BIN
new file mode 100644
index 0000000..2f35f61
Binary files /dev/null and b/bin/SUBSCN.BIN differ
diff --git a/index.ts b/index.ts
index 07f72b2..021d98c 100644
--- a/index.ts
+++ b/index.ts
@@ -22,6 +22,7 @@ import {
replaceHunterSeeker,
replaceDrillArm,
} from "./src/EncodeWeapon";
+import { updateDemoLogo } from "./src/GAME";
encodeTitle("miku/title.png");
// process.exit();
@@ -212,6 +213,7 @@ replaceDrillArm("miku/weapons/PL00R10_001.obj");
encodeApronMegaman();
updateST03T("miku/apron/body-01.png", "miku/faces/ST03T.png");
updateSceneModel();
+updateDemoLogo("miku/title-smol.png");
/**
Encode Rom
diff --git a/miku/title-smol.png b/miku/title-smol.png
new file mode 100644
index 0000000..d50b578
Binary files /dev/null and b/miku/title-smol.png differ
diff --git a/out/.gitkeep b/out/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/EncodeRom.ts b/src/EncodeRom.ts
index 60c9e62..8275437 100644
--- a/src/EncodeRom.ts
+++ b/src/EncodeRom.ts
@@ -572,6 +572,7 @@ const encodeRom = () => {
"cut-ST1802T.BIN",
"cut-ST1803.BIN",
"cut-ST2501.BIN",
+ "GAME.BIN",
];
console.log("--- Replacing Cut Scene Textures ---");
diff --git a/src/EncodeTitle.ts b/src/EncodeTitle.ts
index d953294..b7eb324 100644
--- a/src/EncodeTitle.ts
+++ b/src/EncodeTitle.ts
@@ -30,6 +30,116 @@ type Command = {
word: number;
};
+const updateSmallLogo = (src: Buffer) => {
+ const tim = {
+ type: src.readUInt32LE(0x00),
+ fullSize: src.readUInt32LE(0x04),
+ paletteX: src.readUInt16LE(0x0c),
+ paletteY: src.readUInt16LE(0x0e),
+ colorCount: src.readUInt16LE(0x10),
+ paletteCount: src.readUInt16LE(0x12),
+ imageX: src.readUInt16LE(0x14),
+ imageY: src.readUInt16LE(0x16),
+ width: src.readUInt16LE(0x18),
+ height: src.readUInt16LE(0x1a),
+ bitfieldSize: src.readUInt16LE(0x24),
+ payloadSize: src.readUInt16LE(0x26),
+ };
+
+ tim.width *= 4;
+
+ const { fullSize, bitfieldSize } = tim;
+ const bitfield: number[] = new Array();
+ const target = Buffer.alloc(fullSize);
+
+ // Read Bitfield
+ const bitfieldBuffer = src.subarray(0x30, 0x30 + bitfieldSize);
+ let ofs = 0x30;
+ for (let i = 0; i < bitfieldSize; i += 4) {
+ const dword = src.readUInt32LE(ofs + i);
+ for (let k = 31; k > -1; k--) {
+ bitfield.push(dword & (1 << k) ? 1 : 0);
+ }
+ }
+
+ ofs += bitfieldSize;
+ const payloadStart = 0;
+
+ // Decompress
+
+ let outOfs = 0;
+ let windowOfs = 0;
+ let cmdCount = 0;
+ let bytes = 0;
+
+ for (let i = 0; i < bitfield.length; i++) {
+ const bit = bitfield[i];
+ if (outOfs === fullSize) {
+ const payload = src.subarray(0x30 + bitfieldSize, ofs);
+ break;
+ }
+
+ const word = src.readUInt16LE(ofs);
+ ofs += 2;
+
+ switch (bit) {
+ case 0:
+ target.writeUInt16LE(word, outOfs);
+ outOfs += 2;
+ break;
+ case 1:
+ if (word === 0xffff) {
+ windowOfs += 0x2000;
+ cmdCount = 0;
+ bytes = 0;
+ } else {
+ cmdCount++;
+ const copyFrom = windowOfs + ((word >> 3) & 0x1fff);
+ const copyLen = ((word & 0x07) + 2) * 2;
+ bytes += copyLen;
+ for (let i = 0; i < copyLen; i++) {
+ target[outOfs++] = target[copyFrom + i];
+ }
+ }
+ break;
+ }
+ }
+
+ // Read the image data
+ const imageData: number[] = new Array();
+ for (ofs = 0; ofs < target.length; ofs++) {
+ const byte = target.readUInt8(ofs);
+ imageData.push(byte & 0xf);
+ imageData.push(byte >> 4);
+ }
+
+ // Update title to debug palette
+ const { width, height } = tim;
+ let index = 0;
+ for (let y = 0; y < height; y++) {
+ for (var x = 0; x < width; x++) {
+ if (x >= 48 && x < 48 + 96) {
+ imageData[index] = Math.floor((x - 48) / 12);
+ if (y >= 64 + 20 && y < 64 + 40) {
+ imageData[index] += 8;
+ }
+ }
+ }
+ }
+
+ // Re-encode the image data into a buffer
+ ofs = 0;
+ for (let i = 0; i < imageData.length; i += 2) {
+ const a = imageData[i] & 0x0f;
+ const b = imageData[i + 1] & 0x0f;
+ const byte = a | (b << 4);
+ target.writeUInt8(byte, ofs);
+ ofs++;
+ }
+
+ return target;
+};
+
const encodeBitfield = (bits: boolean[]): Buffer => {
const length = Math.ceil(bits.length / 32) * 4;
let ofs = 0;
diff --git a/src/GAME.ts b/src/GAME.ts
new file mode 100644
index 0000000..c7559fe
--- /dev/null
+++ b/src/GAME.ts
@@ -0,0 +1,183 @@
+/**
+
+ Miku-Legends-2
+ Copyright (C) 2024, DashGL Project
+ By Kion (kion@dashgl.com)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+**/
+
+import { readFileSync, writeFileSync } from "fs";
+import { encodeCutSceneTexture, compressNewTexture } from "./EncodeTexture";
+
+const decompress = (src: Buffer) => {
+ const tim = {
+ type: src.readUInt32LE(0x00),
+ fullSize: src.readUInt32LE(0x04),
+ paletteX: src.readUInt16LE(0x0c),
+ paletteY: src.readUInt16LE(0x0e),
+ colorCount: src.readUInt16LE(0x10),
+ paletteCount: src.readUInt16LE(0x12),
+ imageX: src.readUInt16LE(0x14),
+ imageY: src.readUInt16LE(0x16),
+ width: src.readUInt16LE(0x18),
+ height: src.readUInt16LE(0x1a),
+ bitfieldSize: src.readUInt16LE(0x24),
+ payloadSize: src.readUInt16LE(0x26),
+ };
+
+ switch (tim.colorCount) {
+ case 16:
+ tim.width *= 4;
+ break;
+ case 256:
+ tim.width *= 2;
+ break;
+ default:
+ tim.paletteCount *= tim.colorCount / 16;
+ tim.colorCount = 16;
+ tim.width *= 4;
+ break;
+ }
+
+ const { fullSize, bitfieldSize } = tim;
+ const bitfield: number[] = new Array();
+ const target = Buffer.alloc(fullSize);
+
+ // Read Bitfield
+
+ const bitfieldBuffer = src.subarray(0x30, 0x30 + bitfieldSize);
+ let ofs = 0x30;
+ for (let i = 0; i < bitfieldSize; i += 4) {
+ const dword = src.readUInt32LE(ofs + i);
+ for (let k = 31; k > -1; k--) {
+ bitfield.push(dword & (1 << k) ? 1 : 0);
+ }
+ }
+
+ ofs += bitfieldSize;
+ const payloadStart = 0;
+
+ // Decompress
+
+ let outOfs = 0;
+ let windowOfs = 0;
+ let cmdCount = 0;
+ let bytes = 0;
+
+ for (let i = 0; i < bitfield.length; i++) {
+ const bit = bitfield[i];
+ if (outOfs === fullSize) {
+ const payload = src.subarray(0x30 + bitfieldSize, ofs);
+ break;
+ }
+
+ const word = src.readUInt16LE(ofs);
+ ofs += 2;
+
+ switch (bit) {
+ case 0:
+ target.writeUInt16LE(word, outOfs);
+ outOfs += 2;
+ break;
+ case 1:
+ if (word === 0xffff) {
+ windowOfs += 0x2000;
+ cmdCount = 0;
+ bytes = 0;
+ } else {
+ cmdCount++;
+ const copyFrom = windowOfs + ((word >> 3) & 0x1fff);
+ const copyLen = ((word & 0x07) + 2) * 2;
+ bytes += copyLen;
+ for (let i = 0; i < copyLen; i++) {
+ target[outOfs++] = target[copyFrom + i];
+ }
+ }
+ break;
+ }
+ }
+
+ return target;
+};
+
+const updateDemoLogo = (pngPath: string) => {
+ const bin = readFileSync("bin/GAME.BIN");
+ const pngData = readFileSync(pngPath);
+
+ const imgOfs = 0x041800;
+ const pal: number[] = [];
+
+ const encodedLogo = encodeCutSceneTexture(pal, pngData);
+ const encodedTexture = decompress(Buffer.from(bin.subarray(imgOfs)));
+
+ // Update Palette
+ const palOfs = 0x44800;
+ for (let i = 0; i < pal.length; i++) {
+ bin.writeUInt16LE(pal[i], palOfs + 0x30 + i * 2);
+ }
+
+ console.log("Encoded Logo: 0x%s", encodedLogo.length.toString(16));
+ console.log("Encoded Texture: 0x%s", encodedTexture.length.toString(16));
+
+ let texOfs = 0x2000;
+ let logoOfs = 0;
+ for (let y = 0; y < 40; y++) {
+ texOfs += 24;
+ for (let x = 0; x < 48; x++) {
+ encodedTexture[texOfs++] = encodedLogo[logoOfs++];
+ }
+ texOfs += 56;
+ }
+
+ console.log("Logo Pos: 0x%s", logoOfs.toString(16));
+
+ const [bodyBitField, compressedBody] = compressNewTexture(
+ Buffer.alloc(0),
+ encodedTexture,
+ 0,
+ );
+ const len = bodyBitField.length + compressedBody.length;
+ console.log("Segment 2: 0x%s", len.toString(16));
+
+ for (let i = 0x41830; i < 0x432f2; i++) {
+ bin[i] = 0;
+ }
+
+ let ofs = 0x41830;
+ for (let i = 0; i < bodyBitField.length; i++) {
+ bin[ofs++] = bodyBitField[i];
+ }
+
+ for (let i = 0; i < compressedBody.length; i++) {
+ bin[ofs++] = compressedBody[i];
+ }
+
+ if (ofs <= 0x43000) {
+ console.log("too short!!!");
+ } else if (len > 0x43800) {
+ console.log("too long");
+ } else {
+ console.log("yaya!!!");
+ }
+
+ console.log("End: 0x%s", ofs.toString(16));
+ bin.writeInt16LE(bodyBitField.length, 0x41824);
+
+ writeFileSync("out/GAME.BIN", bin);
+};
+
+export default updateDemoLogo;
+export { updateDemoLogo };
diff --git a/test/logo.test.ts b/test/logo.test.ts
new file mode 100644
index 0000000..61bff42
--- /dev/null
+++ b/test/logo.test.ts
@@ -0,0 +1,203 @@
+/**
+
+ Miku-Legends-2
+ Copyright (C) 2024, DashGL Project
+ By Kion (kion@dashgl.com)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+**/
+
+import { test, expect } from "bun:test";
+import { readFileSync, writeFileSync } from "fs";
+import { PNG } from "pngjs";
+
+type Pixel = {
+ r: number;
+ g: number;
+ b: number;
+ a: number;
+};
+
+const wordToColor = (word: number): Pixel => {
+ const r = ((word >> 0x00) & 0x1f) << 3;
+ const g = ((word >> 0x05) & 0x1f) << 3;
+ const b = ((word >> 0x0a) & 0x1f) << 3;
+ const a = word > 0 ? 255 : 0;
+ return { r, g, b, a };
+};
+
+const renderImage = (src: Buffer, pos: number, palette: Pixel[]) => {
+ const tim = {
+ type: src.readUInt32LE(0x00),
+ fullSize: src.readUInt32LE(0x04),
+ paletteX: src.readUInt16LE(0x0c),
+ paletteY: src.readUInt16LE(0x0e),
+ colorCount: src.readUInt16LE(0x10),
+ paletteCount: src.readUInt16LE(0x12),
+ imageX: src.readUInt16LE(0x14),
+ imageY: src.readUInt16LE(0x16),
+ width: src.readUInt16LE(0x18),
+ height: src.readUInt16LE(0x1a),
+ bitfieldSize: src.readUInt16LE(0x24),
+ payloadSize: src.readUInt16LE(0x26),
+ };
+
+ switch (tim.colorCount) {
+ case 16:
+ tim.width *= 4;
+ break;
+ case 256:
+ tim.width *= 2;
+ break;
+ default:
+ tim.paletteCount *= tim.colorCount / 16;
+ tim.colorCount = 16;
+ tim.width *= 4;
+ break;
+ }
+
+ const { fullSize, bitfieldSize } = tim;
+ const bitfield: number[] = new Array();
+ const target = Buffer.alloc(fullSize);
+
+ // Read Bitfield
+
+ const bitfieldBuffer = src.subarray(0x30, 0x30 + bitfieldSize);
+ let ofs = 0x30;
+ for (let i = 0; i < bitfieldSize; i += 4) {
+ const dword = src.readUInt32LE(ofs + i);
+ for (let k = 31; k > -1; k--) {
+ bitfield.push(dword & (1 << k) ? 1 : 0);
+ }
+ }
+
+ ofs += bitfieldSize;
+ const payloadStart = 0;
+
+ // Decompress
+
+ let outOfs = 0;
+ let windowOfs = 0;
+ let cmdCount = 0;
+ let bytes = 0;
+
+ for (let i = 0; i < bitfield.length; i++) {
+ const bit = bitfield[i];
+ if (outOfs === fullSize) {
+ const payload = src.subarray(0x30 + bitfieldSize, ofs);
+ break;
+ }
+
+ const word = src.readUInt16LE(ofs);
+ ofs += 2;
+
+ switch (bit) {
+ case 0:
+ target.writeUInt16LE(word, outOfs);
+ outOfs += 2;
+ break;
+ case 1:
+ if (word === 0xffff) {
+ windowOfs += 0x2000;
+ cmdCount = 0;
+ bytes = 0;
+ } else {
+ cmdCount++;
+ const copyFrom = windowOfs + ((word >> 3) & 0x1fff);
+ const copyLen = ((word & 0x07) + 2) * 2;
+ bytes += copyLen;
+ for (let i = 0; i < copyLen; i++) {
+ target[outOfs++] = target[copyFrom + i];
+ }
+ }
+ break;
+ }
+ }
+
+ ofs = 0;
+ const { colorCount, paletteCount } = tim;
+
+ // Read the image data
+ const imageData: number[] = new Array();
+ for (ofs; ofs < target.length; ofs++) {
+ const byte = target.readUInt8(ofs);
+ if (colorCount === 256) {
+ imageData.push(byte);
+ } else {
+ imageData.push(byte & 0xf);
+ imageData.push(byte >> 4);
+ }
+ }
+
+ const { width, height } = tim;
+ const png = new PNG({ width, height });
+
+ let index = 0;
+ let dst = 0;
+ for (let y = 0; y < height; y++) {
+ for (var x = 0; x < width; x++) {
+ const colorIndex = imageData[index++];
+ const { r, g, b, a } = palette[colorIndex!];
+ png.data[dst++] = r;
+ png.data[dst++] = g;
+ png.data[dst++] = b;
+ png.data[dst++] = a;
+ }
+ }
+
+ // Export file
+ const buffer = PNG.sync.write(png);
+ writeFileSync(`out/miku_${pos.toString(16)}.png`, buffer);
+};
+
+test("it should search for textures in the file", () => {
+ const src = readFileSync("bin/GAME.BIN");
+ const pals: Pixel[][] = [];
+
+ for (let i = 0; i < src.length; i += 0x800) {
+ const tim = {
+ type: src.readUInt32LE(i + 0x00),
+ fullSize: src.readUInt32LE(i + 0x04),
+ paletteX: src.readUInt16LE(i + 0x0c),
+ paletteY: src.readUInt16LE(i + 0x0e),
+ colorCount: src.readUInt16LE(i + 0x10),
+ paletteCount: src.readUInt16LE(i + 0x12),
+ imageX: src.readUInt16LE(i + 0x14),
+ imageY: src.readUInt16LE(i + 0x16),
+ width: src.readUInt16LE(i + 0x18),
+ height: src.readUInt16LE(i + 0x1a),
+ bitfieldSize: src.readUInt16LE(i + 0x24),
+ payloadSize: src.readUInt16LE(i + 0x26),
+ };
+
+ if (tim.type !== 2) {
+ continue;
+ }
+
+ const palette: Pixel[] = [];
+ for (let k = 0; k < 16; k++) {
+ const word = src.readUInt16LE(i + 0x30 + k * 2);
+ palette.push(wordToColor(word));
+ }
+ console.log(pals.length.toString(16));
+ console.log("Offset: 0x%s", i.toString(16));
+ pals.push(palette);
+ }
+
+ const img = src.subarray(0x041800);
+ pals.forEach((palette, index) => {
+ renderImage(img, index, palette);
+ });
+});