| @@ -47,6 +47,7 @@ SRC_SRCS_1 += apu.c audio.c | |||
| SRC_SRCS_1 += file.c save.c | |||
| SRC_SRCS_1 += sdl_render.c sdl_input.c sdl_audio.c sdl_timer.c | |||
| SRC_SRCS_1 += overlay.c sdl_overlay.c | |||
| SRC_SRCS_1 += sdl_menu.c | |||
| PLAT_SRCS_1 = filemap.c | |||
| @@ -2,6 +2,7 @@ | |||
| #include "filemap.h" | |||
| #include "ines.h" | |||
| #include "mapper.h" | |||
| #include "save.h" | |||
| int nes_cart_init_mem(nes_cart* cart, void* mem, int len) { | |||
| @@ -107,3 +108,30 @@ int nes_cart_init_file(nes_cart* cart, FILE* file) { | |||
| return status; | |||
| } | |||
| FILE* nes_load_cart(nes_cart* cart, const char* cart_filename) { | |||
| int status = 0; | |||
| FILE* cart_file = fopen(cart_filename, "rb"); | |||
| if (NULL == cart_file) { | |||
| status = -1; | |||
| fprintf(stderr, "Could not open %s\n", cart_filename); | |||
| } | |||
| if (status == 0) { | |||
| status = nes_cart_init_file(cart, cart_file); | |||
| } | |||
| if (status == 0) { | |||
| // Failure might mean there's nothing to load | |||
| load_sram(cart, cart_filename); | |||
| } | |||
| if (status != 0 && NULL != cart_file) { | |||
| fclose(cart_file); | |||
| cart_file = NULL; | |||
| } | |||
| return cart_file; | |||
| } | |||
| @@ -28,5 +28,7 @@ int nes_cart_init_file(nes_cart*, FILE* file); | |||
| int nes_cart_init_mem(nes_cart*, void*, int len); | |||
| void nes_cart_done(nes_cart*); | |||
| FILE* nes_load_cart(nes_cart* cart, const char* cart_filename); | |||
| #endif // NES_CART_H_ | |||
| @@ -0,0 +1,27 @@ | |||
| #ifndef NESE_MENU_H_ | |||
| #define NESE_MENU_H_ | |||
| #include "nes.h" | |||
| #include "render.h" | |||
| #include "input.h" | |||
| typedef struct { | |||
| int cursor; | |||
| int top; | |||
| } menu_state; | |||
| // Returns filename of selected ROM | |||
| char* run_main_menu(menu_state*, nes_Renderer*, | |||
| nes_Input_Reader*, nes*); | |||
| // TODO: What does this return? | |||
| int run_game_menu(menu_state*, nes_Renderer*, | |||
| nes_Input_Reader*, nes*); | |||
| int modal_popup(const char* message, | |||
| nes_Renderer*, nes_Input_Reader*, nes*); | |||
| #endif // NESE_MENU_H_ | |||
| @@ -1,6 +1,6 @@ | |||
| #include <inttypes.h> | |||
| #include <limits.h> | |||
| #include <stdlib.h> | |||
| #include <string.h> | |||
| #include "nes.h" | |||
| #include "timer.h" | |||
| @@ -9,6 +9,7 @@ | |||
| #include "audio.h" | |||
| #include "mapper.h" | |||
| #include "save.h" | |||
| #include "menu.h" | |||
| #define audio_freq (44100U) | |||
| @@ -87,29 +88,6 @@ int loadsave_tick(loadsave_state* loadsave) { | |||
| int main(int argc, char* argv[]) { | |||
| int status = 0; | |||
| FILE* cart_file = NULL; | |||
| const char* cart_filename = NULL; | |||
| if (argc > 1) { | |||
| cart_filename = argv[1]; | |||
| cart_file = fopen(argv[1], "rb"); | |||
| if (NULL == cart_file) { | |||
| status = -1; | |||
| fprintf(stderr, "Could not open %s\n", argv[1]); | |||
| } | |||
| } else { | |||
| status = -1; | |||
| fprintf(stderr, "Missing cartridge file\n"); | |||
| } | |||
| if (status == 0) { | |||
| status = nes_cart_init_file(&sys.cart, cart_file); | |||
| } | |||
| if (status == 0) { | |||
| // Failure might mean there's nothing to load | |||
| load_sram(&sys.cart, cart_filename); | |||
| } | |||
| nes_Renderer* rend = &sdl_renderer; | |||
| if (status == 0) { | |||
| status = nes_render_init(rend); | |||
| @@ -125,6 +103,43 @@ int main(int argc, char* argv[]) { | |||
| status = nes_audio_init(audio, audio_freq); | |||
| } | |||
| char* cart_filename = NULL; | |||
| FILE* cart_file = NULL; | |||
| if (0 == status && argc > 1) { | |||
| cart_filename = strdup(argv[1]); | |||
| cart_file = nes_load_cart(&sys.cart, argv[1]); | |||
| } | |||
| menu_state menu = {0}; | |||
| while (0 == status && NULL == cart_file) { | |||
| // Display a load failure message? | |||
| if (NULL != cart_filename) { | |||
| char message[1024]; | |||
| snprintf(message, sizeof(message) - 1, | |||
| "Could not load\n%s", cart_filename); | |||
| int button = modal_popup(message, rend, input, &sys); | |||
| if (input_Result_Quit == (button >> 8)) { | |||
| // Program closed inside modal | |||
| status = -1; | |||
| } | |||
| } | |||
| if (0 == status) { | |||
| // If we didn't launch with a file, run the loader | |||
| cart_filename = run_main_menu(&menu, rend, | |||
| input, &sys); | |||
| if (NULL == cart_filename) { | |||
| // This means that we dumped out of the loader | |||
| status = -1; | |||
| } else { | |||
| cart_file = nes_load_cart(&sys.cart, | |||
| cart_filename); | |||
| } | |||
| } | |||
| } | |||
| if (status == 0) { | |||
| status = nes_init(&sys, audio_freq); | |||
| } | |||
| @@ -227,9 +242,8 @@ int main(int argc, char* argv[]) { | |||
| save_sram(&sys.cart, cart_filename); | |||
| } | |||
| if (cart_file != NULL) { | |||
| fclose(cart_file); | |||
| } | |||
| if (NULL != cart_file) fclose(cart_file); | |||
| free(cart_filename); | |||
| return status; | |||
| } | |||
| @@ -9,6 +9,12 @@ 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 (*draw_last_frame)(struct nes_Renderer_t*); | |||
| void (*draw_text)(struct nes_Renderer_t*, const char*, int x, int y); | |||
| void (*text_size)(struct nes_Renderer_t*, const char*, int* w, int* h); | |||
| void (*draw_done)(struct nes_Renderer_t*); | |||
| Overlay overlay; | |||
| void* data; | |||
| } nes_Renderer; | |||
| @@ -25,5 +31,24 @@ static inline int nes_render(nes_Renderer* rend, nes_ppu* ppu) { | |||
| return rend->render(rend, ppu); | |||
| } | |||
| static inline void nes_draw_last_frame(nes_Renderer* rend) { | |||
| rend->draw_last_frame(rend); | |||
| } | |||
| static inline void nes_text_size(nes_Renderer* rend, | |||
| const char* str, | |||
| int* w, int* h) { | |||
| rend->text_size(rend, str, w, h); | |||
| } | |||
| static inline void nes_draw_text(nes_Renderer* rend, | |||
| const char* str, int x, int y) { | |||
| rend->draw_text(rend, str, x, y); | |||
| } | |||
| static inline void nes_draw_done(nes_Renderer* rend) { | |||
| rend->draw_done(rend); | |||
| } | |||
| #endif // NES_RENDER_H_ | |||
| @@ -0,0 +1,226 @@ | |||
| #include <stdlib.h> | |||
| #include <string.h> | |||
| #include <dirent.h> | |||
| #include "menu.h" | |||
| #include "file.h" | |||
| #include "timer.h" | |||
| static int get_input(nes_Input_Reader* reader, | |||
| nes_input* input, int *last) { | |||
| int status = nes_input_update(reader, input); | |||
| int new_buttons = input->controllers[0].buttons; | |||
| if (0 == status) { | |||
| status = (~*last & new_buttons); | |||
| } else { | |||
| status = (status << 8); | |||
| } | |||
| *last = new_buttons; | |||
| return status; | |||
| } | |||
| static int wait_for_input(nes_Input_Reader* reader, | |||
| nes_input* input) { | |||
| int buttons = input->controllers[0].buttons; | |||
| int status = 0; | |||
| for ( ; | |||
| 0 == status; | |||
| status = get_input(reader, input, &buttons) ) { | |||
| time_sleep(US_PER_S / 60); | |||
| } | |||
| return status; | |||
| } | |||
| static int count_files(DIR* dir) { | |||
| int count = 0; | |||
| struct dirent* de = NULL; | |||
| while (NULL != (de = readdir(dir))) { | |||
| if ('.' != de->d_name[0]) ++count; | |||
| } | |||
| rewinddir(dir); | |||
| return count; | |||
| } | |||
| typedef struct { | |||
| int count; | |||
| char** files; | |||
| } file_list; | |||
| int cmp_files(const void* _a, const void* _b) { | |||
| const char* a = *(const char**)_a; | |||
| const char* b = *(const char**)_b; | |||
| int diff = 0; | |||
| for (char ca = 1, cb = 1; ca && cb && 0 == diff; ++a, ++b) { | |||
| // Cut extensions; replace underscore with space | |||
| ca = (*a == '_' ? ' ' : (*a == '.' ? '\0' : *a)); | |||
| cb = (*b == '_' ? ' ' : (*b == '.' ? '\0' : *b)); | |||
| diff = (ca - cb); | |||
| } | |||
| return diff; | |||
| } | |||
| static void make_file_list(DIR* dir, file_list* files) { | |||
| files->count = count_files(dir); | |||
| files->files = calloc(files->count, sizeof(char*)); | |||
| struct dirent* de = NULL; | |||
| int i_file = 0; | |||
| while (NULL != (de = readdir(dir))) { | |||
| if ('.' != de->d_name[0]) { | |||
| files->files[i_file] = strdup(de->d_name); | |||
| ++i_file; | |||
| } | |||
| } | |||
| qsort(files->files, files->count, sizeof(char*), cmp_files); | |||
| } | |||
| static void free_file_list(file_list* files) { | |||
| for (int i = 0; i < files->count; ++i) { | |||
| free(files->files[i]); | |||
| } | |||
| free(files->files); | |||
| } | |||
| static void fix_filename(char* dst, int n, const char* src) { | |||
| for ( int i = 0; | |||
| i < n && *src && '.' != *src; | |||
| ++i, ++dst, ++src) { | |||
| *dst = ('_' == *src ? ' ' : *src); | |||
| } | |||
| *dst = '\0'; | |||
| } | |||
| static inline int n_visible(void) { | |||
| return (((nes_ppu_render_h - 30) - 1) / 11) - 1; | |||
| } | |||
| static void show_menu(const menu_state* menu, | |||
| nes_Renderer* rend, file_list* files) { | |||
| nes_draw_last_frame(rend); | |||
| int bottom = menu->top + n_visible(); | |||
| for ( int n = menu->top, y = 10; | |||
| n < files->count && n <= bottom; | |||
| ++n, y += 11 ) { | |||
| char filename[100]; | |||
| fix_filename(filename, sizeof(filename) - 1, | |||
| files->files[n]); | |||
| nes_draw_text( | |||
| rend, | |||
| ( (menu->top == n && 0 != menu->top) || | |||
| (bottom == n && files->count - 1 > bottom) ) ? | |||
| "..." : filename, | |||
| 20, y); | |||
| if (menu->cursor == n) nes_draw_text(rend, ">", 10, y); | |||
| } | |||
| nes_draw_done(rend); | |||
| } | |||
| char* run_main_menu(menu_state* state, nes_Renderer* rend, | |||
| nes_Input_Reader* input, nes* sys) { | |||
| char* cart_filename = NULL; | |||
| DIR* dir = opendir("rom"); | |||
| if (NULL == dir) { | |||
| nes_draw_last_frame(rend); | |||
| nes_draw_text(rend, "No ROMS found!", 10, 10); | |||
| nes_draw_text(rend, "Press any key to exit", 10, 21); | |||
| nes_draw_done(rend); | |||
| wait_for_input(input, &sys->input); | |||
| } else { | |||
| file_list files = {0}; | |||
| make_file_list(dir, &files); | |||
| closedir(dir); | |||
| menu_state menu = {0}; | |||
| if (NULL != state) { | |||
| menu = *state; | |||
| if (menu.cursor < 0) { | |||
| menu.cursor = 0; | |||
| } else if (menu.cursor >= files.count) { | |||
| menu.cursor = files.count - 1; | |||
| } | |||
| } | |||
| while (1) { | |||
| // Scrolling (do this first to ensure menu is valid) | |||
| const int visible = n_visible(); | |||
| menu.top = menu.cursor - (visible / 2); | |||
| if (menu.top <= 0) { | |||
| // We use <= so we don't readjust the top from 0 | |||
| menu.top = 0; | |||
| } else if (menu.top + visible >= files.count) { | |||
| menu.top = (files.count - 1) - visible; | |||
| } | |||
| show_menu(&menu, rend, &files); | |||
| int buttons = wait_for_input(input, &sys->input); | |||
| int special = (buttons >> 8); | |||
| if ( input_Result_Quit == special || | |||
| (buttons & (1 << Button_B))) { | |||
| // Cancel | |||
| menu.cursor = -1; | |||
| break; | |||
| } else if (buttons & ( (1 << Button_A) | | |||
| (1 << Button_Start) )) { | |||
| // Select | |||
| break; | |||
| } else if (buttons & (1 << Button_Up)) { | |||
| if (menu.cursor > 0) --menu.cursor; | |||
| } else if (buttons & ( (1 << Button_Down) | | |||
| (1 << Button_Select) )) { | |||
| if (menu.cursor < (files.count - 1)) { | |||
| ++menu.cursor; | |||
| } | |||
| } | |||
| } | |||
| // Selection has been made (or cancelled) | |||
| if (menu.cursor >= 0 && menu.cursor < files.count) { | |||
| char filename[1024]; | |||
| snprintf(filename, sizeof(filename) - 1, | |||
| "%s/%s", "rom", files.files[menu.cursor]); | |||
| cart_filename = strdup(filename); | |||
| } | |||
| free_file_list(&files); | |||
| if (NULL != state) { | |||
| *state = menu; | |||
| if (menu.cursor < 0) state->cursor = 0; | |||
| } | |||
| } | |||
| return cart_filename; | |||
| } | |||
| int modal_popup(const char* message, nes_Renderer* rend, | |||
| nes_Input_Reader* input, nes* sys) { | |||
| int w = 0; | |||
| int h = 0; | |||
| nes_text_size(rend, message, &w, &h); | |||
| int x = (nes_ppu_render_w - w) / 2; | |||
| int y = (nes_ppu_render_h - h) / 2; | |||
| nes_draw_last_frame(rend); | |||
| nes_draw_text(rend, message, x, y); | |||
| nes_draw_done(rend); | |||
| return wait_for_input(input, &sys->input); | |||
| } | |||
| @@ -86,6 +86,8 @@ void sdl_overlay_font_done(sdl_overlay_font* font) { | |||
| #define overlay_start_x (char_w / 2) | |||
| #define overlay_start_y (char_h / 2) | |||
| #define space_width ((char_w / 2) - 1) | |||
| static inline int char_index(char c) { | |||
| if (c >= 'a' && c <= 'z') c += ('A' - 'a'); | |||
| if (c > ' ' && c < 'a') return (c - ' '); | |||
| @@ -93,28 +95,54 @@ static inline int char_index(char c) { | |||
| return 0; | |||
| } | |||
| static void render_string(SDL_Renderer* rend, int ox, int oy, | |||
| int sx, int sy, | |||
| sdl_overlay_font* font, | |||
| const char* string) { | |||
| void measure_string(sdl_overlay_font* font, const char* string, | |||
| int* w, int* h) { | |||
| *h = char_h; | |||
| int max_w = 1; | |||
| int cur_w = 1; | |||
| for (const char* c = string; *c; ++c) { | |||
| if (*c == '\n') { | |||
| if (cur_w > max_w) max_w = cur_w; | |||
| cur_w = 1; | |||
| *h += char_h; | |||
| } else if (*c == ' ') { | |||
| cur_w += space_width; | |||
| } else { | |||
| int index = char_index(*c); | |||
| int cw = font->charbits[index * charbit_size]; | |||
| cur_w += (cw - 1); | |||
| } | |||
| } | |||
| if (cur_w > max_w) max_w = cur_w; | |||
| *w = max_w; | |||
| } | |||
| void render_string(SDL_Renderer* rend, | |||
| int ox, int oy, int sx, int sy, | |||
| sdl_overlay_font* font, const char* string) { | |||
| int x = ox; | |||
| int y = oy; | |||
| for (const char* c = string; *c; ++c) { | |||
| if (*c == ' ') { | |||
| ox += ((char_w / 2) - 1); | |||
| if (*c == '\n') { | |||
| x = ox; | |||
| y += (char_h + 1); | |||
| } else if (*c == ' ') { | |||
| x += space_width; | |||
| } else { | |||
| int index = char_index(*c); | |||
| int cw = font->charbits[index * charbit_size]; | |||
| SDL_Texture* texture = font->textures[index]; | |||
| SDL_Rect rect = { | |||
| .x = ox * sx, | |||
| .y = oy * sy, | |||
| .x = x * sx, | |||
| .y = y * sy, | |||
| .w = cw * sx, | |||
| .h = char_h * sy, | |||
| }; | |||
| SDL_RenderCopy(rend, texture, NULL, &rect); | |||
| ox += (cw - 1); | |||
| x += (cw - 1); | |||
| } | |||
| } | |||
| } | |||
| @@ -24,4 +24,12 @@ int sdl_overlay_frame(Overlay*, sdl_overlay_font*, SDL_Renderer*, | |||
| int sx, int sy); | |||
| void measure_string(sdl_overlay_font* font, const char* string, | |||
| int* w, int* h); | |||
| void render_string(SDL_Renderer* rend, | |||
| int ox, int oy, int sx, int sy, | |||
| sdl_overlay_font* font, const char* string); | |||
| #endif // NESE_SDL_OVERLAY_ | |||
| @@ -148,6 +148,7 @@ static int sdl_render_init(nes_Renderer* rend) { | |||
| } else { | |||
| SDL_LockTextureToSurface(data->texture, NULL, | |||
| &data->target); | |||
| SDL_FillRect(data->target, NULL, 0x556677FF); | |||
| } | |||
| } | |||
| @@ -559,8 +560,39 @@ static int sdl_render(nes_Renderer* rend, nes_ppu* ppu) { | |||
| } | |||
| static void sdl_redraw_frame(nes_Renderer* rend) { | |||
| sdl_render_data* data = (sdl_render_data*)rend->data; | |||
| SDL_UnlockTexture(data->texture); | |||
| SDL_RenderCopy(data->renderer, data->texture, NULL, NULL); | |||
| SDL_LockTextureToSurface(data->texture, NULL, &data->target); | |||
| } | |||
| static void sdl_draw_present(nes_Renderer* rend) { | |||
| sdl_render_data* data = (sdl_render_data*)rend->data; | |||
| SDL_RenderPresent(data->renderer); | |||
| } | |||
| static void sdl_draw_text(nes_Renderer* rend, | |||
| const char* str, int x, int y) { | |||
| sdl_render_data* data = (sdl_render_data*)rend->data; | |||
| render_string(data->renderer, x, y, | |||
| data->win_w / nes_ppu_render_w, | |||
| data->win_h / nes_ppu_render_h, | |||
| &data->font, str); | |||
| } | |||
| static void sdl_text_size(nes_Renderer* rend, | |||
| const char* str, int* w, int* h) { | |||
| sdl_render_data* data = (sdl_render_data*)rend->data; | |||
| measure_string(&data->font, str, w, h); | |||
| } | |||
| nes_Renderer sdl_renderer = { | |||
| .init = sdl_render_init, | |||
| .done = sdl_render_done, | |||
| .render = sdl_render, | |||
| .draw_last_frame = sdl_redraw_frame, | |||
| .draw_text = sdl_draw_text, | |||
| .text_size = sdl_text_size, | |||
| .draw_done = sdl_draw_present, | |||
| }; | |||