js-wrapper/nes-rom-file.js

const fs = require('fs'),
    path = require('path'),
    getCallingPath = require('../util/get-calling-path');

/**
 * Used to represent a NES rom. 
 * Contains a bunch of methods to analyze the static content in the rom.
 */
class NesRomFile {

    romFile = null;
    debugFile = null;

    _romData = [];
    _symbols = null;

    /**
     * Creates a NesRomFile instance.
     * @param {String} romFile The path to the rom, relative to the current test file.
     */
    constructor(romFile) {
        this.romFile = path.resolve(getCallingPath(), romFile);

        if (!fs.existsSync(this.romFile)) {
            throw new Error('Rom not found! -- ' + this.romFile);
        }
        this._romData = fs.readFileSync(this.romFile);

        const debugFile = this.romFile.substr(0, this.romFile.lastIndexOf('.')) + '.dbg';
        if (fs.existsSync(debugFile)) {
            this.debugFile = debugFile;
        }
    }

    /**
     * Test the rom's header, to make sure it is valid and makes sense for the rest of the rom contents.
     * @returns {Boolean} True if the rom has a valid header, and contents that match the header. Otherwise false.
     */
    hasValidHeader() {
        // Check first 4 characters
        if (!(this.raw[0] === 'N'.charCodeAt(0) && this.raw[1] === 'E'.charCodeAt(0) && this.raw[2] === 'S'.charCodeAt(0) && this.raw[3] === 0x1A)) {
            return false;
        }

        const prgLength = this.raw[4];
        const chrLength = this.raw[5];

        if (this.raw.length !== (16/* header*/) + (prgLength * 16384) + (chrLength * 8192)) {
            return false;
        }

        return true;
    }

    /**
     * Get the mapper number
     * @returns {Number} The mapper number specified in the header.
     * @see {@link https://wiki.nesdev.org/w/index.php?title=Mapper|NESDev Mapper Reference}
     */
    getMapper() {
        return (this.raw[6] >> 4) + (this.raw[7] & 0xf0);
    }

    /**
     * Gets the mirroring value from the rom header.
     * @returns {String} A string value of either "vertical" or "horizontal".
     */
    getMirroring() {
        return (this.raw[6] & 0x01) ? 'vertical' : 'horizontal';
    }

    /**
     * Check if the rom includes battery-backed ram.
     * @returns {Boolean} True if the header indicates using battery backed ram, otherwise false.
     */
    getIncludesBatteryBackedRam() {
        return !!(this.raw[6] & 0x02);
    }

    /**
     * Test if the rom includes 4 screen vram.
     * @returns {Boolean} True if the header indicates supporting 4 screen vram, false otherwise.
     */
    getIncludesFourScreenVram() {
        return !!(this.raw[6] & 0x08);
    }

    /**
     * Array containing the raw data from the rom, for your own inspection.
     */
    get raw() {
        return this._romData;
    }

    /**
     * If there is a `.dbg` file with the same name as the rom in the same folder, this will have a list of 
     * all recognized assembly and c symbols parsed from the debug file, mapped to memory addresses. 
     * If you have debugging in mesen working, this work with the same file. 
     *
     * It has two sub-objects: `assembly` and `c`. (The C one will only be populated if you created a game with C debugging
     * symbols.)

     * This is used within NesEmulator to test ram values at these locations.     
    */
    get symbols() {
        if (this.debugFile !== null) {
            if (this._symbols === null) {
                this._symbols = this._parseSymbols();
            }
            return this._symbols;
        } else {
            return null;
        }
    }

    // Get the symbols out of the .dbg file, assuming it exists.
    _parseSymbols() {
        const usefulLines = fs.readFileSync(this.debugFile).toString().split('\n').filter(l => (l.startsWith('sym') || l.startsWith('csym')));

        let symbolTable = {c: {}, assembly: {}};
        let cSymbols = [];

        usefulLines.forEach(line => {
            // NOTE: We rely on c symbols appearing earlier in the file than regular symbols, so we can look them up!
            if (line.startsWith('csym')) {
                // Skip any lines that don't have a symbol they're linked to. Not much we can do without that.
                if (line.indexOf(',sym=') === -1 ) { return; }
                const nameIdx = line.indexOf(',name="') + 7,
                    nameEnd = line.indexOf('"', nameIdx+1);
                    
                    // Since we're going to look up by name, we can bypass a lot of the funkiness with linking these
                    // and just grab the names. 
                    cSymbols.push(line.substring(nameIdx, nameEnd));
            } else {
                // Skip symbols that don't have a value, we can't ddo anything with them
                if (line.indexOf(',val=') === -1) { return; }
                
                const nameIdx = line.indexOf(',name="') + 7,
                    nameEnd = line.indexOf('"', nameIdx+1),
                    name = line.substring(nameIdx, nameEnd),
                    valIdx = line.indexOf(',val=0x') + 7,
                    valEnd = line.indexOf(',', valIdx),
                    valStr = line.substring(valIdx, valEnd);

                symbolTable.assembly[name] = parseInt(valStr, 16);

                if (cSymbols.indexOf(name.substring(1)) !== -1) {
                    symbolTable.c[name.substring(1)] = parseInt(valStr, 16);
                }
            }
        });

        return symbolTable;
    }
}

module.exports = NesRomFile;