| @@ -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*); | |||||