diff --git a/.gitignore b/.gitignore index bdf39ee..783d0b7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ crater crater-dev tests/runner tests/asm/*.gg +*.sav diff --git a/LICENSE b/LICENSE index 1811091..4841a70 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (C) 2014-2016 Ben Kurtovic +Copyright (C) 2014-2017 Ben Kurtovic Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 554c258..b9db931 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,11 @@ Add `--fullscreen` (`-f`) to enable fullscreen mode, or `--scale ` (`-s `) 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 `.sav`, +where `` is the path to the ROM file. You can set a custom save location +with `--save ` 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. diff --git a/src/config.c b/src/config.c index eef8956..5b48fee 100644 --- a/src/config.c +++ b/src/config.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2016 Ben Kurtovic +/* Copyright (C) 2014-2017 Ben Kurtovic Released under the terms of the MIT License. See LICENSE for details. */ #include @@ -39,6 +39,9 @@ static void print_help(const char *arg1) " to show more detailed logs, including an emulator trace\n" " -s, --scale scale the game screen by an integer factor\n" " (applies to windowed mode only; defaults to 4)\n" +" -b, --save save cartridge RAM (\"battery save\") to the given file\n" +" (defaults to .sav)\n" +" -n, --no-save disable saving cartridge RAM entirely\n" " -a, --assemble []\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") } diff --git a/src/config.h b/src/config.h index f7514ff..e266948 100644 --- a/src/config.h +++ b/src/config.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2016 Ben Kurtovic +/* Copyright (C) 2014-2017 Ben Kurtovic 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 */ diff --git a/src/emulator.c b/src/emulator.c index deff1d5..f2c04eb 100644 --- a/src/emulator.c +++ b/src/emulator.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2016 Ben Kurtovic +/* Copyright (C) 2014-2017 Ben Kurtovic Released under the terms of the MIT License. See LICENSE for details. */ #include @@ -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); } diff --git a/src/gamegear.c b/src/gamegear.c index 8278adc..bf7436a 100644 --- a/src/gamegear.c +++ b/src/gamegear.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2016 Ben Kurtovic +/* Copyright (C) 2014-2017 Ben Kurtovic Released under the terms of the MIT License. See LICENSE for details. */ #include @@ -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 diff --git a/src/gamegear.h b/src/gamegear.h index 9d85e31..a8c71a4 100644 --- a/src/gamegear.h +++ b/src/gamegear.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2016 Ben Kurtovic +/* Copyright (C) 2014-2017 Ben Kurtovic 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*); diff --git a/src/mmu.c b/src/mmu.c index dc17869..377c0e4 100644 --- a/src/mmu.c +++ b/src/mmu.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2016 Ben Kurtovic +/* Copyright (C) 2014-2017 Ben Kurtovic Released under the terms of the MIT License. See LICENSE for details. */ #include @@ -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; diff --git a/src/mmu.h b/src/mmu.h index 816af96..ea482b2 100644 --- a/src/mmu.h +++ b/src/mmu.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2015 Ben Kurtovic +/* Copyright (C) 2014-2017 Ben Kurtovic Released under the terms of the MIT License. See LICENSE for details. */ #pragma once @@ -7,6 +7,8 @@ #include #include +#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); diff --git a/src/save.c b/src/save.c new file mode 100644 index 0000000..019779b --- /dev/null +++ b/src/save.c @@ -0,0 +1,239 @@ +/* Copyright (C) 2014-2017 Ben Kurtovic + Released under the terms of the MIT License. See LICENSE for details. */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/src/save.h b/src/save.h new file mode 100644 index 0000000..00d6959 --- /dev/null +++ b/src/save.h @@ -0,0 +1,29 @@ +/* Copyright (C) 2014-2017 Ben Kurtovic + Released under the terms of the MIT License. See LICENSE for details. */ + +#pragma once + +#include +#include + +#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*);