wasmboy
Version:
Gameboy / Gameboy Color Emulator written for Web Assembly using AssemblyScript. Shell/Debugger in Preact
702 lines (564 loc) • 21.2 kB
JavaScript
import { idbKeyval } from './idb';
// Import worker stuff
import { WORKER_MESSAGE_TYPE, MEMORY_TYPE } from '../worker/constants';
import { getEventData } from '../worker/util';
// Fetch rom
import { fetchROMAsByteArray } from '../wasmboy/fetchrom';
// import Functions involving GB and WasmBoy memory
import { getSaveState } from './state.js';
import { initializeAutoSave } from './autosave.js';
const BOOT_ROM_KEY = 'boot-rom-';
class WasmBoyMemoryService {
constructor() {
this.worker = undefined;
this.maxNumberOfAutoSaveStates = undefined;
this.saveStateCallback = undefined;
this.loadedCartridgeMemoryState = {
ROM: false,
RAM: false,
BOOT: false
};
// Our different types of memory
this.bootRom = undefined;
this.cartridgeRom = undefined;
this.cartridgeRomFileName = undefined;
this.cartridgeHeader = undefined;
this.cartridgeRam = undefined;
this.gameboyMemory = undefined;
this.paletteMemory = undefined;
this.internalState = undefined;
// Going to set the key for idbKeyval as the cartridge header.
// Then, for each cartridge, it will return an object.
// there will be a cartridgeRam Key, settings Key, and a saveState key
// Not going to make one giant object, as we want to keep idb transactions light and fast
this.WASMBOY_UNLOAD_STORAGE = 'WASMBOY_UNLOAD_STORAGE';
// Define some constants since calls to wasm are expensive
this.WASMBOY_GAME_BYTES_LOCATION = 0;
this.WASMBOY_GAME_RAM_BANKS_LOCATION = 0;
this.WASMBOY_INTERNAL_STATE_SIZE = 0;
this.WASMBOY_INTERNAL_STATE_LOCATION = 0;
this.WASMBOY_INTERNAL_MEMORY_SIZE = 0;
this.WASMBOY_INTERNAL_MEMORY_LOCATION = 0;
this.WASMBOY_PALETTE_MEMORY_SIZE = 0;
this.WASMBOY_PALETTE_MEMORY_LOCATION = 0;
// Define some other constants
this.SUPPORTED_BOOT_ROM_TYPES = {
GB: 'GB',
GBC: 'GBC'
};
}
initialize(headless, maxNumberOfAutoSaveStates, saveStateCallback) {
this.maxNumberOfAutoSaveStates = maxNumberOfAutoSaveStates;
this.saveStateCallback = saveStateCallback;
const initializeTask = async () => {
await this._initializeConstants();
if (!headless) {
await initializeAutoSave.call(this);
}
};
return initializeTask();
}
setWorker(worker) {
this.worker = worker;
// Also set our handler
this.worker.addMessageListener(event => {
const eventData = getEventData(event);
switch (eventData.message.type) {
case WORKER_MESSAGE_TYPE.UPDATED: {
// Simply set our memory
const memoryTypes = Object.keys(eventData.message);
delete memoryTypes.type;
if (memoryTypes.includes(MEMORY_TYPE.BOOT_ROM)) {
this.bootRom = new Uint8Array(eventData.message[MEMORY_TYPE.BOOT_ROM]);
}
if (memoryTypes.includes(MEMORY_TYPE.CARTRIDGE_ROM)) {
this.cartridgeRom = new Uint8Array(eventData.message[MEMORY_TYPE.CARTRIDGE_ROM]);
}
if (memoryTypes.includes(MEMORY_TYPE.CARTRIDGE_RAM)) {
this.cartridgeRam = new Uint8Array(eventData.message[MEMORY_TYPE.CARTRIDGE_RAM]);
}
if (memoryTypes.includes(MEMORY_TYPE.GAMEBOY_MEMORY)) {
this.gameboyMemory = new Uint8Array(eventData.message[MEMORY_TYPE.GAMEBOY_MEMORY]);
}
if (memoryTypes.includes(MEMORY_TYPE.PALETTE_MEMORY)) {
this.paletteMemory = new Uint8Array(eventData.message[MEMORY_TYPE.PALETTE_MEMORY]);
}
if (memoryTypes.includes(MEMORY_TYPE.INTERNAL_STATE)) {
this.internalState = new Uint8Array(eventData.message[MEMORY_TYPE.INTERNAL_STATE]);
}
return;
}
}
});
}
// Function to get all cartridge objects
// Saved in our indexed db
getSavedMemory() {
const getSavedMemoryTask = async () => {
const memory = [];
const keys = await idbKeyval.keys();
for (let i = 0; i < keys.length; i++) {
const cartridgeObject = await idbKeyval.get(keys[i]);
memory.push(cartridgeObject);
}
return memory;
};
return getSavedMemoryTask();
}
getLoadedCartridgeMemoryState() {
return this.loadedCartridgeMemoryState;
}
clearMemory() {
// Clear Wasm memory
// https://docs.google.com/spreadsheets/d/17xrEzJk5-sCB9J2mMJcVnzhbE-XH_NvczVSQH9OHvRk/edit?usp=sharing
return this.worker
.postMessage({
type: WORKER_MESSAGE_TYPE.CLEAR_MEMORY
})
.then(event => {
this.loadedCartridgeMemoryState.ROM = false;
this.loadedCartridgeMemoryState.RAM = false;
// Reset everything
this.cartridgeRom = undefined;
this.cartridgeHeader = undefined;
this.cartridgeRam = undefined;
this.gameboyMemory = undefined;
this.paletteMemory = undefined;
this.internalState = undefined;
});
}
isValidBootROMType(type) {
return Object.keys(this.SUPPORTED_BOOT_ROM_TYPES).some(bootROMTypeKey => {
return this.SUPPORTED_BOOT_ROM_TYPES[bootROMTypeKey] === type;
});
}
async addBootROM(type, file, fetchHeaders, additionalInfo) {
type = type.toUpperCase();
if (!this.isValidBootROMType(type)) {
throw new Error('Invalid Boot ROM type');
}
// Get our fetch rom object
const fetchROMObject = await fetchROMAsByteArray(file, fetchHeaders);
// Remove any keys we don't want to allow
// Overriding in the additionalInfo
if (additionalInfo) {
delete additionalInfo.name;
delete additionalInfo.ROM;
}
let name = 'Game Boy';
if (this.SUPPORTED_BOOT_ROM_TYPES.GBC === type) {
name = 'Game Boy Color';
}
const bootROMObject = {
ROM: fetchROMObject.ROM,
name,
type,
date: Date.now(),
...additionalInfo
};
await idbKeyval.set(BOOT_ROM_KEY + type, bootROMObject);
}
async getBootROMs() {
const bootROMs = [];
for (let bootROMType in this.SUPPORTED_BOOT_ROM_TYPES) {
const bootROMObject = await idbKeyval.get(BOOT_ROM_KEY + bootROMType);
if (bootROMObject) {
bootROMs.push(bootROMObject);
}
}
return bootROMs;
}
async loadBootROMIfAvailable(type) {
if (!idbKeyval) {
// TODO: Allow headless Boot ROMs
return;
}
type = type.toUpperCase();
if (!this.isValidBootROMType(type)) {
throw new Error('Invalid Boot ROM type');
}
// Try to get the boot rom object
const bootROMObject = await idbKeyval.get(BOOT_ROM_KEY + type);
if (!bootROMObject) {
// Return silently
return;
}
const workerMemoryObject = {};
workerMemoryObject[MEMORY_TYPE.BOOT_ROM] = bootROMObject.ROM.buffer;
// Don't pass the rom as a transferrable, since,
// We want to keep a copy of it for reset
await this.worker
.postMessage({
type: WORKER_MESSAGE_TYPE.SET_MEMORY,
...workerMemoryObject
})
.then(event => {
this.loadedCartridgeMemoryState.BOOT = true;
});
// Also get our cartridge header
await this.worker
.postMessage({
type: WORKER_MESSAGE_TYPE.GET_MEMORY,
memoryTypes: [MEMORY_TYPE.BOOT_ROM]
})
.then(event => {
const eventData = getEventData(event);
this.bootRom = new Uint8Array(eventData.message[MEMORY_TYPE.BOOT_ROM]);
});
}
loadCartridgeRom(ROM, fileName) {
const loadTask = async () => {
const workerMemoryObject = {};
workerMemoryObject[MEMORY_TYPE.CARTRIDGE_ROM] = ROM.buffer;
// Don't pass the rom as a transferrable, since,
// We want to keep a copy of it for reset
await this.worker
.postMessage({
type: WORKER_MESSAGE_TYPE.SET_MEMORY,
...workerMemoryObject
})
.then(event => {
this.loadedCartridgeMemoryState.ROM = true;
});
// Also get our cartridge header
await this.worker
.postMessage({
type: WORKER_MESSAGE_TYPE.GET_MEMORY,
memoryTypes: [MEMORY_TYPE.CARTRIDGE_ROM, MEMORY_TYPE.CARTRIDGE_HEADER]
})
.then(event => {
const eventData = getEventData(event);
this.cartridgeRom = new Uint8Array(eventData.message[MEMORY_TYPE.CARTRIDGE_ROM]);
this.cartridgeRomFileName = fileName;
this.cartridgeHeader = new Uint8Array(eventData.message[MEMORY_TYPE.CARTRIDGE_HEADER]);
});
};
return loadTask();
}
saveLoadedCartridge(additionalInfo) {
const saveLoadedCartridgeRomTask = async () => {
if (!this.cartridgeHeader) {
throw new Error('Error parsing the cartridge header');
}
let cartridgeObject = await idbKeyval.get(this.cartridgeHeader);
if (!cartridgeObject) {
cartridgeObject = {};
}
const cartridgeInfo = await this.getCartridgeInfo();
// Remove any keys we don't want to allow
// Overriding in the additionalInfo
if (additionalInfo) {
delete additionalInfo.ROM;
delete additionalInfo.header;
}
// In the rare chance we don't know the name, set to unkown.
let fileName = this.cartridgeRomFileName || 'Unknown';
cartridgeObject.cartridgeRom = {
ROM: this.cartridgeRom,
header: this.cartridgeHeader,
fileName: fileName,
date: Date.now(),
...additionalInfo
};
cartridgeObject.cartridgeInfo = cartridgeInfo;
if (this.cartridgeRam) {
await this.saveCartridgeRam();
}
await idbKeyval.set(this.cartridgeHeader, cartridgeObject);
return cartridgeObject;
};
return saveLoadedCartridgeRomTask();
}
deleteSavedCartridge(cartridge) {
const deleteLoadedCartridgeTask = async () => {
const cartridgeHeader = cartridge.cartridgeInfo.header;
if (!cartridgeHeader) {
throw new Error('Error parsing the cartridge header');
}
let cartridgeObject = await idbKeyval.get(cartridgeHeader);
if (!cartridgeObject) {
throw new Error('Could not find the passed cartridge');
}
delete cartridgeObject.cartridgeRom;
await idbKeyval.set(cartridgeHeader, cartridgeObject);
return cartridgeObject;
};
return deleteLoadedCartridgeTask();
}
// Function to save the cartridge ram
// This emulates the cartridge having a battery to
// Keep things like Pokemon Save data in memory
// Also allows passing in a a Uint8Array header and ram to be set manually
saveCartridgeRam(passedHeader, passedCartridgeRam) {
const saveCartridgeRamTask = async () => {
// Get the entire header in byte memory
// Each version of a rom can have similar title and checksums
// Therefore comparing all of it should help with this :)
// https://drive.google.com/file/d/0B7y-o-Uytiv9OThXWXFCM1FPbGs/view
let header;
let cartridgeRam;
if (passedHeader && passedCartridgeRam) {
header = passedHeader;
cartridgeRam = passedCartridgeRam;
} else {
header = this.cartridgeHeader;
cartridgeRam = this.cartridgeRam;
}
if (!header || !cartridgeRam) {
throw new Error('Error parsing the cartridgeRam or cartridge header');
}
// Get our cartridge object
let cartridgeObject = await idbKeyval.get(header);
if (!cartridgeObject) {
cartridgeObject = {};
}
// Set the cartridgeRam to our cartridgeObject
cartridgeObject.cartridgeRam = cartridgeRam;
await idbKeyval.set(header, cartridgeObject);
};
return saveCartridgeRamTask();
}
// function to load the cartridge ram
// opposite of above
loadCartridgeRam() {
const loadCartridgeRamTask = async () => {
const header = this.cartridgeHeader;
if (!header) {
throw new Error('Error parsing the cartridge header');
}
const cartridgeObject = await idbKeyval.get(header);
if (!cartridgeObject || !cartridgeObject.cartridgeRam) {
return;
}
// Set the cartridgeRam
// Don't transfer, because we want to keep a reference to it
const workerMemoryObject = {};
workerMemoryObject[MEMORY_TYPE.CARTRIDGE_RAM] = cartridgeObject.cartridgeRam.buffer;
await this.worker
.postMessage({
type: WORKER_MESSAGE_TYPE.SET_MEMORY,
...workerMemoryObject
})
.then(event => {
this.loadedCartridgeMemoryState.RAM = true;
this.cartridgeRam = cartridgeObject.cartridgeRam;
});
};
return loadCartridgeRamTask();
}
// Function to save the state to the indexeddb
saveState(passedHeader, passedSaveState) {
const saveStateTask = async () => {
// Get our save state
let saveState;
let header;
if (passedHeader && passedSaveState) {
saveState = passedSaveState;
header = passedHeader;
} else {
saveState = getSaveState.call(this);
header = this.cartridgeHeader;
}
if (!header) {
throw new Error('Error parsing the cartridge header');
}
let cartridgeObject = await idbKeyval.get(header);
if (!cartridgeObject) {
cartridgeObject = {};
}
if (!cartridgeObject.saveStates) {
cartridgeObject.saveStates = [];
}
// Check if we are auto
if (saveState.isAuto && this.maxNumberOfAutoSaveStates && this.maxNumberOfAutoSaveStates > 0) {
// Make sure we are not exceeding the max number of auto save states
const autoSaveStates = [];
cartridgeObject.saveStates.forEach(savedState => {
if (savedState.isAuto) {
autoSaveStates.push(savedState);
}
});
// Sort auto save states by date
autoSaveStates.sort((a, b) => {
if (a.date < b.date) {
return -1;
}
if (a.date > b.date) {
return 1;
}
return 0;
});
while (autoSaveStates.length > 0 && autoSaveStates.length + 1 > this.maxNumberOfAutoSaveStates) {
const autoSaveState = autoSaveStates.shift();
// Find the save state
const saveStateIndex = this._indexOfSaveStateIndexInSaveStates(autoSaveState, cartridgeObject.saveStates);
cartridgeObject.saveStates.splice(saveStateIndex, 1);
}
if (this.maxNumberOfAutoSaveStates > 0) {
cartridgeObject.saveStates.push(saveState);
}
} else {
cartridgeObject.saveStates.push(saveState);
}
await idbKeyval.set(header, cartridgeObject);
return saveState;
};
return saveStateTask();
}
loadState(saveState) {
const loadStateTask = async () => {
const header = this.cartridgeHeader;
if (!header) {
throw new Error('Error getting the cartridge header');
}
if (!saveState) {
const cartridgeObject = await idbKeyval.get(header);
if (!cartridgeObject || !cartridgeObject.saveStates) {
throw new Error('No Save State passed, and no cartridge object found');
return;
}
saverState = cartridgeObject.saveStates[0];
}
const workerMemoryObject = {};
workerMemoryObject[MEMORY_TYPE.CARTRIDGE_RAM] = saveState.wasmboyMemory.cartridgeRam.buffer;
workerMemoryObject[MEMORY_TYPE.GAMEBOY_MEMORY] = saveState.wasmboyMemory.gameBoyMemory.buffer;
workerMemoryObject[MEMORY_TYPE.PALETTE_MEMORY] = saveState.wasmboyMemory.wasmBoyPaletteMemory.buffer;
workerMemoryObject[MEMORY_TYPE.INTERNAL_STATE] = saveState.wasmboyMemory.wasmBoyInternalState.buffer;
await this.worker.postMessage(
{
type: WORKER_MESSAGE_TYPE.SET_MEMORY,
...workerMemoryObject
},
[
workerMemoryObject[MEMORY_TYPE.CARTRIDGE_RAM],
workerMemoryObject[MEMORY_TYPE.GAMEBOY_MEMORY],
workerMemoryObject[MEMORY_TYPE.PALETTE_MEMORY],
workerMemoryObject[MEMORY_TYPE.INTERNAL_STATE]
]
);
await this.worker
.postMessage({
type: WORKER_MESSAGE_TYPE.GET_MEMORY,
memoryTypes: [MEMORY_TYPE.CARTRIDGE_RAM, MEMORY_TYPE.GAMEBOY_MEMORY, MEMORY_TYPE.PALETTE_MEMORY, MEMORY_TYPE.INTERNAL_STATE]
})
.then(event => {
const eventData = getEventData(event);
this.cartridgeRam = eventData.message[MEMORY_TYPE.CARTRIDGE_RAM];
this.gameboyMemory = eventData.message[MEMORY_TYPE.GAMEBOY_MEMORY];
this.paletteMemory = eventData.message[MEMORY_TYPE.PALETTE_MEMORY];
this.internalState = eventData.message[MEMORY_TYPE.INTERNAL_STATE];
});
};
return loadStateTask();
}
deleteState(saveState, passedHeader) {
const deleteStateTask = async () => {
if (!saveState) {
throw new Error('You must provide a save state to delete');
return;
}
let header;
if (passedHeader) {
header = passedHeader;
} else if (this.cartridgeHeader) {
header = this.cartridgeHeader;
}
if (!header) {
throw new Error('Please load a ROM, or pass a Cartridge header...');
return;
}
let cartridgeObject = await idbKeyval.get(header);
if (!cartridgeObject || !cartridgeObject.saveStates) {
throw new Error('No save states found for the Cartridge...');
return;
}
// Find the save state
const saveStateIndex = this._indexOfSaveStateIndexInSaveStates(saveState, cartridgeObject.saveStates);
// If not found, throw an error
if (saveStateIndex < 0) {
throw new Error('Could not find the passed save state for the related cartridge...');
return;
}
cartridgeObject.saveStates.splice(saveStateIndex, 1);
await idbKeyval.set(header, cartridgeObject);
return saveState;
};
return deleteStateTask();
}
// Function to return the current cartridge object
getCartridgeObject() {
return idbKeyval.get(this.cartridgeHeader);
}
// Function to return all informationh aboyut the currently loaded cart.
// This will include, the ROM, the RAM, the header, and the indivudal pieces of the header
// See: http://gbdev.gg8.se/wiki/articles/The_Cartridge_Header
getCartridgeInfo() {
if (!this.loadedCartridgeMemoryState.ROM) {
return Promise.reject('No ROM has been loaded');
}
let getCartridgeInfoTask = async () => {
const cartridgeInfo = {};
cartridgeInfo.header = this.cartridgeHeader;
cartridgeInfo.ROM = this.cartridgeRom;
cartridgeInfo.RAM = this.cartridgeRam;
// Now parse our header for additional information
cartridgeInfo.nintendoLogo = cartridgeInfo.ROM.slice(0x104, 0x134);
cartridgeInfo.title = cartridgeInfo.ROM.slice(0x134, 0x144);
cartridgeInfo.titleAsString = String.fromCharCode.apply(null, cartridgeInfo.title);
cartridgeInfo.manufacturerCode = cartridgeInfo.ROM.slice(0x13f, 0x143);
cartridgeInfo.CGBFlag = cartridgeInfo.ROM[0x143];
cartridgeInfo.newLicenseeCode = cartridgeInfo.ROM.slice(0x144, 0x146);
cartridgeInfo.SGBFlag = cartridgeInfo.ROM[0x146];
cartridgeInfo.cartridgeType = cartridgeInfo.ROM[0x147];
cartridgeInfo.ROMSize = cartridgeInfo.ROM[0x148];
cartridgeInfo.RAMSize = cartridgeInfo.ROM[0x149];
cartridgeInfo.destinationCode = cartridgeInfo.ROM[0x14a];
cartridgeInfo.oldLicenseeCode = cartridgeInfo.ROM[0x14b];
cartridgeInfo.maskROMVersionNumber = cartridgeInfo.ROM[0x14c];
cartridgeInfo.headerChecksum = cartridgeInfo.ROM[0x14d];
cartridgeInfo.globalChecksum = cartridgeInfo.ROM.slice(0x14e, 0x150);
return cartridgeInfo;
};
return getCartridgeInfoTask();
}
_initializeConstants() {
// Initialize our cached wasm constants
return this.worker
.postMessage({
type: WORKER_MESSAGE_TYPE.GET_CONSTANTS
})
.then(event => {
const eventData = getEventData(event);
Object.keys(this).forEach(key => {
if (eventData.message[key] !== undefined) {
this[key] = eventData.message[key];
}
});
});
}
_indexOfSaveStateIndexInSaveStates(saveState, saveStates) {
// Find the save state
let saveStateIndex = saveStates.indexOf(saveState);
if (saveStateIndex < 0) {
const keysCheck = (a, b) => {
return JSON.stringify(Object.keys(a)) === JSON.stringify(Object.keys(b));
};
const dateCheck = (a, b) => {
return a.date === b.date;
};
const autoCheck = (a, b) => {
return a.isAuto === b.isAuto;
};
saveStates.some((savedState, index) => {
if (keysCheck(saveState, savedState) && dateCheck(saveState, savedState) && autoCheck(saveState, savedState)) {
saveStateIndex = index;
return true;
}
return false;
});
}
return saveStateIndex;
}
}
// Create a singleton to export
export const WasmBoyMemory = new WasmBoyMemoryService();