| #include <cmath> | |
| #include <cstring> | |
| #include <cstdio> | |
| #include <cstdlib> | |
| #include <list> | |
| #include "launcher.hpp" | |
| #include "assets.hpp" | |
| #include "engine/api_private.hpp" | |
| #include "graphics/color.hpp" | |
| #include "executable.hpp" | |
| #include "metadata.hpp" | |
| #include "dialog.hpp" | |
| #include "theme.hpp" | |
| #include "credits.hpp" | |
| using namespace blit; | |
| struct PathSave { | |
| char last_path[512]; | |
| }; | |
| static const int path_save_slot = 256; | |
| static Dialog dialog; | |
| const Font launcher_font(font8x8); | |
| constexpr uint32_t qspi_flash_sector_size = 64 * 1024; | |
| static Screen current_screen = Screen::main; | |
| static bool show_fps = false; | |
| static bool sd_detected = true; | |
| static Vec2 file_list_scroll_offset(10.0f, 0.0f); | |
| static Point game_info_offset(120, 20); | |
| static Point game_actions_offset(game_info_offset.x + 128 + 8, 28); | |
| static float directory_list_scroll_offset = 0.0f; | |
| static std::vector<GameInfo> game_list; | |
| static std::list<DirectoryInfo> directory_list; | |
| static std::list<DirectoryInfo>::iterator current_directory; | |
| static SortBy file_sort = SortBy::name; | |
| static GameInfo selected_game; | |
| static BlitGameMetadata selected_game_metadata; | |
| static Surface *spritesheet; | |
| static Surface *screenshot; | |
| static AutoRepeat ar_button_up(250, 600); | |
| static AutoRepeat ar_button_down(250, 600); | |
| static AutoRepeat ar_button_left(0, 0); | |
| static AutoRepeat ar_button_right(0, 0); | |
| #ifndef PICO_BUILD | |
| static uint8_t screenshot_buf[320 * 240 * 3]; | |
| #endif | |
| static int calc_num_blocks(uint32_t size) { | |
| return (size - 1) / qspi_flash_sector_size + 1; | |
| } | |
| // insertion sort | |
| template <class Iterator, class Compare> | |
| static void insertion_sort(Iterator first, Iterator last, Compare comp) { | |
| if(last - first < 2) | |
| return; | |
| for(auto it = first + 1; it != last; ++it) { | |
| auto temp = it; | |
| while(temp != first && comp(*temp, *(temp - 1))) { | |
| std::swap(*temp, *(temp - 1)); | |
| --temp; | |
| } | |
| } | |
| } | |
| static bool parse_file_metadata(const std::string &filename, BlitGameMetadata &metadata, bool unpack_images = false) { | |
| blit::File f(filename); | |
| if(!f.is_open()) | |
| return false; | |
| uint32_t offset = 0; | |
| uint8_t buf[sizeof(BlitGameHeader)]; | |
| auto read = f.read(offset, sizeof(buf), (char *)&buf); | |
| // skip relocation data | |
| if(memcmp(buf, "RELO", 4) == 0) { | |
| uint32_t num_relocs; | |
| f.read(4, 4, (char *)&num_relocs); | |
| offset = num_relocs * 4 + 8; | |
| // re-read header | |
| read = f.read(offset, sizeof(buf), (char *)&buf); | |
| } | |
| // game header - skip to metadata | |
| if(memcmp(buf, "BLITMETA", 8) != 0) { | |
| auto &header = *(BlitGameHeader *)buf; | |
| if(read == sizeof(BlitGameHeader) && header.magic == blit_game_magic) { | |
| offset += (header.end & 0x1FFFFFF); | |
| read = f.read(offset, 10, (char *)buf); | |
| } | |
| } | |
| if(read >= 10 && memcmp(buf, "BLITMETA", 8) == 0) { | |
| // don't bother reading the whole thing if we don't want the images | |
| auto metadata_len = unpack_images ? *reinterpret_cast<uint16_t *>(buf + 8) : sizeof(RawMetadata); | |
| uint8_t metadata_buf[0xFFFF]; | |
| f.read(offset + 10, metadata_len, (char *)metadata_buf); | |
| parse_metadata(reinterpret_cast<char *>(metadata_buf), metadata_len, metadata, unpack_images); | |
| return true; | |
| } | |
| return false; | |
| } | |
| static void sort_file_list() { | |
| using Iterator = std::vector<GameInfo>::iterator; | |
| using Compare = bool(const GameInfo &, const GameInfo &); | |
| if (file_sort == SortBy::name) { | |
| // Sort by filename | |
| insertion_sort<Iterator, Compare>(game_list.begin(), game_list.end(), [](const auto &a, const auto &b) { return a.title < b.title; }); | |
| } | |
| if (file_sort == SortBy::size) { | |
| // Sort by filesize | |
| insertion_sort<Iterator, Compare>(game_list.begin(), game_list.end(), [](const auto &a, const auto &b) { return a.size < b.size; }); | |
| } | |
| } | |
| static void load_file_list(const std::string &directory) { | |
| game_list.clear(); | |
| auto files = list_files(directory, [&](auto &file) { | |
| if(file.flags & FileFlags::directory) | |
| return false; | |
| if(file.name[0] == '.') // hidden file | |
| return false; | |
| auto path = directory == "/" ? file.name : directory + "/" + file.name; | |
| auto res = api.can_launch(path.c_str()); | |
| if(res == CanLaunchResult::UnknownType) { | |
| // special case for images | |
| auto last_dot = file.name.find_last_of('.'); | |
| auto ext = last_dot == std::string::npos ? "" : file.name.substr(last_dot + 1); | |
| for(auto &c : ext) | |
| c = tolower(c); | |
| if(ext == "bmp" || ext == "blim") | |
| return true; | |
| } | |
| // will filter incompatible later | |
| return res == CanLaunchResult::Success || res == CanLaunchResult::IncompatibleBlit; | |
| }); | |
| game_list.reserve(files.size()); // worst case | |
| for(auto &file : files) { | |
| auto last_dot = file.name.find_last_of('.'); | |
| auto ext = last_dot == std::string::npos ? "" : file.name.substr(last_dot + 1); | |
| for(auto &c : ext) | |
| c = tolower(c); | |
| GameInfo game; | |
| game.title = file.name.substr(0, file.name.length() - ext.length() - 1); | |
| game.filename = directory == "/" ? file.name : directory + "/" + file.name; | |
| game.size = file.size; | |
| if(ext == "blit") { | |
| game.type = GameType::game; | |
| game.can_launch = api.can_launch(game.filename.c_str()) == CanLaunchResult::Success; | |
| // check for metadata | |
| BlitGameMetadata meta; | |
| if(parse_file_metadata(game.filename, meta)) | |
| game.title = meta.title; | |
| } else if(ext == "bmp" || ext == "blim") { | |
| game.type = GameType::screenshot; | |
| // Special case check for an installed handler for these types, ie: a sprite editor | |
| game.can_launch = api.get_type_handler_metadata && api.get_type_handler_metadata(ext.c_str()); | |
| } else { | |
| // it's launch-able so there must be a handler | |
| game.type = GameType::file; | |
| strncpy(game.ext, ext.c_str(), 5); | |
| game.ext[4] = 0; | |
| game.can_launch = true; | |
| // check for a metadata file (fall back to handler's metadata) | |
| BlitGameMetadata meta; | |
| auto meta_filename = game.filename + ".blmeta"; | |
| if(parse_file_metadata(meta_filename, meta)) | |
| game.title = meta.title; | |
| } | |
| game_list.push_back(game); | |
| } | |
| int total_items = (int)game_list.size(); | |
| if(selected_menu_item >= total_items) | |
| selected_menu_item = std::max(0, total_items - 1); | |
| // probably doesn't do anything... | |
| game_list.shrink_to_fit(); | |
| sort_file_list(); | |
| } | |
| static void load_directory_list(const std::string &directory) { | |
| directory_list.clear(); | |
| auto dir_filter = [](const FileInfo &info){ | |
| if(!(info.flags & FileFlags::directory)) | |
| return false; | |
| if(info.name.compare("System Volume Information") == 0 || info.name[0] == '.') | |
| return false; | |
| return true; | |
| }; | |
| for(auto &folder : ::list_files(directory, dir_filter)) | |
| directory_list.push_back({folder.name, 0, 0}); | |
| directory_list.sort([](const auto &a, const auto &b) { return a.name > b.name; }); | |
| directory_list.push_front({"/", 0, 0}); | |
| directory_list.push_front({"flash:", 0, 0}); | |
| // measure positions | |
| int x = 0; | |
| for(auto &dir : directory_list) { | |
| dir.x = x; | |
| dir.w = screen.measure_text(dir.name == "/" ? "ROOT" : dir.name, launcher_font).w; | |
| x += dir.w + 10; | |
| } | |
| } | |
| static void load_current_game_metadata() { | |
| bool loaded = false; | |
| if(!game_list.empty()) { | |
| selected_game = game_list[selected_menu_item]; | |
| if(selected_game.type == GameType::file) { | |
| // not a .blit - look for a metadata file | |
| auto meta_filename = selected_game.filename + ".blmeta"; | |
| if(!parse_file_metadata(meta_filename, selected_game_metadata, true)) { | |
| // fallback to handler metadata/placeholders | |
| auto handler_meta = (char *)api.get_type_handler_metadata(selected_game.ext); | |
| auto len = *reinterpret_cast<uint16_t *>(handler_meta + 8); | |
| parse_metadata(handler_meta + 10, len, selected_game_metadata, true); | |
| selected_game_metadata.description = "Launches with: " + selected_game_metadata.title; | |
| selected_game_metadata.title = selected_game.title; | |
| selected_game_metadata.author = ""; | |
| selected_game_metadata.version = ""; | |
| } | |
| loaded = true; | |
| } else | |
| loaded = parse_file_metadata(selected_game.filename, selected_game_metadata, true); | |
| } | |
| #ifndef PICO_BUILD | |
| if(selected_game.type == GameType::screenshot) { | |
| // Free any old buffers | |
| if(screenshot) { | |
| delete[] screenshot->palette; | |
| delete screenshot; | |
| screenshot = nullptr; | |
| } | |
| // Load the new screenshot | |
| screenshot = Surface::load(selected_game.filename, screenshot_buf, sizeof(screenshot_buf)); | |
| } else { | |
| // Not showing a screenshot, free the buffers | |
| if(screenshot) { | |
| delete[] screenshot->palette; | |
| delete screenshot; | |
| screenshot = nullptr; | |
| } | |
| } | |
| #endif | |
| // no valid metadata, reset | |
| if(!loaded) { | |
| selected_game_metadata.free_surfaces(); | |
| selected_game_metadata = BlitGameMetadata(); | |
| } | |
| } | |
| static bool launch_current_game() { | |
| // save last file launched | |
| PathSave save{}; | |
| strncpy(save.last_path, selected_game.filename.c_str(), sizeof(save.last_path) - 1); | |
| write_save(save, path_save_slot); | |
| if(!api.launch) | |
| return false; | |
| return api.launch(selected_game.filename.c_str()); | |
| } | |
| static void delete_current_game() { | |
| dialog.show("Confirm", "Really delete " + selected_game.title + "?", [](bool yes){ | |
| if(yes) { | |
| if(selected_game.filename.compare(0, 7, "flash:/") == 0) | |
| api.erase_game(std::stoi(selected_game.filename.substr(7)) * qspi_flash_sector_size); | |
| ::remove_file(selected_game.filename); | |
| load_file_list(current_directory->name); | |
| load_current_game_metadata(); | |
| } | |
| }); | |
| } | |
| static void init_lists() { | |
| load_directory_list("/"); | |
| current_directory = directory_list.begin(); | |
| load_file_list(current_directory->name); | |
| load_current_game_metadata(); | |
| } | |
| static void scan_flash() { | |
| if(api.list_installed_games) { | |
| api.list_installed_games([](const uint8_t *ptr, uint32_t block, uint32_t size){ | |
| File::add_buffer_file("flash:/" + std::to_string(block) + ".blit", ptr, size); | |
| }); | |
| } | |
| } | |
| void init() { | |
| set_screen_mode(ScreenMode::hires); | |
| screen.clear(); | |
| // shrink the filename column on narrower screens | |
| if(screen.bounds.w < game_actions_offset.x + 24) { | |
| int diff = (game_actions_offset.x + 24) - screen.bounds.w; | |
| game_info_offset.x -= diff; | |
| game_actions_offset.x -= diff; | |
| } | |
| selected_menu_item = 0; | |
| init_theme(); | |
| spritesheet = Surface::load(sprites); | |
| scan_flash(); | |
| init_lists(); | |
| // restore previously selected file | |
| PathSave save; | |
| save.last_path[0] = 0; | |
| if(read_save(save, path_save_slot)) { | |
| auto path = std::string_view(save.last_path); | |
| auto slash = path.find_first_of('/'); | |
| std::string_view dir; | |
| if(slash == std::string_view::npos) | |
| dir = "/"; | |
| else | |
| dir = path.substr(0, slash); | |
| // select dir | |
| for(auto it = directory_list.begin(); it != directory_list.end(); ++it) { | |
| if(it->name == dir) { | |
| if(it != current_directory) | |
| load_file_list(it->name); | |
| current_directory = it; | |
| break; | |
| } | |
| } | |
| // select file | |
| for(auto it = game_list.begin(); it != game_list.end(); ++it) { | |
| if(it->filename == path) { | |
| selected_menu_item = it - game_list.begin(); | |
| break; | |
| } | |
| } | |
| load_current_game_metadata(); | |
| } | |
| credits::prepare(); | |
| } | |
| static void swoosh(uint32_t time, float t1, float t2, float s1, float s2, int t0, int offset_y=120, int size=60, int alpha=64) { | |
| constexpr int swoosh_resolution = 32; | |
| int w = (screen.bounds.w + swoosh_resolution - 1) / swoosh_resolution; | |
| for(auto x = 0; x < w; x++) { | |
| float t_a = (x / s1) + float(time + t0) / t1; | |
| float t_b = (x / s2) + float(time + t0) / t2; | |
| int y1 = sinf(t_a) * size; | |
| int y2 = sinf(t_b) * size; | |
| if(y1 > y2) std::swap(y1, y2); | |
| y1 += offset_y; | |
| y2 += offset_y + 2; | |
| int range = y2 - y1; | |
| for(auto y = 0; y <= range; y++) { | |
| if(y > range / 2) { | |
| screen.pen.a = alpha - (alpha * y / range); | |
| } else { | |
| screen.pen.a = alpha * y / range; | |
| } | |
| // This is an optimisation, not an aesthetic choice! | |
| screen.h_span(Point(x * swoosh_resolution, y1 + y), swoosh_resolution); | |
| } | |
| } | |
| } | |
| static void render_fps(uint32_t us_start) { | |
| if(!show_fps) return; | |
| // draw FPS meter | |
| uint32_t us_end = now_us(); | |
| uint32_t us_elapsed = us_diff(us_start, us_end); | |
| screen.mask = nullptr; | |
| screen.pen = Pen(0, 0, 0); | |
| screen.rectangle(Rect(Point(0, screen.bounds.h - 14), Size(game_info_offset.x - 10, 14))); | |
| screen.pen = Pen(255, 0, 0); | |
| for (unsigned int i = 0; i < us_elapsed / 1000; i++) { | |
| screen.pen = Pen(i * 5, 255 - (i * 5), 0); | |
| screen.rectangle(Rect(i * 3 + 1, screen.bounds.h - 3, 2, 2)); | |
| } | |
| screen.pen = Pen(255, 255, 255); | |
| screen.text(std::to_string(us_elapsed), minimal_font, Point(0, screen.bounds.h - 12)); | |
| } | |
| static void render_directory_list() { | |
| // adjust alignment rect for vertical spacing | |
| const int text_align_height = ROW_HEIGHT + launcher_font.spacing_y; | |
| // list folders | |
| if(directory_list.empty()) | |
| return; | |
| int width = screen.bounds.w - game_info_offset.x - 10; | |
| // darken behind if showing screenshot | |
| if(screenshot) { | |
| screen.pen = theme.color_background; | |
| screen.pen.a = 150; | |
| screen.rectangle(Rect(game_info_offset.x - 10, 0, width + 20, 20)); | |
| } | |
| screen.clip = Rect(game_info_offset.x, 5, width, text_align_height); | |
| for(auto &directory : directory_list) { | |
| if(directory.name == current_directory->name) | |
| screen.pen = theme.color_accent; | |
| else | |
| screen.pen = theme.color_text; | |
| int x = 120 + (width / 2) + directory.x - directory_list_scroll_offset; | |
| screen.text(directory.name == "/" ? "ROOT" : directory.name, launcher_font, Rect(x, 5, width, text_align_height), true, TextAlign::center_v); | |
| } | |
| screen.clip = Rect(Point(0, 0), screen.bounds); | |
| } | |
| static void render_file_list() { | |
| // adjust alignment rect for vertical spacing | |
| const int text_align_height = ROW_HEIGHT + launcher_font.spacing_y; | |
| if(game_list.empty()) | |
| return; | |
| // background | |
| if(screenshot) { | |
| // darken if showing screenshot | |
| screen.pen = theme.color_background; | |
| screen.pen.a = 150; | |
| } else | |
| screen.pen = theme.color_overlay; | |
| screen.rectangle(Rect(0, 0, game_info_offset.x - 10, screen.bounds.h)); | |
| screen.clip = Rect(0, 0, game_info_offset.x - 20, screen.bounds.h); | |
| int title_w = screen.clip.w - file_list_scroll_offset.x; | |
| int y = (screen.bounds.h / 2) - 5 - file_list_scroll_offset.y; | |
| int i = 0; | |
| for(auto &file : game_list) { | |
| if(i++ == selected_menu_item) | |
| screen.pen = theme.color_accent; | |
| else | |
| screen.pen = theme.color_text; | |
| screen.text(file.title, launcher_font, Rect(file_list_scroll_offset.x, y, title_w, text_align_height), true, TextAlign::center_v); | |
| y += ROW_HEIGHT; | |
| } | |
| screen.clip = Rect(Point(0, 0), screen.bounds); | |
| } | |
| static void render_screenshot() { | |
| if(screenshot->bounds.w == screen.bounds.w) { | |
| // full screen image | |
| screen.blit(screenshot, Rect(Point(0, 0), screenshot->bounds), Point(0, 0)); | |
| } else if(screenshot->bounds == Size(128, 128)) { | |
| // standard spritesheet size, show in info column | |
| screen.pen = Pen(0, 0, 0, 255); | |
| screen.rectangle(Rect(game_info_offset, Size(128, 128))); | |
| screen.blit(screenshot, Rect(Point(0, 0), screenshot->bounds), game_info_offset); | |
| } else { | |
| screen.stretch_blit(screenshot, Rect(Point(0, 0), screenshot->bounds), Rect(Point(0, 0), screen.bounds)); | |
| } | |
| } | |
| static void render_game_info() { | |
| // run game / launch file | |
| if(selected_game.can_launch) { | |
| screen.sprite(1, Point(game_actions_offset.x, game_actions_offset.y + 12)); | |
| screen.sprite(0, Point(game_actions_offset.x + 10, game_actions_offset.y + 12), SpriteTransform::R90); | |
| } | |
| // game info | |
| if(selected_game_metadata.splash) | |
| screen.blit(selected_game_metadata.splash, Rect(Point(0, 0), selected_game_metadata.splash->bounds), game_info_offset); | |
| screen.pen = theme.color_accent; | |
| std::string wrapped_title = screen.wrap_text(selected_game_metadata.title, screen.bounds.w - game_info_offset.x - 10, launcher_font); | |
| Size title_size = screen.measure_text(wrapped_title, launcher_font); | |
| screen.text(wrapped_title, launcher_font, Point(game_info_offset.x, game_info_offset.y + 104)); | |
| Rect desc_rect(game_info_offset.x, game_info_offset.y + 108 + title_size.h, screen.bounds.w - game_info_offset.x - 10, 64); | |
| screen.pen = theme.color_text; | |
| std::string wrapped_desc = screen.wrap_text(selected_game_metadata.description, desc_rect.w, launcher_font); | |
| screen.text(wrapped_desc, launcher_font, desc_rect); | |
| screen.text(selected_game_metadata.author, minimal_font, Point(game_info_offset.x, screen.bounds.h - 32)); | |
| screen.text(selected_game_metadata.version, minimal_font, Point(game_info_offset.x, screen.bounds.h - 24)); | |
| int num_blocks = calc_num_blocks(selected_game.size); | |
| char buf[20]; | |
| snprintf(buf, 20, "%i block%s", num_blocks, num_blocks == 1 ? "" : "s"); | |
| screen.text(buf, minimal_font, Point(game_info_offset.x, screen.bounds.h - 16)); | |
| if(!selected_game.can_launch) { | |
| screen.pen = {255, 0, 0}; | |
| screen.text("INCOMPATIBLE", minimal_font, Point(screen.bounds.w - 10, screen.bounds.h - 16), true, TextAlign::top_right); | |
| } | |
| } | |
| void render(uint32_t time) { | |
| uint32_t us_start = now_us(); | |
| screen.sprites = spritesheet; | |
| screen.pen = theme.color_background; | |
| screen.clear(); | |
| // display background swoosh if not displaying a screenshot or the credits | |
| if(current_screen != Screen::screenshot && current_screen != Screen::credits && selected_game.type != GameType::screenshot) { | |
| screen.pen = Pen(255, 255, 255); | |
| swoosh(time, 5100.0f, 3900.0f, 1900.0f, 900.0f, 3500); | |
| screen.pen = theme.color_accent; | |
| swoosh(time, 5000.0f, 3000.0f, 1000.0f, 1000.0f, 0); | |
| screen.pen = Pen(~theme.color_accent.r, ~theme.color_accent.g, ~theme.color_accent.b); | |
| swoosh(time, 5100.0f, 3900.0f, 900.0f, 1100.0f, 5000); | |
| } | |
| // display image preview | |
| if(!game_list.empty() && screenshot) | |
| render_screenshot(); | |
| // don't display lists over fullscreen screenshot | |
| if(current_screen != Screen::screenshot) { | |
| render_directory_list(); | |
| render_file_list(); | |
| } | |
| // current file info/actions | |
| if(!game_list.empty()) { | |
| // delete | |
| screen.sprite(2, Point(game_actions_offset.x, game_actions_offset.y)); | |
| screen.sprite(0, Point(game_actions_offset.x + 10, game_actions_offset.y)); | |
| if(selected_game.type == GameType::screenshot) { | |
| if(screenshot && screenshot->bounds == Size(128, 128)) { | |
| if(selected_game.can_launch){ | |
| // edit (in sprite editor, presumably) | |
| screen.sprite(1, Point(game_actions_offset.x, game_actions_offset.y + 12)); | |
| screen.sprite(0, Point(game_actions_offset.x + 10, game_actions_offset.y + 12), SpriteTransform::R90); | |
| } | |
| } else if (current_screen == Screen::screenshot) { | |
| // exit fullscreen | |
| screen.sprite(5, Point(game_actions_offset.x, game_actions_offset.y + 12)); | |
| screen.sprite(0, Point(game_actions_offset.x + 10, game_actions_offset.y + 12), SpriteTransform::R180); | |
| } else if(screenshot) { | |
| // view screenshot fullscreen | |
| screen.sprite(4, Point(game_actions_offset.x, game_actions_offset.y + 12)); | |
| screen.sprite(0, Point(game_actions_offset.x + 10, game_actions_offset.y + 12), SpriteTransform::R90); | |
| } | |
| } else { | |
| render_game_info(); | |
| } | |
| } else { | |
| screen.pen = theme.color_text; | |
| if(current_directory->name != "flash:" && !blit::is_storage_available()) | |
| screen.text("No SD Card\nDetected.", launcher_font, Point(screen.bounds.w / 2, screen.bounds.h / 2), true, TextAlign::center_center); | |
| else | |
| screen.text("No Games Found.", launcher_font, Point(screen.bounds.w / 2, screen.bounds.h / 2), true, TextAlign::center_center); | |
| } | |
| if (current_screen == Screen::credits) { | |
| credits::render(); | |
| } | |
| //progress.draw(); | |
| dialog.draw(); | |
| render_fps(us_start); | |
| } | |
| void update(uint32_t time) { | |
| if(blit::is_storage_available() != sd_detected) { | |
| init_lists(); | |
| sd_detected = blit::is_storage_available(); | |
| } | |
| bool button_a = buttons.released & Button::A; | |
| bool button_b = buttons.pressed & Button::B; | |
| bool button_x = buttons.pressed & Button::X; | |
| bool button_y = buttons.pressed & Button::Y; | |
| bool button_menu = buttons.pressed & Button::MENU; | |
| bool button_up = ar_button_up.next(time, buttons.state & Button::DPAD_UP || joystick.y < -0.2f); | |
| bool button_down = ar_button_down.next(time, buttons.state & Button::DPAD_DOWN || joystick.y > 0.2f); | |
| bool button_left = ar_button_left.next(time, buttons.state & Button::DPAD_LEFT || joystick.x < -0.5f); | |
| bool button_right = ar_button_right.next(time, buttons.state & Button::DPAD_RIGHT || joystick.x > 0.5f); | |
| if(dialog.update()) | |
| return; | |
| // update/close credits | |
| if (current_screen == Screen::credits) { | |
| credits::update(time); | |
| if (button_menu) { | |
| current_screen = Screen::main; | |
| } | |
| if(button_y) { | |
| show_fps = !show_fps; | |
| } | |
| return; | |
| } | |
| // display credits | |
| if (button_menu) { | |
| credits::reset_scrolling(); | |
| current_screen = Screen::credits; | |
| } | |
| // scroll through file list | |
| int total_items = (int)game_list.size(); | |
| auto old_menu_item = selected_menu_item; | |
| if(button_up) { | |
| selected_menu_item--; | |
| if(selected_menu_item < 0) { | |
| selected_menu_item = total_items - 1; | |
| } | |
| } | |
| if(button_down) { | |
| selected_menu_item++; | |
| if(selected_menu_item > total_items - 1) { | |
| selected_menu_item = 0; | |
| } | |
| } | |
| if(current_screen == Screen::screenshot) { | |
| // b to exit full screen screenshot view | |
| if(button_b) { | |
| current_screen = Screen::main; | |
| } | |
| } else { | |
| // scroll through directories | |
| if(button_left) { | |
| if(current_directory == directory_list.begin()) | |
| current_directory = --directory_list.end(); | |
| else | |
| --current_directory; | |
| } | |
| if(button_right) { | |
| current_directory++; | |
| if(current_directory == directory_list.end()) { | |
| current_directory = directory_list.begin(); | |
| } | |
| } | |
| // reload file list if dir changed | |
| if(button_left || button_right) { | |
| load_file_list(current_directory->name); | |
| selected_menu_item = 0; | |
| old_menu_item = -1; | |
| } | |
| // toggle sort mode | |
| if (button_y) { | |
| file_sort = file_sort == SortBy::name ? SortBy::size : SortBy::name; | |
| sort_file_list(); | |
| } | |
| } | |
| // scroll list towards selected item | |
| file_list_scroll_offset.y += ((selected_menu_item * ROW_HEIGHT) - file_list_scroll_offset.y) / 5.0f; | |
| directory_list_scroll_offset += (current_directory->x + current_directory->w / 2 - directory_list_scroll_offset) / 5.0f; | |
| if(game_list.empty()) | |
| return; | |
| // load metadata for selected item | |
| if(selected_menu_item != old_menu_item) { | |
| load_current_game_metadata(); | |
| } | |
| // paranoid bail out if you're browsing screenshots full screen and come across a game | |
| if(selected_game.type != GameType::screenshot && current_screen == Screen::screenshot) { | |
| current_screen = Screen::main; | |
| } | |
| // delete current game / screenshot | |
| if(button_x) { | |
| delete_current_game(); | |
| } | |
| // launch game / show screenshot fullscreen | |
| if(button_a) { | |
| if(selected_game.type == GameType::screenshot && !selected_game.can_launch) { | |
| current_screen = Screen::screenshot; | |
| } else if(selected_game.can_launch) { | |
| if(!launch_current_game()) | |
| dialog.show("Error!", "Failed to launch " + selected_game.filename, [](bool){}, false); | |
| } | |
| } | |
| } |