gbnp/script/gbnp.js

600 lines
16 KiB
JavaScript

const CARTRIDGE_TYPES = [
'None',
'MBC1', 'MBC1', 'MBC1', null,
'MBC2', 'MBC2', null,
'None', 'None', null, null, null, null, null,
'MBC3', 'MBC3', 'MBC3', 'MBC3', 'MBC3', null, null, null, null, null,
'MBC5', 'MBC5', 'MBC5', 'MBC5', 'MBC5', 'MBC5'
];
const MAP_TRAILER_BYTES = [0x02, 0x00, 0x30, 0x12, 0x99, 0x11, 0x12, 0x20, 0x37, 0x57, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00];
const BITMAP_PREVIEW_BYTES = [
[0xFF, 0xFF, 0xFF, 0xFF], // white
[0xBB, 0xBB, 0xBB, 0xFF], // light grey
[0x66, 0x66, 0x66, 0xFF], // dark grey
[0x00, 0x00, 0x00, 0xFF] // black
]
const IG_POWER_CART_HACK = [0x3E, 0x01, 0xEA, 0x00, 0x20, 0xF0, 0xFD, 0x01, 0x00, 0x20, 0x4F, 0x0A, 0xEA, 0x00, 0x60];
const FONTS = [
{ style: 'normal 8px Gameboy', y: 7 },
{ style: 'normal 8px PokemonGB', y: 7 },
{ style: 'normal 8px Nokia', y: 7 },
{ style: 'normal 16px Gamer', y: 7 }
]
class Menu {
constructor() {
this.data = null;
const menuData = localStorage.getItem('menuData');
if (menuData) {
this.data = JSON.parse(menuData).data;
console.log("Menu data loaded from storage")
} else {
this.loadMenuDataFromScript();
}
}
present() {
return !!this.data
}
setData(arrayBuffer) {
this.data = new Uint8Array(arrayBuffer);
localStorage.setItem('menuData', JSON.stringify({ data: Array.from(this.data) }));
console.log("Menu data loaded from script")
}
loadMenuDataFromScript() {
const head = document.getElementsByTagName('head')[0];
const devScript = document.createElement('script');
devScript.src = 'script/menu.js';
head.appendChild(devScript);
}
}
class ROM {
constructor(arrayBuffer, fontIndex) {
let file = new FileSeeker(arrayBuffer);
file.seek(0x134);
this.title = String.fromCharCode(...file.read(0xF)).replace(/\0/g, '');
this.menuText = this.title;
file.seek(0x143);
let cgbByte = file.readByte();
this.cgb = cgbByte == 0x80 || cgbByte == 0xC0;
file.seek(0x147);
this.typeByte = file.readByte();
this.type = CARTRIDGE_TYPES[this.typeByte];
this.romByte = file.readByte();
this.ramByte = file.readByte();
this.updateBitmap(fontIndex);
file.rewind();
this.arrayBuffer = new ArrayBuffer(this.paddedRomSizeKB() * 1024);
let paddedFile = new FileSeeker(this.arrayBuffer);
if (file.size() > this.arrayBuffer.byteLength) {
alert('ROM header size is smaller than the file size! Did you load a menu by mistake?')
this.bad = true;
return;
} else {
paddedFile.writeBytes(file.read(file.size()));
}
if (!this.valid()) { alert('File is not a valid Game Boy ROM!'); this.bad = true; }
else if (!this.type) { alert('Cartridge type could not be determined!'); this.bad = true; }
else if (this.ramSizeKB() > 32) { alert('Game requires more than 32 KB of RAM!'); this.bad = true; }
}
valid() {
if (!this._valid) {
let check = Array.from(new Uint8Array(this.arrayBuffer.slice(260, 265)));
this._valid = (JSON.stringify(check) === JSON.stringify([0xCE,0xED,0x66, 0x66, 0xCC]));
}
return this._valid;
}
romSizeKB() {
return 32 << this.romByte;
}
paddedRomSizeKB() {
return this.padded() ? 128 : this.romSizeKB();
}
padded() {
return this.romSizeKB() < 128;
}
ramSizeKB() {
return Math.trunc(Math.pow(4, this.ramByte - 1)) * 2;
}
updateMenuText(text, fontIndex) {
this.menuText = text;
this.updateBitmap(fontIndex);
}
updateBitmap(fontIndex) {
let buffer = [];
const canvas = document.createElement("canvas");
canvas.height = 8;
canvas.width = 128;
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, 128, 8);
const font = FONTS[fontIndex || 0];
ctx.font = font.style;
ctx.fillStyle = 'black';
ctx.fillText(this.menuText,1,font.y);
ctx.fillStyle = 'white';
ctx.fillRect(127, 0, 127, 8);
const imageData = ctx.getImageData(0, 0, 128, 8).data;
for (let i = 0; i < imageData.length; i+=16){
let byte = 0;
for (let j = 0; j < 4; j++) {
let red = imageData[i+j*4];
if (red < 127) {
byte = byte | 0b11 << (6 - j*2);
}
}
buffer.push(byte)
}
let outputBuffer = []
for (let h = 0; h < 32; h+=2) {
for (let i = h; i < 256; i+=32) {
let a = buffer[i]
let b = buffer[i+1]
let outputA =
(a & 0b10000000) | ((a & 0b00100000) << 1) | ((a & 0b00001000) << 2) | ((a & 0b00000010) << 3) |
((b & 0b10000000) >> 4) | ((b & 0b00100000) >> 3) | ((b & 0b00001000) >> 2) | ((b & 0b00000010) >> 1);
let outputB =
((a & 0b01000000) << 1) | ((a & 0b00010000) << 2) | ((a & 0b00000100) << 3) | ((a & 0b00000001) << 4) |
((b & 0b01000000) >> 3) | ((b & 0b00010000) >> 2) | ((b & 0b00000100) >> 1) | (b & 0b00000001);
outputBuffer.push(outputA, outputB);
}
}
this.bitmapBuffer = new Uint8Array(outputBuffer);
this.updateBitmapPreview(buffer);
}
updateBitmapPreview(buffer) {
let previewBuffer = []
for (let i = 0; i < buffer.length; i++) {
const byte = buffer[i];
let bits = [
(byte & 0b11000000) >> 6,
(byte & 0b00110000) >> 4,
(byte & 0b00001100) >> 2,
(byte & 0b00000011)
]
for (let j = 0; j < bits.length; j++){
let rgba = BITMAP_PREVIEW_BYTES[bits[j]];
previewBuffer.push(...rgba);
}
}
this.bitmapPreviewBuffer = new Uint8ClampedArray(previewBuffer);
}
}
class Processor {
constructor(roms, tickerText) {
this.roms = roms;
this.menu = null;
this.disableCGB = false;
this.forceDMG = false;
this.tickerBitmap = [];
this.cartType = 0;
}
romTotalKB() {
return this.roms.reduce((total, rom) => {
return total += rom.romSizeKB();
}, 0);
}
romUsedKB() {
return this.roms.reduce((total, rom) => {
return total += rom.paddedRomSizeKB();
}, 0);
}
ramUsedKB() {
return this.roms.reduce((total, rom) => {
return total += rom.ramSizeKB();
}, 0);
}
romOverflow() {
return (this.romUsedKB() > 896);
}
mapData() {
const mapBuffer = new ArrayBuffer(128);
const mapFile = new FileSeeker(mapBuffer);
// write mbc type, rom size, and ram size for menu
mapFile.writeBytes([0xA8, 0, 0]);
// write mbc type, rom size, and ram size for each rom
let romOffset = 128;
let ramOffset = 0;
for (let i = 0; i < this.roms.length; i++) {
let rom = this.roms[i];
let bits = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] // 16
// set mbc bits
let tb = rom.typeByte
if (tb >= 0x01 && tb <= 0x03) {
bits[15] = 0; bits[14] = 0; bits[13] = 1;
} else if (tb >= 0x05 && tb <= 0x06) {
bits[15] = 0; bits[14] = 1; bits[13] = 0;
} else if (tb >= 0x0F && tb <= 0x13 ) {
bits[15] = 0; bits[14] = 1; bits[13] = 1;
} else if (tb >= 0x19 && tb <= 0x1E ) {
bits[15] = 1; bits[14] = 0; bits[13] = 0;
}
// set rom bits
let rs = rom.paddedRomSizeKB();
if (rs == 64) { // 010
bits[12] = 0; bits[11] = 1; bits[10] = 0;
} else if (rs == 128) { // 010
bits[12] = 0; bits[11] = 1; bits[10] = 0;
} else if (rs == 256) { // 011
bits[12] = 0; bits[11] = 1; bits[10] = 1;
} else if (rs == 512) { // 100
bits[12] = 1; bits[11] = 0; bits[10] = 0;
} else { // 101
bits[12] = 1; bits[11] = 0; bits[10] = 1;
}
// set ram bits
if (rom.typeByte == 0x06) { // MBC2+BATTERY 001
bits[9] = 0; bits[8] = 0; bits[7] = 1;
} else if (rom.ramSizeKB == 0) { // No RAM 000
bits[9] = 0; bits[8] = 0; bits[7] = 0;
} else if (rom.ramSizeKB == 8) { // 8KB 010
bits[9] = 0; bits[8] = 1; bits[7] = 0;
} else if (rom.ramSizeKB >= 32) { // 32KB+ 011
bits[9] = 0; bits[8] = 1; bits[7] = 1;
} else { // < 8KB 010
bits[9] = 0; bits[8] = 1; bits[7] = 0;
}
// rom offset and cart info bits
bits.reverse();
let bytes = [parseInt(bits.slice(0, 8).join(''), 2), parseInt(bits.slice(8, 16).join(''), 2)]
bytes[1] = bytes[1] | Math.trunc(romOffset / 32);
mapFile.writeBytes(bytes)
romOffset += rom.paddedRomSizeKB();
// ram offset
mapFile.writeByte(Math.trunc(ramOffset / 2));
ramOffset += (rom.typeByte == 0x06 || rom.ramSizeKB() < 8) ? 8 : rom.ramSizeKB();
}
// trailer
mapFile.writeByteUntil(0xFF, 128 - MAP_TRAILER_BYTES.length);
mapFile.writeBytes(MAP_TRAILER_BYTES);
return new Uint8Array(mapBuffer);
}
romData() {
const romBuffer = new ArrayBuffer(Math.pow(1024, 2));
const romFile = new FileSeeker(romBuffer);
// copy menu data
romFile.writeBytes(this.menu.data);
// apply iG power cart hack
if (this.cartType == 1) {
romFile.seek(0x130E); // For CGB+
romFile.writeBytes([0xC3, 0x00, 0x1F]); // Jump to 0x1F00
romFile.seek(0x140F); // For DMG/Pocket
romFile.writeBytes([0xC3, 0x00, 0x1F]); // Jump to 0x1F00
// Set rom bank, ram enabled/disabled, 8KB/32KB locked and ram bank
romFile.seek(0x1F00);
romFile.writeBytes([0x3e, 0x01, 0xea, 0x00, 0x20, // Set 0x2000, 1
0xf0, 0xfd, 0x01, 0x00, 0x22, 0x4f, 0x0a, 0xea, 0x00, 0x40, 0x3e, 0x00, 0xea, 0x00, 0x40, // Ram bank set bit 1+. 8KB/32KB locking = bit 0
0xf0, 0xfd, 0x01, 0x00, 0x21, 0x4f, 0x0a, 0xea, 0x00, 0x00, // Ram enable/disable
0xf0, 0xfd, 0x01, 0x00, 0x20, 0x4f, 0x0a, 0xea, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); // Rom bank and restart GB
// Patch out any writes to 0x0000-0x1000
romFile.seek(0x0DDF);
romFile.writeBytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
romFile.seek(0x0E22);
romFile.writeBytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
}
// apply cgb hack
if (this.disableCGB) {
romFile.seek(0x143);
romFile.writeByte(0);
romFile.seek(0x14D); // fix checksum
romFile.writeByte(83);
}
// apply dmg menu hack
if (this.forceDMG) {
romFile.seek(0x100);
romFile.writeByte(0xAF);
romFile.seek(0x150);
romFile.writeBytes([0x3C, 0xE0, 0xFE, 0x3D]);
}
// ticker text
romFile.seek(0x18040);
romFile.writeByteUntil(0x00, 0x19140); // overwrite existing data
romFile.seek(0x18040);
romFile.writeBytes(Array.from(this.tickerBitmap));
let romBase = 0x01;
let romFileIndex = 0x1C200;
// disable existing entries
for (let i = 0; i < 7; i++) {
romFile.seek(romFileIndex + i * 512);
romFile.writeByte(0xFF);
}
let romOffset = 8;
let ramOffset = 0;
for (let i = 0; i < this.roms.length; i++) {
const rom = this.roms[i];
romFile.seek(romFileIndex);
// rom index
romFile.writeByte(i + 1);
// rom base (in 128k units)
romFile.writeByte(romBase)
romBase += Math.trunc(rom.paddedRomSizeKB() / 128);
// sram base? (this is zero in the source)
romFile.writeByte(0)
// rom size (in 128k units)
romFile.writeByte(Math.trunc(rom.paddedRomSizeKB() / 128));
romFile.writeByte(0);
// sram size in 32b units (this is zero in the source)
romFile.writeByte(0)
romFile.writeByte(0)
romFile.seek(romFileIndex + 63);
// title bitmap
romFile.writeBytes(rom.bitmapBuffer);
romFileIndex += 512
// apply iG power cart hack
if (this.cartType == 1) {
// Set rom bank start
romFile.seek(0x2001 + i);
if (rom.typeByte >= 1 && rom.typeByte <= 3) {// MBC1 enabled, setting bank 0 = bank 1
romFile.writeByte(romOffset | 0x01);
}
else {
romFile.writeByte(romOffset);
}
romOffset += Math.trunc(rom.paddedRomSizeKB() / 16);
// Set ram state
romFile.seek(0x2101 + i);
if (rom.ramByte > 0) {
romFile.writeByte(1); // Ram enabled
}
else {
romFile.writeByte(0); // Ram disabled
}
// Set ram bank info
romFile.seek(0x2201 + i);
// Ram offset and if we are 8KB or 32KB locked
if (rom.ramByte == 2) { // 8KB
romFile.writeByte(ramOffset); // 8KB locked
}
else if (rom.ramByte == 3) { // 32KB
romFile.writeByte(ramOffset | 0x01); // 32KB locked
}
else {
romFile.writeByte(0);
}
// Increment ram offset after
if (rom.ramByte == 2) { // 8KB
ramOffset += 2;
}
else if (rom.ramByte == 3) { // 32KB
ramOffset += 8;
}
}
}
// write the roms
romFile.seek(0x20000)
for (let i = 0; i < this.roms.length; i++) {
romFile.writeBytes(new Uint8Array(this.roms[i].arrayBuffer));
}
return new Uint8Array(romBuffer);
}
parseMenuData(menuBuffer, fontIndex) {
this.roms = [];
const menuFile = new FileSeeker(menuBuffer);
let romSizes = [];
for (let i = 0; i < 7; i++) {
const indexPosition = 0x1C200 + i * 512
menuFile.seek(indexPosition);
const byte = menuFile.readByte();
if (byte > 0 && byte < 8) {
menuFile.seek(indexPosition + 3)
romSizes.push(menuFile.readByte());
}
}
if (romSizes.length > 0) {
try {
menuFile.seek(0x20000)
for (let i = 0; i < romSizes.length; i++) {
const romData = menuFile.read(romSizes[i] * 128 * 1024);
const romBuffer = (new Uint8Array(romData)).buffer;
this.roms.push(new ROM(romBuffer, fontIndex))
}
} catch(e) {
console.log(e)
alert("Failed to parse roms!");
}
} else {
alert("No roms detected!")
}
return this.roms;
}
}
class TickerText {
constructor(text, fontIndex, canvas) {
this.text = text;
this.fontIndex = fontIndex;
this.canvas = canvas;
}
generate() {
let buffer = [];
const canvas = this.canvas;
const text = this.text;
const font = FONTS[this.fontIndex].style;
const ctx = canvas.getContext('2d');
ctx.font = font;
let width = Math.ceil(ctx.measureText(text).width) + 2
for (let i = 0; i < 16; i++) {
if ((width % 16) == 0) { break; }
width++
}
width = Math.min(1024, Math.max(64, width));
canvas.width = width;
ctx.font = font;
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#888';
ctx.fillText(text,3,14);
ctx.fillStyle = 'white';
ctx.fillText(text,1,12);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
for (let i = 0; i < imageData.length; i+=16){
let byte = 0;
for (let j = 0; j < 4; j++) {
let red = imageData[i+j*4];
if (red < 100) {
byte = byte | 0b11 << (6 - j*2);
} else if (red < 210) {
byte = byte | 0b01 << (6 - j*2);
}
}
buffer.push(byte)
}
let outputBuffer = []
for (let h = 0; h < Math.trunc(canvas.width/4); h+=2) {
for (let i = h; i < canvas.width * 4; i+=Math.trunc(canvas.width/4)) {
let a = buffer[i]
let b = buffer[i+1]
let outputA =
(a & 0b10000000) | ((a & 0b00100000) << 1) | ((a & 0b00001000) << 2) | ((a & 0b00000010) << 3) |
((b & 0b10000000) >> 4) | ((b & 0b00100000) >> 3) | ((b & 0b00001000) >> 2) | ((b & 0b00000010) >> 1);
let outputB =
((a & 0b01000000) << 1) | ((a & 0b00010000) << 2) | ((a & 0b00000100) << 3) | ((a & 0b00000001) << 4) |
((b & 0b01000000) >> 3) | ((b & 0b00010000) >> 2) | ((b & 0b00000100) >> 1) | (b & 0b00000001);
outputBuffer.push(outputA, outputB);
}
}
return outputBuffer;
}
}
class FileSeeker {
constructor(arrayBuffer) {
this.view = new DataView(arrayBuffer);
this.position = 0;
}
seek(address) {
this.position = address;
}
rewind() {
this.position = 0;
}
size() {
return this.view.byteLength;
}
read(bytes) {
let data = [];
for (let i = 0; i < bytes; i++) {
data.push(this.readByte());
}
return data;
}
readByte() {
let byte = this.view.getUint8(this.position);
this.position++;
return byte;
}
writeByte(byte) {
this.view.setUint8(this.position, byte);
this.position++;
}
writeBytes(bytes) {
for (let i = 0; i < bytes.length; i++) {
this.writeByte(bytes[i]);
}
}
writeByteUntil(byte, stop) {
while (this.position < stop) {
this.writeByte(byte)
}
}
}