| @@ -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 | |||
| @@ -0,0 +1,194 @@ | |||
| #include <ctype.h> | |||
| #include <stdint.h> | |||
| #include <stdio.h> | |||
| #include <stdlib.h> | |||
| #include <string.h> | |||
| #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; | |||
| } | |||
| @@ -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_ | |||
| @@ -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) | |||
| @@ -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 = { | |||
| @@ -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); | |||
| @@ -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); | |||
| @@ -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 | |||
| @@ -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, | |||
| @@ -0,0 +1,79 @@ | |||
| #include <stddef.h> | |||
| #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; | |||
| } | |||
| @@ -0,0 +1,37 @@ | |||
| #ifndef NESE_STATE_H_ | |||
| #define NESE_STATE_H_ | |||
| #include <stdint.h> | |||
| #include <stdio.h> | |||
| #include <stdlib.h> | |||
| 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_ | |||