mirror of
https://github.com/orangeglo/gbnp.git
synced 2026-04-17 22:26:16 -05:00
376 lines
9.8 KiB
JavaScript
376 lines
9.8 KiB
JavaScript
const CARTRIDGE_TYPES = [
|
|
'ROM Only',
|
|
'MBC1', 'MBC1+RAM', 'MBC1+RAM+BATTERY', null,
|
|
'MBC2', 'MBC2+BATTERY', null,
|
|
'ROM+RAM', 'ROM+RAM+BATTERY', null, null, null, null, null,
|
|
'MBC3+TIMER+BATTERY', 'MBC3+TIMER+RAM+BATTERY', 'MBC3', 'MBC3+RAM', 'MBC3+RAM+BATTERY', null, null, null, null, null,
|
|
'MBC5', 'MBC5+RAM', 'MBC5+RAM+BATTERY', 'MBC5+RUMBLE', 'MBC5+RUMBLE+RAM', 'MBC5+RUMBLE+RAM+BATTERY'
|
|
];
|
|
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 FONTS = [
|
|
{ style: 'normal 16px Pixeltype', y: 7 },
|
|
{ style: 'normal 8px Early-Gameboy', 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.loadedFromStorage = true;
|
|
this.data = JSON.parse(menuData).data;
|
|
}
|
|
}
|
|
|
|
present() {
|
|
return !!this.data
|
|
}
|
|
|
|
valid() {
|
|
return true;
|
|
}
|
|
|
|
ready() {
|
|
return this.present() && this.valid();
|
|
}
|
|
|
|
setData(arrayBuffer) {
|
|
this.data = new Uint8Array(arrayBuffer);
|
|
if (this.valid()) {
|
|
localStorage.setItem('menuData', JSON.stringify({ data: Array.from(this.data) }));
|
|
}
|
|
}
|
|
}
|
|
|
|
class ROM {
|
|
constructor(arrayBuffer, fontIndex) {
|
|
let file = new FileSeeker(arrayBuffer);
|
|
|
|
file.seek(0x134);
|
|
this.title = String.fromCharCode(...file.read(0xF));
|
|
this.menuText = this.title.replace(/\0/g, '');
|
|
|
|
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();
|
|
|
|
file.rewind();
|
|
this.arrayBuffer = new ArrayBuffer(this.paddedRomSizeKB() * 1024);
|
|
let paddedFile = new FileSeeker(this.arrayBuffer);
|
|
paddedFile.writeBytes(file.read(file.size()));
|
|
|
|
this.updateBitmap(fontIndex);
|
|
|
|
// add error for "invalid" rom (IE not a gb rom file)
|
|
if (!this.type) { alert('Cartridge type could not be determined!') }
|
|
if (this.ramSizeKB() > 32) { alert('Game requires more than 32 KB of RAM!') }
|
|
}
|
|
|
|
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 = 96;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.imageSmoothingEnabled = false;
|
|
ctx.fillStyle = 'white';
|
|
ctx.fillRect(0, 0, 96, 8);
|
|
const font = FONTS[fontIndex || 0];
|
|
ctx.font = font.style;
|
|
ctx.fillStyle = 'black';
|
|
ctx.fillText(this.menuText,1,font.y);
|
|
const imageData = ctx.getImageData(0, 0, 96, 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 < 24; h+=2) {
|
|
for (let i = h; i < 192; i+=24) {
|
|
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) {
|
|
this.roms = roms;
|
|
this.menu = null;
|
|
}
|
|
|
|
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);
|
|
|
|
let romBase = 0x01;
|
|
let romFileIndex = 0x1C200;
|
|
|
|
// disable existing entries
|
|
for (let i = 0; i < 7; i++) {
|
|
romFile.seek(romFileIndex + i * 512);
|
|
romFile.writeByte(0xFF);
|
|
}
|
|
|
|
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.writeByteUntil(0xFF, romFileIndex + 63 + 192); // solid black
|
|
romFile.writeBytes(rom.bitmapBuffer);
|
|
|
|
rom.bitmapBuffer
|
|
|
|
romFileIndex += 512
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|