Browse Source

Add support for saved game files for cart RAM (fixes #3)

master
Ben Kurtovic 3 years ago
parent
commit
a0c12d019d
12 changed files with 398 additions and 28 deletions
  1. +1
    -0
      .gitignore
  2. +1
    -1
      LICENSE
  3. +5
    -0
      README.md
  4. +39
    -13
      src/config.c
  5. +3
    -1
      src/config.h
  6. +13
    -2
      src/emulator.c
  7. +15
    -2
      src/gamegear.c
  8. +4
    -2
      src/gamegear.h
  9. +43
    -5
      src/mmu.c
  10. +6
    -2
      src/mmu.h
  11. +239
    -0
      src/save.c
  12. +29
    -0
      src/save.h

+ 1
- 0
.gitignore View File

@@ -5,3 +5,4 @@ crater
crater-dev
tests/runner
tests/asm/*.gg
*.sav

+ 1
- 1
LICENSE View File

@@ -1,4 +1,4 @@
Copyright (C) 2014-2016 Ben Kurtovic <ben.kurtovic@gmail.com>
Copyright (C) 2014-2017 Ben Kurtovic <ben.kurtovic@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

+ 5
- 0
README.md View File

@@ -53,6 +53,11 @@ Add `--fullscreen` (`-f`) to enable fullscreen mode, or `--scale <n>`
(`-s <n>`) to scale the game screen by an integer factor in windowed mode (this
only sets the starting configuration; the window should be resizeable).

By default, crater will save cartridge RAM ("battery saves"; these are distinct
from save states, which are not yet supported) to a file named `<rom>.sav`,
where `<rom>` is the path to the ROM file. You can set a custom save location
with `--save <path>` or disable saving entirely with `--no-save`.

Add `--debug` (`-g`) to show logging information while running. Pass it twice
(`-gg`) to show more detailed logs, including an emulator trace.


+ 39
- 13
src/config.c View File

@@ -1,4 +1,4 @@
/* Copyright (C) 2014-2016 Ben Kurtovic <ben.kurtovic@gmail.com>
/* Copyright (C) 2014-2017 Ben Kurtovic <ben.kurtovic@gmail.com>
Released under the terms of the MIT License. See LICENSE for details. */

#include <dirent.h>
@@ -39,6 +39,9 @@ static void print_help(const char *arg1)
" to show more detailed logs, including an emulator trace\n"
" -s, --scale <n> scale the game screen by an integer factor\n"
" (applies to windowed mode only; defaults to 4)\n"
" -b, --save <path> save cartridge RAM (\"battery save\") to the given file\n"
" (defaults to <rom_path>.sav)\n"
" -n, --no-save disable saving cartridge RAM entirely\n"
" -a, --assemble <in> [<out>]\n"
" convert z80 assembly source code into a binary file that\n"
" can be run by crater\n"
@@ -179,9 +182,7 @@ static int parse_pos_arg(Config *config, Arguments *args, const char *arg)
return CONFIG_EXIT_FAILURE;
}

char *path = cr_malloc(sizeof(char) * (strlen(arg) + 1));
strcpy(path, arg);

char *path = cr_strdup(arg);
if (args->paths_read == 1) {
/* If this is the second path given, it can only be an output file for
the assembler. If the assembler is not enabled by subsequent
@@ -242,6 +243,18 @@ static int parse_opt_arg(Config *config, Arguments *args, const char *arg)
}
config->scale = scale;
}
else if (arg_check(arg, "b", "save")) {
const char *next = consume_next(args);
if (!next) {
ERROR("the save option requires an argument")
return CONFIG_EXIT_FAILURE;
}
free(config->sav_path);
config->sav_path = cr_strdup(next);
}
else if (arg_check(arg, "n", "no-save")) {
config->no_saving = true;
}
else if (arg_check(arg, "g", "debug")) {
config->debug++;
}
@@ -287,17 +300,19 @@ static int parse_args(Config *config, int argc, char *argv[])
return retval;
}

if (!config->assemble && !config->disassemble && args.paths_read >= 2) {
ERROR("too many arguments given - emulator mode accepts one ROM file")
return CONFIG_EXIT_FAILURE;
}
if (!config->assemble && !config->disassemble && args.paths_read == 0) {
char *path = get_rom_path_from_user();
if (path[0] == '\0') {
ERROR("no ROM image given")
if (!config->assemble && !config->disassemble) {
if (args.paths_read >= 2) {
ERROR("too many arguments given - emulator mode accepts one ROM file")
return CONFIG_EXIT_FAILURE;
}
config->rom_path = path;
if (args.paths_read == 0) {
char *path = get_rom_path_from_user();
if (path[0] == '\0') {
ERROR("no ROM image given")
return CONFIG_EXIT_FAILURE;
}
config->rom_path = path;
}
}

return CONFIG_OK;
@@ -367,6 +382,12 @@ static bool set_defaults(Config *config)
ERROR("refusing to overwrite the assembler input file; pass -r to override")
return false;
}
if (!assembler && !config->sav_path && !config->no_saving) {
config->sav_path = cr_malloc(sizeof(char) *
(strlen(config->rom_path) + 4));
strcpy(config->sav_path, config->rom_path);
strcat(config->sav_path, ".sav");
}
return true;
}

@@ -389,9 +410,11 @@ int config_create(Config** config_ptr, int argc, char* argv[])
config->fullscreen = false;
config->scale = 0;
config->rom_path = NULL;
config->sav_path = NULL;
config->src_path = NULL;
config->dst_path = NULL;
config->overwrite = false;
config->no_saving = false;

retval = parse_args(config, argc, argv);
if (retval == CONFIG_OK && !(sanity_check(config) && set_defaults(config)))
@@ -411,6 +434,7 @@ int config_create(Config** config_ptr, int argc, char* argv[])
void config_destroy(Config *config)
{
free(config->rom_path);
free(config->sav_path);
free(config->src_path);
free(config->dst_path);
free(config);
@@ -429,7 +453,9 @@ void config_dump_args(const Config* config)
DEBUG("- fullscreen: %s", config->fullscreen ? "true" : "false")
DEBUG("- scale: %d", config->scale)
DEBUG("- rom_path: %s", config->rom_path ? config->rom_path : "(null)")
DEBUG("- sav_path: %s", config->sav_path ? config->sav_path : "(null)")
DEBUG("- src_path: %s", config->src_path ? config->src_path : "(null)")
DEBUG("- dst_path: %s", config->dst_path ? config->dst_path : "(null)")
DEBUG("- overwrite: %s", config->overwrite ? "true" : "false")
DEBUG("- no_saving: %s", config->no_saving ? "true" : "false")
}

+ 3
- 1
src/config.h View File

@@ -1,4 +1,4 @@
/* Copyright (C) 2014-2016 Ben Kurtovic <ben.kurtovic@gmail.com>
/* Copyright (C) 2014-2017 Ben Kurtovic <ben.kurtovic@gmail.com>
Released under the terms of the MIT License. See LICENSE for details. */

#pragma once
@@ -27,9 +27,11 @@ typedef struct {
bool fullscreen;
unsigned scale;
char *rom_path;
char *sav_path;
char *src_path;
char *dst_path;
bool overwrite;
bool no_saving;
} Config;

/* Functions */

+ 13
- 2
src/emulator.c View File

@@ -1,4 +1,4 @@
/* Copyright (C) 2014-2016 Ben Kurtovic <ben.kurtovic@gmail.com>
/* Copyright (C) 2014-2017 Ben Kurtovic <ben.kurtovic@gmail.com>
Released under the terms of the MIT License. See LICENSE for details. */

#include <signal.h>
@@ -8,6 +8,7 @@
#include "emulator.h"
#include "gamegear.h"
#include "logging.h"
#include "save.h"
#include "util.h"

typedef struct {
@@ -176,13 +177,21 @@ static void cleanup_graphics()
*/
void emulate(ROM *rom, Config *config)
{
Save save;
if (!config->no_saving) {
if (!save_init(&save, config->sav_path, rom))
return;
}

emu.gg = gamegear_create();
signal(SIGINT, handle_sigint);
setup_graphics(config->fullscreen, config->scale);

gamegear_attach_callback(emu.gg, frame_callback);
gamegear_attach_display(emu.gg, emu.pixels);
gamegear_load(emu.gg, rom);
gamegear_load_rom(emu.gg, rom);
if (!config->no_saving)
gamegear_load_save(emu.gg, &save);

gamegear_simulate(emu.gg);

@@ -197,4 +206,6 @@ void emulate(ROM *rom, Config *config)
signal(SIGINT, SIG_DFL);
gamegear_destroy(emu.gg);
emu.gg = NULL;
if (!config->no_saving)
save_free(&save);
}

+ 15
- 2
src/gamegear.c View File

@@ -1,4 +1,4 @@
/* Copyright (C) 2014-2016 Ben Kurtovic <ben.kurtovic@gmail.com>
/* Copyright (C) 2014-2017 Ben Kurtovic <ben.kurtovic@gmail.com>
Released under the terms of the MIT License. See LICENSE for details. */

#include <stdlib.h>
@@ -59,7 +59,7 @@ void gamegear_destroy(GameGear *gg)
until another ROM is loaded or the GameGear is destroyed. Calling this
function while the GameGear is powered on has no effect.
*/
void gamegear_load(GameGear *gg, const ROM *rom)
void gamegear_load_rom(GameGear *gg, const ROM *rom)
{
if (gg->powered)
return;
@@ -68,6 +68,19 @@ void gamegear_load(GameGear *gg, const ROM *rom)
}

/*
Load a game save into the GameGear object.

The same rules with gamegear_load_rom() apply here.
*/
void gamegear_load_save(GameGear *gg, Save *save)
{
if (gg->powered)
return;

mmu_load_save(&gg->mmu, save);
}

/*
Update the GameGear's button/joystick state.

'state' should be true when the button is pressed, and false when it is

+ 4
- 2
src/gamegear.h View File

@@ -1,4 +1,4 @@
/* Copyright (C) 2014-2016 Ben Kurtovic <ben.kurtovic@gmail.com>
/* Copyright (C) 2014-2017 Ben Kurtovic <ben.kurtovic@gmail.com>
Released under the terms of the MIT License. See LICENSE for details. */

#pragma once
@@ -10,6 +10,7 @@
#include "mmu.h"
#include "psg.h"
#include "rom.h"
#include "save.h"
#include "z80.h"

#define GG_SCREEN_WIDTH 160
@@ -48,7 +49,8 @@ typedef enum {

GameGear* gamegear_create();
void gamegear_destroy(GameGear*);
void gamegear_load(GameGear*, const ROM*);
void gamegear_load_rom(GameGear*, const ROM*);
void gamegear_load_save(GameGear*, Save*);
void gamegear_simulate(GameGear*);
void gamegear_input(GameGear*, GGButton, bool);
void gamegear_power_off(GameGear*);

+ 43
- 5
src/mmu.c View File

@@ -1,4 +1,4 @@
/* Copyright (C) 2014-2016 Ben Kurtovic <ben.kurtovic@gmail.com>
/* Copyright (C) 2014-2017 Ben Kurtovic <ben.kurtovic@gmail.com>
Released under the terms of the MIT License. See LICENSE for details. */

#include <stdlib.h>
@@ -15,9 +15,11 @@
void mmu_init(MMU *mmu)
{
mmu->system_ram = cr_malloc(sizeof(uint8_t) * MMU_SYSTEM_RAM_SIZE);
mmu->cart_ram = cr_malloc(sizeof(uint8_t) * MMU_CART_RAM_SIZE);
mmu->cart_ram_slot = mmu->cart_ram;
mmu->cart_ram = NULL;
mmu->cart_ram_slot = NULL;
mmu->cart_ram_mapped = false;
mmu->cart_ram_external = false;
mmu->save = NULL;

for (size_t slot = 0; slot < MMU_NUM_SLOTS; slot++)
mmu->rom_slots[slot] = NULL;
@@ -32,7 +34,8 @@ void mmu_init(MMU *mmu)
void mmu_free(MMU *mmu)
{
free(mmu->system_ram);
free(mmu->cart_ram);
if (!mmu->cart_ram_external)
free(mmu->cart_ram);
}

/*
@@ -86,6 +89,30 @@ void mmu_load_rom(MMU *mmu, const uint8_t *data, size_t size)
}

/*
Load a save into the MMU.

If the save has valid cartridge RAM from a previous game, we will load that
into the MMU. Otherwise, we will defer creating fresh cartridge RAM until
it is requested by the system.

This function can be called while the system is running, but it may have
strange consequences. It will replace any existing cart RAM with the save
RAM, which will likely confuse the program.
*/
void mmu_load_save(MMU *mmu, Save *save)
{
mmu->save = save;
if (save_has_cart_ram(save)) {
if (mmu->cart_ram && !mmu->cart_ram_external)
free(mmu->cart_ram);

DEBUG("MMU loading cartridge RAM from external save")
mmu->cart_ram = save_get_cart_ram(save);
mmu->cart_ram_external = true;
}
}

/*
Map the given RAM slot to the given ROM bank.
*/
static inline void map_rom_slot(MMU *mmu, size_t slot, size_t bank)
@@ -106,7 +133,6 @@ void mmu_power(MMU *mmu)
map_rom_slot(mmu, slot, slot);

memset(mmu->system_ram, 0xFF, MMU_SYSTEM_RAM_SIZE);
memset(mmu->cart_ram, 0xFF, MMU_CART_RAM_SIZE);
}

/*
@@ -177,6 +203,18 @@ static void write_ram_control_register(MMU *mmu, uint8_t value)
else if (!slot2_enable && mmu->cart_ram_mapped)
TRACE("MMU disabling cart RAM in memory slot 2")

if (slot2_enable && !mmu->cart_ram) {
DEBUG("MMU initializing cartridge RAM (fresh battery save)")
if (mmu->save && save_init_cart_ram(mmu->save)) {
mmu->cart_ram = save_get_cart_ram(mmu->save);
mmu->cart_ram_external = true;
} else {
mmu->cart_ram = cr_malloc(sizeof(uint8_t) * MMU_CART_RAM_SIZE);
mmu->cart_ram_external = false;
}
memset(mmu->cart_ram, 0xFF, MMU_CART_RAM_SIZE);
}

mmu->cart_ram_slot =
bank_select ? (mmu->cart_ram + 0x4000) : mmu->cart_ram;
mmu->cart_ram_mapped = slot2_enable;

+ 6
- 2
src/mmu.h View File

@@ -1,4 +1,4 @@
/* Copyright (C) 2014-2015 Ben Kurtovic <ben.kurtovic@gmail.com>
/* Copyright (C) 2014-2017 Ben Kurtovic <ben.kurtovic@gmail.com>
Released under the terms of the MIT License. See LICENSE for details. */

#pragma once
@@ -7,6 +7,8 @@
#include <stddef.h>
#include <stdint.h>

#include "save.h"

#define MMU_NUM_SLOTS (3)
#define MMU_NUM_ROM_BANKS (64)
#define MMU_ROM_BANK_SIZE (16 * 1024)
@@ -21,7 +23,8 @@ typedef struct {
const uint8_t *rom_slots[MMU_NUM_SLOTS];
const uint8_t *rom_banks[MMU_NUM_ROM_BANKS];
uint8_t *cart_ram_slot;
bool cart_ram_mapped;
bool cart_ram_mapped, cart_ram_external;
Save *save;
} MMU;

/* Functions */
@@ -29,6 +32,7 @@ typedef struct {
void mmu_init(MMU*);
void mmu_free(MMU*);
void mmu_load_rom(MMU*, const uint8_t*, size_t);
void mmu_load_save(MMU*, Save*);
void mmu_power(MMU*);
uint8_t mmu_read_byte(const MMU*, uint16_t);
uint16_t mmu_read_double(const MMU*, uint16_t);

+ 239
- 0
src/save.c View File

@@ -0,0 +1,239 @@
/* Copyright (C) 2014-2017 Ben Kurtovic <ben.kurtovic@gmail.com>
Released under the terms of the MIT License. See LICENSE for details. */

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "save.h"
#include "mmu.h"
#include "util.h"

static const char *MAGIC = "CRATER GAMEGEAR SAVE FILE\n";
static size_t HEADER_LEN = 64;

/*
Log an error while trying to load the save file.
*/
static void log_error(const char *action, const char *path, const char *reason)
{
ERROR("couldn't %s save file '%s': %s", action, path, reason)
}

/*
Log an error in a standard library function.
*/
static void log_stdlib_error(const char *action, const char *path,
const char *func)
{
ERROR("couldn't %s save file '%s': %s(): %s",
action, path, func, strerror(errno))
}

/*
Parse the header of a save file, and return whether it is valid.

If the load succeeds and off is not NULL, it will be set to the address
of the first byte of non-header data.
*/
static bool parse_save_header(void *ptr, size_t size, size_t *off,
const ROM *rom, const char *path)
{
const char *str = ptr;
if (size < HEADER_LEN) {
log_error("load", path, "too short");
return false;
}
if (strncmp(str, MAGIC, strlen(MAGIC))) {
log_error("load", path,
"invalid header (was this save created by crater?)");
return false;
}
str += strlen(MAGIC);

int version;
uint32_t prodcode;
uint16_t checksum;
if (sscanf(str, "%d:%06d:0x%04hX\n", &version, &prodcode, &checksum) < 3) {
log_error("load", path, "invalid header (failed to parse)");
return false;
}
if (version != 1) {
log_error("load", path, "unknown or unsupported save file version");
return false;
}
if (prodcode != rom->product_code || checksum != rom->expected_checksum) {
log_error("load", path, "save was created for a different ROM");
return false;
}
if (size != HEADER_LEN + MMU_CART_RAM_SIZE) {
log_error("load", path, "cart RAM size is wrong; file may be corrupt");
return false;
}

if (off)
*off = HEADER_LEN;
return true;
}

/*
Initialize a save object, which represents persistent RAM.

The given path will be used to store save data. If it already exists,
it will be loaded here. The return value indicates whether the load was
successful; if it is false, then a file exists in the save location but is
not valid, and this save should not be used. save_free() does not need to
be called in this case.
*/
bool save_init(Save *save, const char *path, const ROM *rom)
{
save->path = NULL;
save->rom = rom;
save->map = NULL;
save->mapsize = 0;
save->cart_ram_offset = 0;
save->has_cart_ram = false;

if (!path)
return true;

int fd = open(path, O_RDWR);
if (fd < 0) {
if (errno == ENOENT) {
save->path = cr_strdup(path);
return true;
}
log_stdlib_error("load", path, "open");
return false;
}

struct stat s;
if (fstat(fd, &s) < 0) {
close(fd);
log_stdlib_error("load", path, "fstat");
return false;
}

size_t size = s.st_size;
void *ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
if (ptr == MAP_FAILED) {
log_stdlib_error("load", path, "mmap");
return false;
}

size_t offset;
if (!parse_save_header(ptr, size, &offset, rom, path))
return false;

save->map = ptr;
save->mapsize = size;
save->cart_ram_offset = offset;
save->has_cart_ram = true;
return true;
}

/*
Free memory previously allocated by the save.

Will flush the save data to disk if necessary.
*/
void save_free(Save *save)
{
free(save->path);
if (save->map) {
msync(save->map, save->mapsize, MS_SYNC);
munmap(save->map, save->mapsize);
}
}

/*
Return whether the save has existing cartridge RAM.
*/
bool save_has_cart_ram(const Save *save)
{
return save->has_cart_ram;
}

/*
Return a readable and writable pointer to existing cartridge RAM.
*/
uint8_t* save_get_cart_ram(Save *save)
{
if (!save->has_cart_ram)
return NULL;
return ((uint8_t*) save->map) + save->cart_ram_offset;
}

/*
Initialize the save file with fresh cartridge RAM as appropriate.

If the save file is already loaded, return true. Otherwise, the return
value indicates whether the save file creation was successful.
*/
bool save_init_cart_ram(Save *save)
{
if (save->has_cart_ram)
return true;
if (!save->path || save->map) // This should not happen normally
return false;

DEBUG("Creating new save file at %s", save->path)

int fd = open(save->path, O_RDWR|O_CREAT|O_EXCL, 0644);
if (fd < 0) {
log_stdlib_error("create", save->path, "open");
return false;
}

// Write header
static const int VERSION = 1;
int header_len = dprintf(fd, "%s%d:%06d:0x%04hX\n", MAGIC, VERSION,
save->rom->product_code, save->rom->expected_checksum);
if (header_len < 0 || (unsigned) header_len > HEADER_LEN) {
if (header_len < 0)
log_stdlib_error("create", save->path, "dprintf");
else
log_error("create", save->path, "header was unexpectedly long");
close(fd);
unlink(save->path);
return false;
}

// Zero out space for the cartridge RAM
char buf[4096];
memset(buf, 0, sizeof(buf));
size_t rem = MMU_CART_RAM_SIZE + (HEADER_LEN - header_len);
while (rem > 0) {
ssize_t chunk = rem > sizeof(buf) ? sizeof(buf) : rem;
if (write(fd, buf, chunk) < chunk) {
log_stdlib_error("create", save->path, "write");
close(fd);
unlink(save->path);
return false;
}
rem -= chunk;
}

// Try to MMAP
size_t size = HEADER_LEN + MMU_CART_RAM_SIZE;
lseek(fd, 0, SEEK_SET);
void *ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
if (ptr == MAP_FAILED) {
log_stdlib_error("create", save->path, "mmap");
unlink(save->path);
return false;
}

save->map = ptr;
save->mapsize = size;
save->cart_ram_offset = HEADER_LEN;
save->has_cart_ram = true;
return true;
}

+ 29
- 0
src/save.h View File

@@ -0,0 +1,29 @@
/* Copyright (C) 2014-2017 Ben Kurtovic <ben.kurtovic@gmail.com>
Released under the terms of the MIT License. See LICENSE for details. */

#pragma once

#include <stdbool.h>
#include <stdint.h>

#include "rom.h"

/* Structs */

typedef struct {
char *path;
const ROM *rom;
void *map;
size_t mapsize;
size_t cart_ram_offset;
bool has_cart_ram;
} Save;

/* Functions */

bool save_init(Save*, const char*, const ROM*);
void save_free(Save*);

bool save_has_cart_ram(const Save*);
uint8_t* save_get_cart_ram(Save*);
bool save_init_cart_ram(Save*);

Loading…
Cancel
Save