From 52af8eec2cbc2296bfc90226e23ac20eaed76a68 Mon Sep 17 00:00:00 2001 From: kion Date: Tue, 1 Oct 2024 07:21:08 +0900 Subject: [PATCH] add st20t --- src/ST20T.ts | 244 +++++++++++++++++++++++ test/{yosyonke.test.ts => yosyonke.skip} | 107 +++++----- 2 files changed, 299 insertions(+), 52 deletions(-) create mode 100644 src/ST20T.ts rename test/{yosyonke.test.ts => yosyonke.skip} (75%) diff --git a/src/ST20T.ts b/src/ST20T.ts new file mode 100644 index 0000000..615dec7 --- /dev/null +++ b/src/ST20T.ts @@ -0,0 +1,244 @@ +/** + + 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, + encodeTexel, +} from "./EncodeTexture"; +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 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 findClosestIndex = (arr: number[], target: number): number => { + let closestIndex = 0; + let closestDifference = Math.abs(arr[0] - target); + + for (let i = 1; i < arr.length; i++) { + const currentDifference = Math.abs(arr[i] - target); + + if (currentDifference < closestDifference) { + closestDifference = currentDifference; + closestIndex = i; + } + } + + return closestIndex; +}; + +const updatePoster = (bin: Buffer, pngPath: string) => { + const pngData = readFileSync(pngPath); + + const imgOfs = 0x3c000; + const pal: number[] = []; + + // console.log(findClosestIndex(pal, darkGrey)); + // process.exit(); + + const encodedLogo = encodeCutSceneTexture(pal, pngData); + + const red = encodeTexel(255, 0, 0, 255); + + const mpTexture = decompress(Buffer.from(bin.subarray(imgOfs))); + + const includedPal = Buffer.from(mpTexture.subarray(0, 0x20)); + const encodedTexture = Buffer.from(mpTexture.subarray(0x20)); + + // Update Palette + const palOfs = 0x32800; + + for (let i = 0; i < 16; i++) { + // bin.writeUInt16LE(pal[i], palOfs + 0x30 + i * 2); + includedPal.writeUInt16LE(red, i * 2); + } + + const ROW_LEN = 0x80; + const X_START = 64; + const Y_START = 128; + let texOfs = ROW_LEN * Y_START; // + PAL_OFS; + let logoOfs = 0; + const HEIGHT = 48; + const WIDTH = 64; + // for (let y = 0; y < HEIGHT; y++) { + // texOfs += X_START / 2; + // for (let x = 0; x < WIDTH / 2; x++) { + // encodedTexture[texOfs++] = encodedLogo[logoOfs++]; + // } + // texOfs += (256 - X_START - WIDTH) / 2; + // } + + // console.log("Logo Pos: 0x%s", logoOfs.toString(16)); + + const imageData: number[] = new Array(); + for (let ofs = 0; ofs < encodedTexture.length; ofs++) { + const byte = encodedTexture.readUInt8(ofs); + + imageData.push(byte & 0xf); + imageData.push(byte >> 4); + } + + const [bodyBitField, compressedBody] = compressNewTexture( + includedPal, + encodedTexture, + 0, + ); + const len = bodyBitField.length + compressedBody.length; + + for (let i = 0x3c030; i < 0x3fbb0; i++) { + bin[i] = 0; + } + + let ofs = 0x3c030; + 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 <= 0x3f800) { + console.log("too short!!!"); + throw new Error("reaverbot painting too short"); + } else if (len > 0x40000) { + console.log("too long"); + throw new Error("reaverbot painting too long"); + } else { + console.log("yaya!!!"); + } + + console.log("End: 0x%s", ofs.toString(16)); + bin.writeInt16LE(bodyBitField.length, 0x3c024); +}; + +const updateCarlbania = (poster: string) => { + const bin = readFileSync("bin/carlbania-ST20T.BIN"); + updatePoster(bin, poster); + writeFileSync("out/carlbania-ST20T.BIN", bin); +}; + +export default updateCarlbania; +export { updateCarlbania }; diff --git a/test/yosyonke.test.ts b/test/yosyonke.skip similarity index 75% rename from test/yosyonke.test.ts rename to test/yosyonke.skip index 81db55f..daa3241 100644 --- a/test/yosyonke.test.ts +++ b/test/yosyonke.skip @@ -135,8 +135,10 @@ const renderImage = ( ofs = 0; const { colorCount, paletteCount } = tim; + const polly: number[] = []; for (let i = 0; i < paletteCount; i++) { for (let k = 0; k < colorCount; k++) { + polly.push(target.readUInt16LE(ofs)); ofs += 2; } } @@ -161,7 +163,7 @@ const renderImage = ( 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!]; + const { r, g, b, a } = wordToColor(polly[colorIndex!]); png.data[dst++] = r; png.data[dst++] = g; png.data[dst++] = b; @@ -174,63 +176,63 @@ const renderImage = ( writeFileSync(`out/${base}_${pos.toString(16)}.png`, buffer); }; -test("it should search for textures in the yosyonke", () => { - const src = readFileSync("bin/flutter-ST05T.BIN"); - const pals: Pixel[][] = [ - [ - { r: 0, g: 0, b: 0, a: 0 }, - { r: 0, g: 0, b: 0, a: 255 }, - { r: 16, g: 16, b: 16, a: 255 }, - { r: 32, g: 32, b: 32, a: 255 }, - { r: 48, g: 48, b: 48, a: 255 }, - { r: 64, g: 64, b: 64, a: 255 }, - { r: 72, g: 72, b: 72, a: 255 }, - { r: 90, g: 90, b: 90, a: 255 }, - { r: 110, g: 110, b: 110, a: 255 }, - { r: 120, g: 120, b: 120, a: 255 }, - { r: 130, g: 130, b: 130, a: 255 }, - { r: 140, g: 140, b: 140, a: 255 }, - { r: 150, g: 150, b: 150, a: 255 }, - { r: 160, g: 160, b: 160, a: 255 }, - { r: 190, g: 190, b: 190, a: 255 }, - { r: 210, g: 210, b: 210, a: 255 }, - { r: 220, g: 220, b: 220, a: 255 }, - { r: 255, g: 255, b: 255, a: 255 }, - ], - ]; +// test("it should search for textures in the yosyonke", () => { +// const src = readFileSync("bin/carlbania-ST20T.BIN"); +// const pals: Pixel[][] = [ +// [ +// { r: 0, g: 0, b: 0, a: 0 }, +// { r: 0, g: 0, b: 0, a: 255 }, +// { r: 16, g: 16, b: 16, a: 255 }, +// { r: 32, g: 32, b: 32, a: 255 }, +// { r: 48, g: 48, b: 48, a: 255 }, +// { r: 64, g: 64, b: 64, a: 255 }, +// { r: 72, g: 72, b: 72, a: 255 }, +// { r: 90, g: 90, b: 90, a: 255 }, +// { r: 110, g: 110, b: 110, a: 255 }, +// { r: 120, g: 120, b: 120, a: 255 }, +// { r: 130, g: 130, b: 130, a: 255 }, +// { r: 140, g: 140, b: 140, a: 255 }, +// { r: 150, g: 150, b: 150, a: 255 }, +// { r: 160, g: 160, b: 160, a: 255 }, +// { r: 190, g: 190, b: 190, a: 255 }, +// { r: 210, g: 210, b: 210, a: 255 }, +// { r: 220, g: 220, b: 220, a: 255 }, +// { r: 255, g: 255, b: 255, a: 255 }, +// ], +// ]; - 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), - }; +// 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 && tim.type !== 3) { - continue; - } +// if (tim.type !== 2 && tim.type !== 3) { +// continue; +// } - if (tim.width == 0 || tim.height == 0) { - continue; - } +// if (tim.width == 0 || tim.height == 0) { +// continue; +// } - const img = src.subarray(i); - renderImage(img, "nino", i, pals[0]); - } -}); +// const img = src.subarray(i); +// renderImage(img, "nino", i, pals[0]); +// } +// }); test("it should search for room203 palette", () => { - const src = readFileSync("bin/flutter-ST05T.BIN"); - const img = src.subarray(0x37800); + const src = readFileSync("bin/carlbania-ST20T.BIN"); + const img = src.subarray(0x3c000); for (let i = 0; i < src.length; i += 0x800) { const tim = { @@ -263,6 +265,7 @@ test("it should search for room203 palette", () => { } renderImage(img, "poster", i, pal); + break; } });