160 lines
5.2 KiB
Zig
160 lines
5.2 KiB
Zig
const std = @import("std");
|
|
|
|
const Backup = @import("backup.zig").Backup;
|
|
const Allocator = std.mem.Allocator;
|
|
const log = std.log.scoped(.GamePak);
|
|
|
|
const intToBytes = @import("../util.zig").intToBytes;
|
|
|
|
const Self = @This();
|
|
|
|
title: [12]u8,
|
|
buf: []u8,
|
|
alloc: Allocator,
|
|
backup: Backup,
|
|
|
|
pub fn init(alloc: Allocator, rom_path: []const u8, save_path: ?[]const u8) !Self {
|
|
const file = try std.fs.cwd().openFile(rom_path, .{});
|
|
defer file.close();
|
|
|
|
const file_buf = try file.readToEndAlloc(alloc, try file.getEndPos());
|
|
const title = parseTitle(file_buf);
|
|
const kind = Backup.guessKind(file_buf) orelse .None;
|
|
|
|
const pak = Self{
|
|
.buf = file_buf,
|
|
.alloc = alloc,
|
|
.title = title,
|
|
.backup = try Backup.init(alloc, kind, title, save_path),
|
|
};
|
|
pak.parseHeader();
|
|
|
|
return pak;
|
|
}
|
|
|
|
fn parseHeader(self: *const Self) void {
|
|
const title = parseTitle(self.buf);
|
|
const code = self.buf[0xAC..0xB0];
|
|
const maker = self.buf[0xB0..0xB2];
|
|
const version = self.buf[0xBC];
|
|
|
|
log.info("Title: {s}", .{title});
|
|
if (version != 0) log.info("Version: {}", .{version});
|
|
log.info("Game Code: {s}", .{code});
|
|
if (lookupMaker(maker)) |c| log.info("Maker: {s}", .{c}) else log.info("Maker Code: {s}", .{maker});
|
|
}
|
|
|
|
fn parseTitle(buf: []u8) [12]u8 {
|
|
return buf[0xA0..0xAC].*;
|
|
}
|
|
|
|
fn lookupMaker(slice: *const [2]u8) ?[]const u8 {
|
|
const id = @as(u16, slice[1]) << 8 | @as(u16, slice[0]);
|
|
return switch (id) {
|
|
0x3130 => "Nintendo",
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
inline fn isLarge(self: *const Self) bool {
|
|
return self.buf.len > 0x100_0000;
|
|
}
|
|
|
|
pub fn deinit(self: Self) void {
|
|
self.alloc.free(self.buf);
|
|
self.backup.deinit();
|
|
}
|
|
|
|
pub fn read(self: *const Self, comptime T: type, address: u32) T {
|
|
const addr = address & 0x1FF_FFFF;
|
|
|
|
if (self.backup.kind == .Eeprom) {
|
|
if (self.isLarge()) {
|
|
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
|
|
// * Backup type is EEPROM
|
|
// * Large ROM (Size is greater than 16MB)
|
|
if (addr > 0x1FF_FEFF)
|
|
return self.backup.eeprom.read();
|
|
} else {
|
|
// Addresses 0x0D00_0000 to 0x0DFF_FFFF are reserved for EEPROM accesses if
|
|
// * Backup type is EEPROM
|
|
// * Small ROM (less than 16MB)
|
|
if (@truncate(u8, address >> 24) == 0x0D)
|
|
return self.backup.eeprom.read();
|
|
}
|
|
}
|
|
|
|
return switch (T) {
|
|
u32 => (@as(T, self.get(addr + 3)) << 24) | (@as(T, self.get(addr + 2)) << 16) | (@as(T, self.get(addr + 1)) << 8) | (@as(T, self.get(addr))),
|
|
u16 => (@as(T, self.get(addr + 1)) << 8) | @as(T, self.get(addr)),
|
|
u8 => self.get(addr),
|
|
else => @compileError("GamePak: Unsupported read width"),
|
|
};
|
|
}
|
|
|
|
pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value: T) void {
|
|
const addr = address & 0x1FF_FFFF;
|
|
|
|
if (self.backup.kind == .Eeprom) {
|
|
const bit = @truncate(u1, value);
|
|
|
|
if (self.isLarge()) {
|
|
// Addresses 0x1FF_FF00 to 0x1FF_FFFF are reserved from EEPROM accesses if
|
|
// * Backup type is EEPROM
|
|
// * Large ROM (Size is greater than 16MB)
|
|
if (addr > 0x1FF_FEFF)
|
|
return self.backup.eeprom.write(word_count, &self.backup.buf, bit);
|
|
} else {
|
|
// Addresses 0x0D00_0000 to 0x0DFF_FFFF are reserved for EEPROM accesses if
|
|
// * Backup type is EEPROM
|
|
// * Small ROM (less than 16MB)
|
|
if (@truncate(u8, address >> 24) == 0x0D)
|
|
return self.backup.eeprom.write(word_count, &self.backup.buf, bit);
|
|
}
|
|
}
|
|
|
|
switch (T) {
|
|
u32 => switch (address) {
|
|
0x0800_00C4 => log.err("Wrote {} 0x{X:} to I/O Port Data and Direction", .{ T, value }),
|
|
0x0800_00C6 => log.err("Wrote {} 0x{X:} to I/O Port Direction and Control", .{ T, value }),
|
|
else => {},
|
|
},
|
|
u16 => switch (address) {
|
|
0x0800_00C4 => log.err("Wrote {} 0x{X:} to I/O Port Data", .{ T, value }),
|
|
0x0800_00C6 => log.err("Wrote {} 0x{X:} to I/O Port Direction", .{ T, value }),
|
|
0x0800_00C8 => log.err("Wrote {} 0x{X:} to I/O Port Control", .{ T, value }),
|
|
else => {},
|
|
},
|
|
u8 => log.warn("Wrote {} 0x{X:} to 0x{X:0>8}, Ignored.", .{ T, value, address }),
|
|
else => @compileError("GamePak: Unsupported write width"),
|
|
}
|
|
}
|
|
|
|
fn get(self: *const Self, i: u32) u8 {
|
|
@setRuntimeSafety(false);
|
|
if (i < self.buf.len) return self.buf[i];
|
|
|
|
const lhs = i >> 1 & 0xFFFF;
|
|
return @truncate(u8, lhs >> 8 * @truncate(u5, i & 1));
|
|
}
|
|
|
|
test "OOB Access" {
|
|
const title = .{ 'H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D', '!' };
|
|
const alloc = std.testing.allocator;
|
|
const pak = Self{
|
|
.buf = &.{},
|
|
.alloc = alloc,
|
|
.title = title,
|
|
.backup = try Backup.init(alloc, .None, title, null),
|
|
};
|
|
|
|
std.debug.assert(pak.get(0) == 0x00); // 0x0000
|
|
std.debug.assert(pak.get(1) == 0x00);
|
|
|
|
std.debug.assert(pak.get(2) == 0x01); // 0x0001
|
|
std.debug.assert(pak.get(3) == 0x00);
|
|
|
|
std.debug.assert(pak.get(4) == 0x02); // 0x0002
|
|
std.debug.assert(pak.get(5) == 0x00);
|
|
}
|