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); + }); +});