@@ -5,3 +5,4 @@ crater | |||||
crater-dev | crater-dev | ||||
tests/runner | tests/runner | ||||
tests/asm/*.gg | tests/asm/*.gg | ||||
*.sav |
@@ -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 | Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
of this software and associated documentation files (the "Software"), to deal | of this software and associated documentation files (the "Software"), to deal | ||||
@@ -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 | (`-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). | 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 | Add `--debug` (`-g`) to show logging information while running. Pass it twice | ||||
(`-gg`) to show more detailed logs, including an emulator trace. | (`-gg`) to show more detailed logs, including an emulator trace. | ||||
@@ -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. */ | Released under the terms of the MIT License. See LICENSE for details. */ | ||||
#include <dirent.h> | #include <dirent.h> | ||||
@@ -39,6 +39,9 @@ static void print_help(const char *arg1) | |||||
" to show more detailed logs, including an emulator trace\n" | " to show more detailed logs, including an emulator trace\n" | ||||
" -s, --scale <n> scale the game screen by an integer factor\n" | " -s, --scale <n> scale the game screen by an integer factor\n" | ||||
" (applies to windowed mode only; defaults to 4)\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" | " -a, --assemble <in> [<out>]\n" | ||||
" convert z80 assembly source code into a binary file that\n" | " convert z80 assembly source code into a binary file that\n" | ||||
" can be run by crater\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; | 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 (args->paths_read == 1) { | ||||
/* If this is the second path given, it can only be an output file for | /* 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 | 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; | 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")) { | else if (arg_check(arg, "g", "debug")) { | ||||
config->debug++; | config->debug++; | ||||
} | } | ||||
@@ -287,17 +300,19 @@ static int parse_args(Config *config, int argc, char *argv[]) | |||||
return retval; | 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; | 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; | 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") | ERROR("refusing to overwrite the assembler input file; pass -r to override") | ||||
return false; | 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; | return true; | ||||
} | } | ||||
@@ -389,9 +410,11 @@ int config_create(Config** config_ptr, int argc, char* argv[]) | |||||
config->fullscreen = false; | config->fullscreen = false; | ||||
config->scale = 0; | config->scale = 0; | ||||
config->rom_path = NULL; | config->rom_path = NULL; | ||||
config->sav_path = NULL; | |||||
config->src_path = NULL; | config->src_path = NULL; | ||||
config->dst_path = NULL; | config->dst_path = NULL; | ||||
config->overwrite = false; | config->overwrite = false; | ||||
config->no_saving = false; | |||||
retval = parse_args(config, argc, argv); | retval = parse_args(config, argc, argv); | ||||
if (retval == CONFIG_OK && !(sanity_check(config) && set_defaults(config))) | 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) | void config_destroy(Config *config) | ||||
{ | { | ||||
free(config->rom_path); | free(config->rom_path); | ||||
free(config->sav_path); | |||||
free(config->src_path); | free(config->src_path); | ||||
free(config->dst_path); | free(config->dst_path); | ||||
free(config); | free(config); | ||||
@@ -429,7 +453,9 @@ void config_dump_args(const Config* config) | |||||
DEBUG("- fullscreen: %s", config->fullscreen ? "true" : "false") | DEBUG("- fullscreen: %s", config->fullscreen ? "true" : "false") | ||||
DEBUG("- scale: %d", config->scale) | DEBUG("- scale: %d", config->scale) | ||||
DEBUG("- rom_path: %s", config->rom_path ? config->rom_path : "(null)") | 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("- src_path: %s", config->src_path ? config->src_path : "(null)") | ||||
DEBUG("- dst_path: %s", config->dst_path ? config->dst_path : "(null)") | DEBUG("- dst_path: %s", config->dst_path ? config->dst_path : "(null)") | ||||
DEBUG("- overwrite: %s", config->overwrite ? "true" : "false") | DEBUG("- overwrite: %s", config->overwrite ? "true" : "false") | ||||
DEBUG("- no_saving: %s", config->no_saving ? "true" : "false") | |||||
} | } |
@@ -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. */ | Released under the terms of the MIT License. See LICENSE for details. */ | ||||
#pragma once | #pragma once | ||||
@@ -27,9 +27,11 @@ typedef struct { | |||||
bool fullscreen; | bool fullscreen; | ||||
unsigned scale; | unsigned scale; | ||||
char *rom_path; | char *rom_path; | ||||
char *sav_path; | |||||
char *src_path; | char *src_path; | ||||
char *dst_path; | char *dst_path; | ||||
bool overwrite; | bool overwrite; | ||||
bool no_saving; | |||||
} Config; | } Config; | ||||
/* Functions */ | /* Functions */ | ||||
@@ -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. */ | Released under the terms of the MIT License. See LICENSE for details. */ | ||||
#include <signal.h> | #include <signal.h> | ||||
@@ -8,6 +8,7 @@ | |||||
#include "emulator.h" | #include "emulator.h" | ||||
#include "gamegear.h" | #include "gamegear.h" | ||||
#include "logging.h" | #include "logging.h" | ||||
#include "save.h" | |||||
#include "util.h" | #include "util.h" | ||||
typedef struct { | typedef struct { | ||||
@@ -176,13 +177,21 @@ static void cleanup_graphics() | |||||
*/ | */ | ||||
void emulate(ROM *rom, Config *config) | 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(); | emu.gg = gamegear_create(); | ||||
signal(SIGINT, handle_sigint); | signal(SIGINT, handle_sigint); | ||||
setup_graphics(config->fullscreen, config->scale); | setup_graphics(config->fullscreen, config->scale); | ||||
gamegear_attach_callback(emu.gg, frame_callback); | gamegear_attach_callback(emu.gg, frame_callback); | ||||
gamegear_attach_display(emu.gg, emu.pixels); | 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); | gamegear_simulate(emu.gg); | ||||
@@ -197,4 +206,6 @@ void emulate(ROM *rom, Config *config) | |||||
signal(SIGINT, SIG_DFL); | signal(SIGINT, SIG_DFL); | ||||
gamegear_destroy(emu.gg); | gamegear_destroy(emu.gg); | ||||
emu.gg = NULL; | emu.gg = NULL; | ||||
if (!config->no_saving) | |||||
save_free(&save); | |||||
} | } |
@@ -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. */ | Released under the terms of the MIT License. See LICENSE for details. */ | ||||
#include <stdlib.h> | #include <stdlib.h> | ||||
@@ -59,7 +59,7 @@ void gamegear_destroy(GameGear *gg) | |||||
until another ROM is loaded or the GameGear is destroyed. Calling this | until another ROM is loaded or the GameGear is destroyed. Calling this | ||||
function while the GameGear is powered on has no effect. | 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) | if (gg->powered) | ||||
return; | 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. | Update the GameGear's button/joystick state. | ||||
'state' should be true when the button is pressed, and false when it is | 'state' should be true when the button is pressed, and false when it is | ||||
@@ -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. */ | Released under the terms of the MIT License. See LICENSE for details. */ | ||||
#pragma once | #pragma once | ||||
@@ -10,6 +10,7 @@ | |||||
#include "mmu.h" | #include "mmu.h" | ||||
#include "psg.h" | #include "psg.h" | ||||
#include "rom.h" | #include "rom.h" | ||||
#include "save.h" | |||||
#include "z80.h" | #include "z80.h" | ||||
#define GG_SCREEN_WIDTH 160 | #define GG_SCREEN_WIDTH 160 | ||||
@@ -48,7 +49,8 @@ typedef enum { | |||||
GameGear* gamegear_create(); | GameGear* gamegear_create(); | ||||
void gamegear_destroy(GameGear*); | 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_simulate(GameGear*); | ||||
void gamegear_input(GameGear*, GGButton, bool); | void gamegear_input(GameGear*, GGButton, bool); | ||||
void gamegear_power_off(GameGear*); | void gamegear_power_off(GameGear*); | ||||
@@ -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. */ | Released under the terms of the MIT License. See LICENSE for details. */ | ||||
#include <stdlib.h> | #include <stdlib.h> | ||||
@@ -15,9 +15,11 @@ | |||||
void mmu_init(MMU *mmu) | void mmu_init(MMU *mmu) | ||||
{ | { | ||||
mmu->system_ram = cr_malloc(sizeof(uint8_t) * MMU_SYSTEM_RAM_SIZE); | 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_mapped = false; | ||||
mmu->cart_ram_external = false; | |||||
mmu->save = NULL; | |||||
for (size_t slot = 0; slot < MMU_NUM_SLOTS; slot++) | for (size_t slot = 0; slot < MMU_NUM_SLOTS; slot++) | ||||
mmu->rom_slots[slot] = NULL; | mmu->rom_slots[slot] = NULL; | ||||
@@ -32,7 +34,8 @@ void mmu_init(MMU *mmu) | |||||
void mmu_free(MMU *mmu) | void mmu_free(MMU *mmu) | ||||
{ | { | ||||
free(mmu->system_ram); | 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. | Map the given RAM slot to the given ROM bank. | ||||
*/ | */ | ||||
static inline void map_rom_slot(MMU *mmu, size_t slot, size_t 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); | map_rom_slot(mmu, slot, slot); | ||||
memset(mmu->system_ram, 0xFF, MMU_SYSTEM_RAM_SIZE); | 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) | else if (!slot2_enable && mmu->cart_ram_mapped) | ||||
TRACE("MMU disabling cart RAM in memory slot 2") | 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 = | mmu->cart_ram_slot = | ||||
bank_select ? (mmu->cart_ram + 0x4000) : mmu->cart_ram; | bank_select ? (mmu->cart_ram + 0x4000) : mmu->cart_ram; | ||||
mmu->cart_ram_mapped = slot2_enable; | mmu->cart_ram_mapped = slot2_enable; | ||||
@@ -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. */ | Released under the terms of the MIT License. See LICENSE for details. */ | ||||
#pragma once | #pragma once | ||||
@@ -7,6 +7,8 @@ | |||||
#include <stddef.h> | #include <stddef.h> | ||||
#include <stdint.h> | #include <stdint.h> | ||||
#include "save.h" | |||||
#define MMU_NUM_SLOTS (3) | #define MMU_NUM_SLOTS (3) | ||||
#define MMU_NUM_ROM_BANKS (64) | #define MMU_NUM_ROM_BANKS (64) | ||||
#define MMU_ROM_BANK_SIZE (16 * 1024) | #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_slots[MMU_NUM_SLOTS]; | ||||
const uint8_t *rom_banks[MMU_NUM_ROM_BANKS]; | const uint8_t *rom_banks[MMU_NUM_ROM_BANKS]; | ||||
uint8_t *cart_ram_slot; | uint8_t *cart_ram_slot; | ||||
bool cart_ram_mapped; | |||||
bool cart_ram_mapped, cart_ram_external; | |||||
Save *save; | |||||
} MMU; | } MMU; | ||||
/* Functions */ | /* Functions */ | ||||
@@ -29,6 +32,7 @@ typedef struct { | |||||
void mmu_init(MMU*); | void mmu_init(MMU*); | ||||
void mmu_free(MMU*); | void mmu_free(MMU*); | ||||
void mmu_load_rom(MMU*, const uint8_t*, size_t); | void mmu_load_rom(MMU*, const uint8_t*, size_t); | ||||
void mmu_load_save(MMU*, Save*); | |||||
void mmu_power(MMU*); | void mmu_power(MMU*); | ||||
uint8_t mmu_read_byte(const MMU*, uint16_t); | uint8_t mmu_read_byte(const MMU*, uint16_t); | ||||
uint16_t mmu_read_double(const MMU*, uint16_t); | uint16_t mmu_read_double(const MMU*, uint16_t); | ||||
@@ -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; | |||||
} |
@@ -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*); |