diff --git a/Makefile b/Makefile index 6e51bcb..80f3888 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,7 @@ SRC_SRCS_1 += cart.c mapper.c SRC_SRCS_1 += apu.c audio.c SRC_SRCS_1 += file.c save.c SRC_SRCS_1 += overlay.c menu.c +SRC_SRCS_1 += state.c ini.c SRC_SRCS_1 += sdl_render.c sdl_input.c sdl_audio.c sdl_timer.c SRC_SRCS_1 += sdl_overlay.c diff --git a/src/ini.c b/src/ini.c new file mode 100644 index 0000000..58cd8d4 --- /dev/null +++ b/src/ini.c @@ -0,0 +1,194 @@ +#include +#include +#include +#include +#include + +#include "ini.h" + + +static inline int stracmp(const char* zstr, + const char* lstr, size_t len) { + int diff = 0; + while (0 == diff && *zstr && len > 0) { + --len; + diff = (*zstr - *lstr); + ++zstr; + ++lstr; + } + if (0 == diff) { + if (0 == len && !*zstr) diff = *zstr; + else if (0 != len && *zstr) diff = *lstr; + } + return diff; +} + +static const ini_datum* find_name(const ini_datum* schema, + const char* name, int len, + const ini_data_type type) { + if (0 == stracmp(schema->name, name, len)) { + return schema; + } + for (int i = 0; i < schema->count; ++i) { + if ( ( ini_none == type || + type == schema->data[i].type) && + 0 == stracmp(schema->data[i].name, name, len)) { + return &schema->data[i]; + } + } + return NULL; +} + + +int write_ini_file(FILE* file, const ini_datum* schema, + const void* data) { + int status = 0; + const void* ptr = (data + schema->offset); + + if (ini_section == schema->type) { + fprintf(file, "[%s]\n", schema->name); + for (int i = 0; i < schema->count; ++i) { + write_ini_file(file, &schema->data[i], ptr); + } + + } else if (ini_comment == schema->type) { + fprintf(file, "; %s", *(char**)ptr); + + } else { + fprintf(file, "%s = ", schema->name); + + if (ini_string == schema->type) { + fprintf(file, "\"%s\"", *(char**)ptr); + + } else if (ini_integer == schema->type) { + fprintf(file, "%d", *(uint32_t*)ptr); + + } else if (ini_flag == schema->type) { + fprintf(file, "%d", !!( *(uint32_t*)ptr & + (1 << schema->shift))); + } + } + fputc('\n', file); + + return status; +} + +static inline const char* first_char(const char* str) { + while (*str && isspace(*str)) ++str; + return str; +} + +static inline const char* first_space(const char* str) { + while (*str && !isspace(*str)) ++str; + return str; +} + +static inline const char* end_key(const char* str) { + while (*str && '=' != *str && !isspace(*str)) ++str; + return str; +} + +static inline const char* last_char(const char* str) { + int len = strlen(str); + const char* end = str + len - 1; + while (end > str && isspace(*end)) --end; + return end; +} + +static inline char* parse_string(const char* str) { + const char* last = last_char(str); + if ('"' == str[0] && '"' == *last) { + return strndup(&str[1], last - str - 1); + } + return strndup(str, last - str + 1); +} + +static inline int parse_key_value(const char* key_start, + const ini_datum* section, + void* data) { + const char* key_end = end_key(key_start + 1); + if (NULL == key_end) return -1; + + const ini_datum* def = find_name(section, key_start, + (key_end - key_start), + ini_none); + if (NULL == def || def->type <= ini_section) return -1; + + const char* equal = first_char(key_end); + if (NULL == equal || '=' != *equal) return -1; + + const char* val = first_char(equal + 1); + if (NULL == val) return -1; + + const char* ptr = data + def->offset; + if (ini_string == def->type) { + *(char**)ptr = parse_string(val); + + } else if ( ini_integer == def->type || + ini_flag == def->type) { + int intval = 0; + if (0 >= sscanf(val, "%d", &intval)) return -1; + if (ini_integer == def->type) { + *(int32_t*)ptr = intval; + } else { + int32_t mask = (1 << def->shift); + if (intval) *(uint32_t*)ptr |= mask; + else *(uint32_t*)ptr &= ~mask; + } + } + + return 0; +} + +int read_ini_file(FILE* file, const ini_datum* schema, + void* data) { + int status = 0; + const ini_datum* section = NULL; + const ini_datum* subsection = NULL; + void* ptr = data; + char* line = NULL; + size_t sz_line = 0; + + while (0 == status && 0 <= getline(&line, &sz_line, file)) { + const char* str = first_char(line); + if ('[' == str[0]) { + const char* start = &str[1]; + const char* end = strchr(start, ']'); + if (NULL != end) { + ptr = data; + int len = (end - start); + if ('.' == start[0]) { + if (NULL != section) { + subsection = find_name( + section, start, len, ini_section + ); + ptr += section->offset; + if (NULL != subsection) { + ptr += subsection->offset; + } + } + } else { + section = find_name( + schema, start, len, ini_section + ); + subsection = section; + if (NULL != section) ptr += section->offset; + } + } + + } else if (';' == str[0]) { + // Ignore comments + + } else if (isalnum(str[0])) { + // Key-value + // Ignoring return value: + // - Unknown sections or keys are ignored + // - Just ignore malformed files, I guess + parse_key_value(str, subsection, ptr); + } + } + + free(line); + + return status; +} diff --git a/src/ini.h b/src/ini.h new file mode 100644 index 0000000..99057f9 --- /dev/null +++ b/src/ini.h @@ -0,0 +1,44 @@ +#ifndef NESE_INI_H_ +#define NESE_INI_H_ + +#define DBG_LOG(...) fprintf(stderr, __VA_ARGS__) +#define ERR_LOG(...) DBG_LOG(__VA_ARGS__) +#define INI_ERR(...) ERR_LOG(__VA_ARGS__) + + +typedef enum { + ini_invalid = -1, + ini_none = 0, + ini_comment, + ini_section, + ini_string, + ini_integer, + ini_flag, +} ini_data_type; + +typedef struct ini_datum { + ini_data_type type; + int offset; // offset into associated struct + char* name; // key, section, or comment + union { + char* string; // string, comment + struct { + int32_t value; // int, flag + uint32_t shift; // flag + }; + struct { + int count; + struct ini_datum* data; // section + }; + }; +} ini_datum; + + +int write_ini_file(FILE* file, const ini_datum* schema, + const void* data); + +int read_ini_file(FILE* file, const ini_datum* schema, + void* data); + + +#endif // NESE_INI_H_ diff --git a/src/input.h b/src/input.h index 4f88331..82add1a 100644 --- a/src/input.h +++ b/src/input.h @@ -16,6 +16,8 @@ typedef enum { input_Result_Save, input_Result_Load, input_Result_Cancel, + input_Result_View, + input_Result_Refresh, } nes_Input_Result; #define nes_controller_num_buttons (8U) diff --git a/src/menu.c b/src/menu.c index b6142ce..3def83c 100644 --- a/src/menu.c +++ b/src/menu.c @@ -187,7 +187,8 @@ static int run_menu(menu_state* state, const file_list* files, int buttons = wait_for_input(input, &sys->input); int special = (buttons >> 8); - if (input_Result_Quit == special) { + if ( input_Result_Quit == special || + input_Result_Refresh == special) { status = special; } else if ( input_Result_Menu == special || @@ -250,8 +251,12 @@ char* run_main_menu(menu_state* state, nes_Renderer* rend, if (current >= 0) menu.cursor = current; } - int status = run_menu(&menu, &files, 20, + // Don't let window refreshes interrupt us. + int status = input_Result_Refresh; + while (input_Result_Refresh == status) { + status = run_menu(&menu, &files, 20, rend, input, sys); + } if (input_Result_Quit == status) { cart_filename = (char*)-1; @@ -283,6 +288,7 @@ int run_game_menu(menu_state* state, nes_Renderer* rend, "Restore", "Reset", "Select ROM", + "Toggle Fullscreen", "Exit", }; static int choices[] = { @@ -291,6 +297,7 @@ int run_game_menu(menu_state* state, nes_Renderer* rend, input_Result_Load, input_Result_Reset, input_Result_Menu, + input_Result_View, input_Result_Quit, }; static const file_list options = { diff --git a/src/nese.c b/src/nese.c index 7f28c81..43d3338 100644 --- a/src/nese.c +++ b/src/nese.c @@ -10,6 +10,7 @@ #include "mapper.h" #include "save.h" #include "menu.h" +#include "state.h" #define audio_freq (44100U) @@ -106,16 +107,6 @@ static int loadsave_tick(loadsave_state* loadsave) { return action; } -typedef struct { - char* filename; - FILE* file; -} cart_info; - -static void cleanup_cart_info(cart_info* cart) { - if (cart->file) fclose(cart->file); - free(cart->filename); -} - static int select_rom(menu_state* menu, nes_Renderer* rend, nes_Input_Reader* input, nes* sys, cart_info* cur_cart) { @@ -162,7 +153,7 @@ static int select_rom(menu_state* menu, nes_Renderer* rend, save_sram(&sys->cart, cur_cart->filename); nes_cart_done(&sys->cart); - cleanup_cart_info(cur_cart); + cart_info_done(cur_cart); sys->cart = new_cart; *cur_cart = cart; @@ -173,18 +164,35 @@ static int select_rom(menu_state* menu, nes_Renderer* rend, return status; } -static int do_game_menu(menu_state* state, nes_Renderer* rend, +static int do_game_menu(menu_state* menu, nes_Renderer* rend, nes_Input_Reader* input, nes* sys, - cart_info* cart) { + nese_State* state) { int status = 0; menu_state rom_menu = {0}; while (1) { - status = run_game_menu(state, rend, input, sys); + status = run_game_menu(menu, rend, input, sys); + + if ( input_Result_View == status || + input_Result_Refresh == status) { + if (input_Result_View == status) { + state->flags ^= (1 << State_Bit_Fullscreen); + } else { + // We need to do this to flush both buffers + nes_draw_last_frame(rend, 1); + nes_draw_done(rend); + } + // We call this both times since it does both + // the toggle and the recalculation. + nes_render_fullscreen( + rend, + state->flags & (1 << State_Bit_Fullscreen) + ); + continue; - if (input_Result_Menu == status) { + } else if (input_Result_Menu == status) { status = select_rom(&rom_menu, rend, input, - sys, cart); + sys, &state->cart); if (input_Result_Cancel == status) { status = input_Result_OK; @@ -202,9 +210,18 @@ static int do_game_menu(menu_state* state, nes_Renderer* rend, int main(int argc, char* argv[]) { int status = 0; + nese_State state = {0}; + load_prefs_filename(&state, "nese.prefs"); + nes_Renderer* rend = &sdl_renderer; if (status == 0) { status = nes_render_init(rend); + if (0 == status) { + nes_render_fullscreen( + rend, + (state.flags & (1 << State_Bit_Fullscreen)) + ); + } } nes_Input_Reader* input = &sdl_input; @@ -222,18 +239,24 @@ int main(int argc, char* argv[]) { status = nes_init(&sys, audio_freq); } - cart_info cart = {0}; if (0 == status && argc > 1) { - cart.filename = strdup(argv[1]); + cart_info cart = { + .filename = strdup(argv[1]), + }; cart.file = nes_load_cart(&sys.cart, cart.filename); if (NULL != cart.file) { + cart_info_done(&state.cart); + state.cart = cart; nes_setup_cart(&sys); + } else { + cart_info_done(&cart); } } // If we didn't launch with a file, run the loader - if (0 == status && NULL == cart.file) { - status = select_rom(NULL, rend, input, &sys, &cart); + if (0 == status && NULL == state.cart.file) { + status = select_rom(NULL, rend, input, + &sys, &state.cart); } if (status == 0) { @@ -261,9 +284,9 @@ int main(int argc, char* argv[]) { // Load/Save Operations int action = loadsave_tick(&loadsave); if (input_Result_Load == action) { - load_state(&sys, cart.filename); + load_state(&sys, state.cart.filename); } else if (input_Result_Save == action) { - save_state(&sys, cart.filename); + save_state(&sys, state.cart.filename); } // Sleep to catch up to master clock @@ -291,7 +314,7 @@ int main(int argc, char* argv[]) { if (input_Result_Menu == status) { status = do_game_menu( &game_menu, rend, - input, &sys, &cart + input, &sys, &state ); if ( input_Result_Load == status || input_Result_Save == status) { @@ -344,11 +367,13 @@ int main(int argc, char* argv[]) { status == 0 ? "OK" : "Halted"); // Failure might mean there's nothing to save - save_sram(&sys.cart, cart.filename); + save_sram(&sys.cart, state.cart.filename); } + save_prefs_filename(&state, "nese.prefs"); + nese_state_done(&state); + nes_done(&sys); - cleanup_cart_info(&cart); nes_audio_done(audio); nes_input_done(input); diff --git a/src/render.h b/src/render.h index 60eba80..c216e01 100644 --- a/src/render.h +++ b/src/render.h @@ -9,6 +9,7 @@ typedef struct nes_Renderer_t { int (*init)(struct nes_Renderer_t*); void (*done)(struct nes_Renderer_t*); int (*render)(struct nes_Renderer_t*, nes_ppu*); + void (*fullscreen)(struct nes_Renderer_t*, int enable); void (*draw_last_frame)(struct nes_Renderer_t*, int dim); void (*draw_text)(struct nes_Renderer_t*, const char*, int x, int y, uint32_t color); @@ -31,6 +32,11 @@ static inline int nes_render(nes_Renderer* rend, nes_ppu* ppu) { return rend->render(rend, ppu); } +static inline void nes_render_fullscreen(nes_Renderer* rend, + int enable) { + rend->fullscreen(rend, enable); +} + static inline void nes_draw_last_frame(nes_Renderer* rend, int dim) { rend->draw_last_frame(rend, dim); diff --git a/src/sdl_input.c b/src/sdl_input.c index 0ad8b12..d53da17 100644 --- a/src/sdl_input.c +++ b/src/sdl_input.c @@ -98,6 +98,12 @@ static int sdl_input_update(nes_Input_Reader* reader, if (SDL_QUIT == event.type) { status = input_Result_Quit; + } else if (SDL_WINDOWEVENT == event.type) { + if ( SDL_WINDOWEVENT_EXPOSED == + event.window.event) { + status = input_Result_Refresh; + } + } else if ( ( SDL_KEYDOWN == event.type || SDL_KEYUP == event.type) && 0 == event.key.repeat diff --git a/src/sdl_render.c b/src/sdl_render.c index 276f510..c6db849 100644 --- a/src/sdl_render.c +++ b/src/sdl_render.c @@ -77,6 +77,9 @@ static int sdl_render_init(nes_Renderer* rend) { data->win_w = (nes_ppu_scan_w * scale); data->win_h = (nes_ppu_scan_h * scale); + data->view.w = data->win_w; + data->view.h = data->win_h; + data->window = SDL_CreateWindow( "NESe", SDL_WINDOWPOS_UNDEFINED, @@ -87,28 +90,6 @@ static int sdl_render_init(nes_Renderer* rend) { fprintf(stderr, "SDL: Failed to create window\n"); SDL_Quit(); status = -1; - } else { - // TODO: Hide behind flag - SDL_SetWindowFullscreen(data->window, - SDL_WINDOW_FULLSCREEN); - SDL_GetWindowSize(data->window, - &data->win_w, &data->win_h); - - // Determine the viewport within the screen - - int w = data->win_w; - int h = data->win_h; - - if ((w * nes_ppu_scan_h) > (h * nes_ppu_scan_w)) { - w = (h * nes_ppu_scan_w) / nes_ppu_scan_h; - } else { - h = (w * nes_ppu_scan_h) / nes_ppu_scan_w; - } - - data->view.x = (data->win_w - w) / 2; - data->view.y = (data->win_h - h) / 2; - data->view.w = w; - data->view.h = h; } } @@ -209,6 +190,33 @@ static void sdl_render_done(nes_Renderer* rend) { SDL_Quit(); } +static void sdl_render_fullscreen(nes_Renderer* rend, int on) { + sdl_render_data* data = (sdl_render_data*)rend->data; + + SDL_SetWindowFullscreen(data->window, + on ? SDL_WINDOW_FULLSCREEN : 0); + SDL_GetWindowSize(data->window, + &data->win_w, &data->win_h); + + // Determine the viewport within the screen + + int w = data->win_w; + int h = data->win_h; + + if ((w * nes_ppu_scan_h) > (h * nes_ppu_scan_w)) { + w = (h * nes_ppu_scan_w) / nes_ppu_scan_h; + } else { + h = (w * nes_ppu_scan_h) / nes_ppu_scan_w; + } + + data->view.x = (data->win_w - w) / 2; + data->view.y = (data->win_h - h) / 2; + data->view.w = w; + data->view.h = h; + + SDL_RenderSetClipRect(data->renderer, &data->view); +} + static inline void render_sprite_line( const nes_ppu* ppu, int index, int y, const uint8_t* pal, uint8_t* dst, int start, int end, const uint8_t* back) { @@ -644,6 +652,7 @@ nes_Renderer sdl_renderer = { .init = sdl_render_init, .done = sdl_render_done, .render = sdl_render, + .fullscreen = sdl_render_fullscreen, .draw_last_frame = sdl_redraw_frame, .draw_text = sdl_draw_text, .text_size = sdl_text_size, diff --git a/src/state.c b/src/state.c new file mode 100644 index 0000000..283c7bd --- /dev/null +++ b/src/state.c @@ -0,0 +1,79 @@ +#include + +#include "state.h" +#include "ini.h" + + +void cart_info_done(cart_info* cart) { + if (cart->file) fclose(cart->file); + free(cart->filename); +} + + +void nese_state_done(nese_State* state) { + cart_info_done(&state->cart); +} + + +static const ini_datum prefs_schema = { + .type = ini_section, + .name = "nese", + .count = 2, + .data = (ini_datum[]){ + { + .type = ini_section, + .name = ".cart", + .offset = offsetof(nese_State, cart), + .count = 1, + .data = (ini_datum[]){ + { + .type = ini_string, + .name = "filename", + .offset = offsetof(cart_info, filename), + }, + } + }, { + .type = ini_section, + .name = ".flags", + .offset = offsetof(nese_State, flags), + .count = 1, + .data = (ini_datum[]){ + { + .type = ini_flag, + .name = "fullscreen", + .shift = State_Bit_Fullscreen, + }, + } + }, + }, +}; + +int save_prefs_file(const nese_State* state, FILE* file) { + return write_ini_file(file, &prefs_schema, state); +} + +int load_prefs_file(nese_State* state, FILE* file) { + return read_ini_file(file, &prefs_schema, state); +} + +int save_prefs_filename(const nese_State* state, + const char* filename) { + int status = -1; + FILE* file = fopen(filename, "w"); + if (NULL != file) { + status = write_ini_file(file, &prefs_schema, state); + fclose(file); + } + return status; +} + +int load_prefs_filename(nese_State* state, + const char* filename) { + int status = -1; + FILE* file = fopen(filename, "r"); + if (NULL != file) { + status = read_ini_file(file, &prefs_schema, state); + fclose(file); + } + return status; +} diff --git a/src/state.h b/src/state.h new file mode 100644 index 0000000..118736c --- /dev/null +++ b/src/state.h @@ -0,0 +1,37 @@ +#ifndef NESE_STATE_H_ +#define NESE_STATE_H_ + +#include +#include +#include + + +typedef struct { + char* filename; + FILE* file; +} cart_info; + +void cart_info_done(cart_info*); + + +typedef enum { + State_Bit_Fullscreen = 0, +} nese_State_Flags; + +typedef struct { + cart_info cart; + nese_State_Flags flags; +} nese_State; + + +void nese_state_done(nese_State*); + + +int load_prefs_file(nese_State*, FILE*); +int save_prefs_file(const nese_State*, FILE*); + +int load_prefs_filename(nese_State*, const char*); +int save_prefs_filename(const nese_State*, const char*); + + +#endif // NESE_STATE_H_