package app; // Java imports import java.nio.charset.*; import java.util.*; // Cartridge ROM module public class ROM { // Instance fields private byte[] data; // Binary data private int format; // File format of loaded file /////////////////////////////////////////////////////////////////////////// // Constants // /////////////////////////////////////////////////////////////////////////// // Formats public static final int RAW = 0; public static final int ISX = 1; /////////////////////////////////////////////////////////////////////////// // Classes // /////////////////////////////////////////////////////////////////////////// // ISX code segments private static class Code { int address; // CPU address int offset; // Position within file int size; // Number of bytes Code(int o, int a, int s) { address = a; offset = o; size = s; } } /////////////////////////////////////////////////////////////////////////// // Constructors // /////////////////////////////////////////////////////////////////////////// // Default constructor ROM(byte[] data) { // Attempt to decode as ISX try { isxDecode(data); return; } catch (Exception e) { } // Attempt to decode as raw try { rawDecode(data); return; } catch (Exception e) { } // Unable to identify the file contents throw new RuntimeException(); } /////////////////////////////////////////////////////////////////////////// // Public Methods // /////////////////////////////////////////////////////////////////////////// // Retrieve the game code from the ROM header public String getGameCode() { var bytes = new byte[4]; System.arraycopy(data, data.length - 517, bytes, 0, 4); return new String(bytes, StandardCharsets.ISO_8859_1); } // Retrieve the file format of the loaded ROM file public int getFormat() { return format; } // Retrieve the maker code from the ROM header public String getMaker() { var bytes = new byte[2]; System.arraycopy(data, data.length - 519, bytes, 0, 2); return new String(bytes, StandardCharsets.ISO_8859_1); } // Retrieve the number of bytes in the ROM data public int getSize() { return data.length; } // Retrieve the game title from the ROM header public String getTitle() { return ShiftJIS.decode(data, data.length - 544, 20).trim(); } // Retrieve the version number from the ROM header public int getVersion() { return data[data.length - 513] & 0xFF; } // Produce a byte array containing the ROM contents public byte[] toByteArray() { return data; } /////////////////////////////////////////////////////////////////////////// // Private Methods // /////////////////////////////////////////////////////////////////////////// // Decode the file data as ISX private void isxDecode(byte[] data) { var codes = isxParse(data); // ISX code segments int head = -1; // Latest address in bottom half of ROM address space int tail = -1; // Earliest address in upper half of ROM address space // Process all code segments for (var code : codes) { int end = code.address + code.size - 1; int start = code.address; boolean isHead = end >= 0; // Lower half of CPU address range boolean isTail = start < 0; // Upper half of CPU address range // The segment spans the middle of the ROM address space if (isHead && isTail) { head = 0x7FFFFF; tail = 0x800000; break; } // Track the segment's position within the ROM address space end &= 0x00FFFFFF; start &= 0x00FFFFFF; if (isHead && (head == -1 || head < end )) head = end; if (isTail && (tail == -1 || tail > start)) tail = start; } // Determine the required ROM size int size = 1024; int minSize = (head == -1 ? 0 : head + 1) + (tail == -1 ? 0 : 0x01000000 - tail); if (minSize == 0 || tail == -1 && (minSize - 1 & minSize) != 0) throw new RuntimeException(); for (; size < minSize; size <<= 1); // Transfer the code segments into the ROM buffer this.data = new byte[size]; for (var code : codes) System.arraycopy( data , code.offset, this.data, code.address & size - 1, code.size ); // The ROM is valid format = ISX; } // Parse the code records from the ISX data private Code[] isxParse(byte[] data) { int count; int offset = 0; var ret = new ArrayList<Code>(); // Check for an extended ISX header if (readInt(data, 0, 3) == 0x585349) // ASCII "ISX" offset = 32; // Process records while (offset != data.length) switch (data[offset++] & 0xFF) { // SNES code case 0x01: if ((data[offset++] & 0xFF) >= 0x80) // Bank offset++; // BankHigh offset += 2; // Address offset += 2 + readInt(data, offset, 2); // Data break; // SNES range case 0x03: // Count, { Bank, StartAddress, EndAddress, Type } offset += 2 + readInt(data, offset, 2) * 6; break; // SNES symbol case 0x04: count = readInt(data, offset, 2); offset += 2; for (; count > 0; count--) // Name, Flags, Bank, Address offset += 5 + readInt(data, offset, 1); break; // Virtual Boy code case 0x11: int address = readInt(data, offset, 4); offset += 4; int size = readInt(data, offset, 4); offset += 4; if ((address & 0x07000000) != 0x07000000 || size < 0 || (address & 0x00FFFFFF) + size > 0x01000000) throw new RuntimeException(); ret.add(new Code(offset, address, size)); offset += size; break; // Virtual Boy range case 0x13: // Count, { StartAddress, EndAddress, Type } offset += 2 + readInt(data, offset, 2) * 9; break; // Virtual Boy symbol case 0x14: count = readInt(data, offset, 2); offset += 2; for (; count > 0; count--) // Name, Flags, Address offset += 7 + readInt(data, offset, 1); break; // System case 0x20: case 0x21: case 0x22: offset += 4 + readInt(data, offset, 4); // Debug, undocumented break; // Invalid record type default: throw new RuntimeException(); } return ret.toArray(new Code[ret.size()]); } // Decode the file as raw private void rawDecode(byte[] data) { // Validate length if ( data.length < 1024 || // Exception handlers and ROM header data.length > 0x00FFFFFF || // 24-bit bus width (data.length & data.length - 1) != 0 // Must be a power of 2 ) throw new RuntimeException(); // The ROM is valid this.data = data; format = RAW; } // Read an integer from a byte array private static int readInt(byte[] data, int offset, int size) { int ret = 0; for (size--; size >= 0; size--) ret = ret << 8 | data[offset + size] & 0xFF; return ret; } }