| @@ -47,6 +47,7 @@ SRC_SRCS_1 += apu.c audio.c | |||||
| SRC_SRCS_1 += file.c save.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 += sdl_render.c sdl_input.c sdl_audio.c sdl_timer.c | ||||
| SRC_SRCS_1 += overlay.c sdl_overlay.c | SRC_SRCS_1 += overlay.c sdl_overlay.c | ||||
| SRC_SRCS_1 += sdl_menu.c | |||||
| PLAT_SRCS_1 = filemap.c | PLAT_SRCS_1 = filemap.c | ||||
| @@ -2,6 +2,7 @@ | |||||
| #include "filemap.h" | #include "filemap.h" | ||||
| #include "ines.h" | #include "ines.h" | ||||
| #include "mapper.h" | #include "mapper.h" | ||||
| #include "save.h" | |||||
| int nes_cart_init_mem(nes_cart* cart, void* mem, int len) { | 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; | 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); | int nes_cart_init_mem(nes_cart*, void*, int len); | ||||
| void nes_cart_done(nes_cart*); | void nes_cart_done(nes_cart*); | ||||
| FILE* nes_load_cart(nes_cart* cart, const char* cart_filename); | |||||
| #endif // NES_CART_H_ | #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 <inttypes.h> | ||||
| #include <limits.h> | |||||
| #include <stdlib.h> | #include <stdlib.h> | ||||
| #include <string.h> | |||||
| #include "nes.h" | #include "nes.h" | ||||
| #include "timer.h" | #include "timer.h" | ||||
| @@ -9,6 +9,7 @@ | |||||
| #include "audio.h" | #include "audio.h" | ||||
| #include "mapper.h" | #include "mapper.h" | ||||
| #include "save.h" | #include "save.h" | ||||
| #include "menu.h" | |||||
| #define audio_freq (44100U) | #define audio_freq (44100U) | ||||
| @@ -87,29 +88,6 @@ int loadsave_tick(loadsave_state* loadsave) { | |||||
| int main(int argc, char* argv[]) { | int main(int argc, char* argv[]) { | ||||
| int status = 0; | 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; | nes_Renderer* rend = &sdl_renderer; | ||||
| if (status == 0) { | if (status == 0) { | ||||
| status = nes_render_init(rend); | status = nes_render_init(rend); | ||||
| @@ -125,6 +103,43 @@ int main(int argc, char* argv[]) { | |||||
| status = nes_audio_init(audio, audio_freq); | 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) { | if (status == 0) { | ||||
| status = nes_init(&sys, audio_freq); | status = nes_init(&sys, audio_freq); | ||||
| } | } | ||||
| @@ -227,9 +242,8 @@ int main(int argc, char* argv[]) { | |||||
| save_sram(&sys.cart, cart_filename); | 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; | return status; | ||||
| } | } | ||||
| @@ -9,6 +9,12 @@ typedef struct nes_Renderer_t { | |||||
| int (*init)(struct nes_Renderer_t*); | int (*init)(struct nes_Renderer_t*); | ||||
| void (*done)(struct nes_Renderer_t*); | void (*done)(struct nes_Renderer_t*); | ||||
| int (*render)(struct nes_Renderer_t*, nes_ppu*); | 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; | Overlay overlay; | ||||
| void* data; | void* data; | ||||
| } nes_Renderer; | } nes_Renderer; | ||||
| @@ -25,5 +31,24 @@ static inline int nes_render(nes_Renderer* rend, nes_ppu* ppu) { | |||||
| return rend->render(rend, 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_ | #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_x (char_w / 2) | ||||
| #define overlay_start_y (char_h / 2) | #define overlay_start_y (char_h / 2) | ||||
| #define space_width ((char_w / 2) - 1) | |||||
| static inline int char_index(char c) { | static inline int char_index(char c) { | ||||
| if (c >= 'a' && c <= 'z') c += ('A' - 'a'); | if (c >= 'a' && c <= 'z') c += ('A' - 'a'); | ||||
| if (c > ' ' && c < 'a') return (c - ' '); | if (c > ' ' && c < 'a') return (c - ' '); | ||||
| @@ -93,28 +95,54 @@ static inline int char_index(char c) { | |||||
| return 0; | 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) { | 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 { | } else { | ||||
| int index = char_index(*c); | int index = char_index(*c); | ||||
| int cw = font->charbits[index * charbit_size]; | int cw = font->charbits[index * charbit_size]; | ||||
| SDL_Texture* texture = font->textures[index]; | SDL_Texture* texture = font->textures[index]; | ||||
| SDL_Rect rect = { | SDL_Rect rect = { | ||||
| .x = ox * sx, | |||||
| .y = oy * sy, | |||||
| .x = x * sx, | |||||
| .y = y * sy, | |||||
| .w = cw * sx, | .w = cw * sx, | ||||
| .h = char_h * sy, | .h = char_h * sy, | ||||
| }; | }; | ||||
| SDL_RenderCopy(rend, texture, NULL, &rect); | 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); | 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_ | #endif // NESE_SDL_OVERLAY_ | ||||
| @@ -148,6 +148,7 @@ static int sdl_render_init(nes_Renderer* rend) { | |||||
| } else { | } else { | ||||
| SDL_LockTextureToSurface(data->texture, NULL, | SDL_LockTextureToSurface(data->texture, NULL, | ||||
| &data->target); | &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 = { | nes_Renderer sdl_renderer = { | ||||
| .init = sdl_render_init, | .init = sdl_render_init, | ||||
| .done = sdl_render_done, | .done = sdl_render_done, | ||||
| .render = sdl_render, | .render = sdl_render, | ||||
| .draw_last_frame = sdl_redraw_frame, | |||||
| .draw_text = sdl_draw_text, | |||||
| .text_size = sdl_text_size, | |||||
| .draw_done = sdl_draw_present, | |||||
| }; | }; | ||||