pvbemu/src/desktop/app/ROM.java

255 lines
8.2 KiB
Java

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() {
var ret = new byte[data.length];
System.arraycopy(data, 0, ret, 0, data.length);
return ret;
}
///////////////////////////////////////////////////////////////////////////
// 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 < 1024 || (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;
}
}