diff --git a/Makefile b/Makefile index dc65291..847ee2a 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/src/cart.c b/src/cart.c index 4f912fa..faadd3e 100644 --- a/src/cart.c +++ b/src/cart.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; +} diff --git a/src/cart.h b/src/cart.h index eaa61c5..2bd31b4 100644 --- a/src/cart.h +++ b/src/cart.h @@ -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_ diff --git a/src/menu.h b/src/menu.h new file mode 100644 index 0000000..e828fa0 --- /dev/null +++ b/src/menu.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_ diff --git a/src/nese.c b/src/nese.c index 0069d15..f1059db 100644 --- a/src/nese.c +++ b/src/nese.c @@ -1,6 +1,6 @@ #include -#include #include +#include #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; } diff --git a/src/render.h b/src/render.h index c74222a..bca3a36 100644 --- a/src/render.h +++ b/src/render.h @@ -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_ diff --git a/src/sdl_menu.c b/src/sdl_menu.c new file mode 100644 index 0000000..fb998c1 --- /dev/null +++ b/src/sdl_menu.c @@ -0,0 +1,226 @@ +#include +#include + +#include + +#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); +} diff --git a/src/sdl_overlay.c b/src/sdl_overlay.c index 00416f2..75db898 100644 --- a/src/sdl_overlay.c +++ b/src/sdl_overlay.c @@ -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); } } } diff --git a/src/sdl_overlay.h b/src/sdl_overlay.h index 594c835..4a25219 100644 --- a/src/sdl_overlay.h +++ b/src/sdl_overlay.h @@ -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_ diff --git a/src/sdl_render.c b/src/sdl_render.c index 48efbb5..a2a7545 100644 --- a/src/sdl_render.c +++ b/src/sdl_render.c @@ -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, };