#include "config.h" #include #include #include #include #include #include #include #include "build/defs.h" #include "build/resource.h" #include "cfg/config.h" #include "cfg/configurator.h" #include "external/imgui/imgui_internal.h" #include "external/imgui/misc/cpp/imgui_stdlib.h" #include "hooks/audio/asio_driver_scan.h" #include "games/io.h" #include "games/sdvx/sdvx.h" #include "games/popn/popn.h" #include "avs/core.h" #include "avs/ea3.h" #include "avs/game.h" #include "light_match_map.h" #include "launcher/launcher.h" #include "launcher/options.h" #include "misc/eamuse.h" #include "overlay/imgui/extensions.h" #include "rawinput/piuio.h" #include "rawinput/rawinput.h" #include "rawinput/touch.h" #include "misc/clipboard.h" #include "util/fileutils.h" #include "util/logging.h" #include "util/resutils.h" #include "util/scope_guard.h" #include "util/time.h" #include "util/sysutils.h" #include "util/utils.h" #ifdef min #undef min #endif static int CALLBACK BrowseCallbackProc( HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData) { if (uMsg == BFFM_INITIALIZED) { SendMessageW(hwnd, BFFM_SETSELECTIONW, TRUE, lpData); } return 0; } namespace overlay::windows { // same width as dummy marker const float INDENT = 22.f; const float ROW_INDENT = overlay::apply_scaling(8); const float ROW_CELL_PADDING_EXTRA = overlay::apply_scaling(2); const auto PROJECT_URL = "https://spice2x.github.io"; constexpr ImVec4 TEXT_COLOR_GREEN(0.f, 1.f, 0.f, 1.f); constexpr ImVec4 TEXT_COLOR_RED(1.f, 0.f, 0.f, 1.f); constexpr uint32_t OPTION_INPUT_TEXT_WIDTH = 512; // subtab groups shown in the Options tab left navigation, in display order static const std::vector> OPTIONS_TAB_GROUPS = { { "Game Options", launcher::Options::OptionsCategory::GameOptions }, { "Display", launcher::Options::OptionsCategory::Display }, { "Audio", launcher::Options::OptionsCategory::Audio }, { "Network", launcher::Options::OptionsCategory::Network }, { "Overlay", launcher::Options::OptionsCategory::Overlay }, { "Others", launcher::Options::OptionsCategory::Advanced }, { "Development", launcher::Options::OptionsCategory::Dev }, { "API", launcher::Options::OptionsCategory::API }, }; static const char *OPTIONS_TAB_SEARCH = "Search"; static const char *OPTIONS_TAB_QUICK = "Quick Settings"; // sub-pages shown in the Controller tab left navigation, in display order static const std::vector> CONTROLLER_PAGE_GROUPS = { { "Buttons", ControllerPage::CONTROLLER_PAGE_BUTTONS }, { "Keypads", ControllerPage::CONTROLLER_PAGE_KEYPADS }, { "Analogs", ControllerPage::CONTROLLER_PAGE_ANALOGS }, { "Overlay", ControllerPage::CONTROLLER_PAGE_OVERLAY }, { "Lights", ControllerPage::CONTROLLER_PAGE_LIGHTS }, { "Presets", ControllerPage::CONTROLLER_PAGE_PRESETS }, }; std::optional> asio_driver_list; Config::Config(overlay::SpiceOverlay *overlay) : Window(overlay) { this->title = "Configuration"; this->toggle_button = games::OverlayButtons::ToggleConfig; this->init_size = overlay::apply_scaling_to_vector(ImVec2(800, 600)); this->size_min = overlay::apply_scaling_to_vector(ImVec2(100, 200)); this->init_pos = ImVec2(0, 0); if (cfg::CONFIGURATOR_STANDALONE && cfg::CONFIGURATOR_TYPE == cfg::ConfigType::Config) { this->active = true; this->flags |= ImGuiWindowFlags_NoResize; this->flags |= ImGuiWindowFlags_NoMove; this->flags |= ImGuiWindowFlags_NoTitleBar; this->flags |= ImGuiWindowFlags_NoCollapse; this->flags |= ImGuiWindowFlags_NoDecoration; // prevent the parent window from absorbing wheel events when a hovered // child has no scrollable content (otherwise the parent's tiny overflow // causes the whole UI to jolt by a few pixels each tick) this->flags |= ImGuiWindowFlags_NoScrollWithMouse; } this->flags |= ImGuiWindowFlags_MenuBar; // build game list auto &game_names = games::get_games(); for (auto &game_name : game_names) { this->games_names.push_back(game_name.c_str()); auto &game = this->games_list.emplace_back(game_name); auto buttons = games::get_buttons(game_name); auto analogs = games::get_analogs(game_name); auto lights = games::get_lights(game_name); if (buttons) { for (auto &item : *buttons) { game.addItems(item); } } if (analogs) { for (auto &item : *analogs) { game.addItems(item); } } if (lights) { for (auto &item : *lights) { game.addItems(item); } } // default to currently running game if (!cfg::CONFIGURATOR_STANDALONE && game_name == eamuse_get_game()) { this->games_selected = games_list.size() - 1; this->games_selected_name = game_name; } // standalone configurator should look for file hints if (cfg::CONFIGURATOR_STANDALONE) { const auto file_hints = games::get_game_file_hints(game_name); if (file_hints) { for (auto &file_hint_list : *file_hints) { bool matches_all_files = true; for (auto &file_hint : file_hint_list) { const auto matched = (fileutils::file_exists(file_hint) || fileutils::dir_exists(file_hint) || fileutils::file_exists(std::filesystem::path("modules") / file_hint) || fileutils::file_exists(std::filesystem::path("contents") / file_hint) || fileutils::file_exists(MODULE_PATH / file_hint)); if (!matched) { matches_all_files = false; break; } } if (matches_all_files) { this->games_selected = games_list.size() - 1; this->games_selected_name = game_name; eamuse_set_game(game_name); break; } } } } } // configurator fallback to detected game name if (cfg::CONFIGURATOR_STANDALONE && this->games_selected == -1) { for (size_t i = 0; i < games_names.size(); i++) { if (games_names[i] == eamuse_get_game()) { this->games_selected = i; } } } // add games to the config and window auto &config = ::Config::getInstance(); for (auto &game : games_list) { config.addGame(game, /* save */ false); if (!config.getStatus()) { log_warning("config", "failure adding game: {}", game.getGameName()); } } // single flush in place of ~33 redundant writes during the loop above config.save(); // read card numbers read_card(); } Config::~Config() { } void Config::inc_buttons_many_index(int index_max) { if (this->buttons_many_index == index_max) { this->buttons_many_index = -1; } else { this->buttons_many_index += 1; } } void Config::read_card(int player) { // check if a game is selected if (this->games_selected_name.empty()) { return; } // iterate bindings auto bindings = ::Config::getInstance().getKeypadBindings(this->games_selected_name); for (int p = 0; p < 2; ++p) { if (player < 0 || player == p) { // get path std::filesystem::path path; if (!bindings.card_paths[p].empty()) { path = bindings.card_paths[p]; } else { path = p > 0 ? "card1.txt" : "card0.txt"; } // open file std::ifstream f(path); if (!f || !f.is_open()) { this->keypads_card_number[p][0] = 0; continue; } // get file size f.seekg(0, f.end); auto length = (size_t) f.tellg(); f.seekg(0, f.beg); // read file contents f.read(this->keypads_card_number[p], 16); this->keypads_card_number[p][length < 16 ? length : 16] = 0; f.close(); } } } // Renders an arrow-less, non-collapsible left-nav header row, highlighted when // active. Returns true if the row was clicked this frame; callers apply their // own selection side effects. bool Config::build_nav_header(const char *label, bool active) { ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf; if (active) { flags |= ImGuiTreeNodeFlags_Selected; } // give the active header a clear, distinct highlight int colors_pushed = 0; if (active) { const ImVec4 accent = ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive); ImGui::PushStyleColor(ImGuiCol_Header, accent); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, accent); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_Text)); colors_pushed = 3; } // taller frame padding so headers match the height of the category rows below const ImVec2 frame_padding = ImGui::GetStyle().FramePadding; ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(frame_padding.x, frame_padding.y + overlay::apply_scaling(4))); ImGui::CollapsingHeader(label, flags); ImGui::PopStyleVar(); if (colors_pushed) { ImGui::PopStyleColor(colors_pushed); } return ImGui::IsItemClicked(); } void Config::build_options_tab(float page_offset) { const float content_height = ImGui::GetWindowContentRegionMax().y - page_offset; const float nav_width = overlay::apply_scaling(130); // default to the first group on first display if (this->options_group_selected.empty()) { this->options_group_selected = OPTIONS_TAB_QUICK; } auto options = games::get_options(this->games_selected_name); // collect the set of categories that currently have at least one option that // would be rendered by build_options(), in a single pass. used to keep the nav // in sync with the content so categories that only contain hidden options appear // in the nav exactly when "Show Hidden" is enabled. quick_categories tracks the // distinct quick_setting_category names (quick settings group under their own // categories, separate from the regular option categories). std::set visible_categories; std::set quick_categories; if (options) { for (auto &option : *options) { auto &definition = option.get_definition(); if (!this->options_show_hidden && option.value.empty() && definition.hidden) { continue; } if (!definition.game_name.empty() && definition.game_name != this->games_selected_name) { continue; } visible_categories.insert(definition.category); if (!definition.quick_setting_category.empty()) { quick_categories.insert(definition.quick_setting_category); } } } // left navigation: one header per subtab, each listing its categories. clicking a // header selects that group; clicking a category scrolls the content to that section. ImGui::BeginChild("OptionsNav", ImVec2(nav_width, content_height), false); // exact height of the global option controls pinned at the bottom of the nav const bool show_restart = !cfg::CONFIGURATOR_STANDALONE && this->options_dirty; const float ctrl_spacing = ImGui::GetStyle().ItemSpacing.y; float nav_controls_height = ImGui::GetFrameHeight(); // "Show Hidden" checkbox if (show_restart) { nav_controls_height += ctrl_spacing + ImGui::GetFrameHeight(); // "Restart Game" button } nav_controls_height += ctrl_spacing * 2.0f + 1.0f; // separator + padding // scrollable nav list (group headers and categories) ImGui::BeginChild("OptionsNavList", ImVec2(0, content_height - nav_controls_height - ctrl_spacing), false); // extra vertical padding between nav rows (headers and tree items) const ImVec2 nav_item_spacing = ImGui::GetStyle().ItemSpacing; ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(nav_item_spacing.x, nav_item_spacing.y + overlay::apply_scaling(2))); // arrow-less, non-collapsible nav header; selecting it clears any highlighted category auto nav_header = [this](const char *label) { if (this->build_nav_header(label, this->options_group_selected == label)) { this->options_group_selected = label; this->options_category_selected.clear(); this->options_scroll_top = true; } }; // indented, selectable category row under a header; clicking it flags the // content pane to scroll to that category's section auto nav_category = [this](const char *group_label, const std::string &category) { ImGui::Indent(INDENT * 0.5f); if (ImGui::Selectable(category.c_str(), this->options_category_selected == category)) { this->options_group_selected = group_label; this->options_category_selected = category; this->options_scroll_pending = true; } ImGui::Unindent(INDENT * 0.5f); }; // search entry: selecting it shows the search box in the content pane nav_header(OPTIONS_TAB_SEARCH); // quick settings entry: selecting it shows the quick settings in the content pane nav_header(OPTIONS_TAB_QUICK); // when quick settings is selected, list its categories so they can be jumped to if (this->options_group_selected == OPTIONS_TAB_QUICK) { ImGui::PushID(OPTIONS_TAB_QUICK); for (const auto &category : launcher::get_quick_setting_categories()) { if (quick_categories.contains(category)) { nav_category(OPTIONS_TAB_QUICK, category); } } ImGui::PopID(); } for (const auto &group : OPTIONS_TAB_GROUPS) { nav_header(group.first); // only the selected group expands to show its categories if (this->options_group_selected != group.first) { continue; } ImGui::PushID(group.first); for (const auto &category : launcher::get_categories(group.second)) { if (!category.empty() && visible_categories.contains(category)) { nav_category(group.first, category); } } ImGui::PopID(); } ImGui::PopStyleVar(); ImGui::EndChild(); // OptionsNavList // global option controls pinned to the bottom of the nav ImGui::SetCursorPosY(content_height - nav_controls_height); ImGui::Separator(); ImGui::Checkbox("Show Hidden", &this->options_show_hidden); // left-align the text inside the full-width control buttons ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.0f, 0.5f)); if (show_restart) { if (ImGui::Button("Restart Game")) { launcher::restart(); } ImGui::SameLine(); ImGui::HelpMarker("You need to restart the game to apply the changed settings."); } ImGui::PopStyleVar(); // ButtonTextAlign ImGui::EndChild(); // OptionsNav ImGui::SameLine(); // content: only the options belonging to the selected group. ImGui::BeginChild("OptionsContent", ImVec2(0, content_height), false); const bool scroll_to_top = this->options_scroll_top; this->options_scroll_top = false; float scroll_to_category_y = -1.0f; // breathing room at the top of the content area ImGui::Dummy(ImVec2(0.0f, overlay::apply_scaling(4))); // when a category was clicked in the nav, record its Y for deferred scroll auto scroll_anchor = [this, &scroll_to_category_y](const std::string &category) { if (this->options_scroll_pending && this->options_category_selected == category) { scroll_to_category_y = ImGui::GetCursorPosY(); this->options_scroll_pending = false; } }; if (this->options_group_selected == OPTIONS_TAB_SEARCH) { // search from all options ImGui::SetNextItemWidth(420.f); if (ImGui::InputTextWithHint( "", "Type here to search in options..", &this->search_filter, ImGuiInputTextFlags_EscapeClearsAll)) { this->search_filter_in_lower_case = strtolower(this->search_filter); } if (!this->search_filter.empty()) { ImGui::SameLine(); if (ImGui::Button("Clear")) { this->search_filter.clear(); this->search_filter_in_lower_case.clear(); } } ImGui::Spacing(); // draw matching options if (!this->search_filter.empty()) { for (auto category : launcher::get_categories(launcher::Options::OptionsCategory::Everything)) { this->build_options( options, category, const_cast(&this->search_filter_in_lower_case)); } } } else if (this->options_group_selected == OPTIONS_TAB_QUICK) { for (const auto &category : launcher::get_quick_setting_categories()) { if (!quick_categories.contains(category)) { continue; } scroll_anchor(category); this->build_options(options, category, nullptr, true); } } else { for (const auto &group : OPTIONS_TAB_GROUPS) { if (this->options_group_selected != group.first) { continue; } for (const auto &category : launcher::get_categories(group.second)) { if (category.empty()) { continue; } scroll_anchor(category); this->build_options(options, category); } } } // apply scroll after content layout to avoid mid-layout jitter ImGuiWindow *content_window = ImGui::GetCurrentWindow(); if (scroll_to_top) { if (content_window->Scroll.y > 0.0f) { content_window->Scroll.y = 0.0f; } content_window->ScrollTarget.y = FLT_MAX; } else if (scroll_to_category_y >= 0.0f) { const float spacing_y = ImMax( content_window->WindowPadding.y, ImGui::GetStyle().ItemSpacing.y); const float desired_scroll = ImMax(0.0f, scroll_to_category_y - spacing_y); if (fabsf(content_window->Scroll.y - desired_scroll) > 0.5f) { content_window->Scroll.y = desired_scroll; } content_window->ScrollTarget.y = FLT_MAX; } ImGui::EndChild(); } void Config::build_controller_tab(float page_offset, ControllerPage *page_selected_new) { const float content_height = ImGui::GetWindowContentRegionMax().y - page_offset; const float nav_width = overlay::apply_scaling(99); // hide sub-pages that expose no controls for the selected game auto analogs = games::get_analogs(this->games_selected_name); const bool has_analogs = analogs && !analogs->empty(); auto lights = games::get_lights(this->games_selected_name); const bool has_lights = lights && !lights->empty(); auto page_hidden = [has_analogs, has_lights](ControllerPage page) { switch (page) { case ControllerPage::CONTROLLER_PAGE_ANALOGS: return !has_analogs; case ControllerPage::CONTROLLER_PAGE_LIGHTS: return !has_lights; default: return false; } }; // default to the first page on first display if (this->controller_page_label.empty()) { this->controller_page_label = CONTROLLER_PAGE_GROUPS.front().first; } // if the selected page is no longer available, fall back to the first one for (const auto &group : CONTROLLER_PAGE_GROUPS) { if (this->controller_page_label == group.first && page_hidden(group.second)) { this->controller_page_label = CONTROLLER_PAGE_GROUPS.front().first; break; } } // left navigation: one header per sub-page; clicking a header selects that page ImGui::BeginChild("ControllerNav", ImVec2(nav_width, content_height), false); // extra vertical padding between nav rows const ImVec2 nav_item_spacing = ImGui::GetStyle().ItemSpacing; ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(nav_item_spacing.x, nav_item_spacing.y + overlay::apply_scaling(2))); // arrow-less, non-collapsible nav header; selecting it switches the content pane auto nav_header = [this](const char *label) { if (this->build_nav_header(label, this->controller_page_label == label)) { this->controller_page_label = label; } }; for (const auto &group : CONTROLLER_PAGE_GROUPS) { if (page_hidden(group.second)) { continue; } nav_header(group.first); } ImGui::PopStyleVar(); ImGui::EndChild(); // ControllerNav ImGui::SameLine(); // content: only the selected sub-page ImGui::BeginChild("ControllerContent", ImVec2(0, content_height), false); // breathing room at the top of the content area ImGui::Dummy(ImVec2(0.0f, overlay::apply_scaling(4))); for (const auto &group : CONTROLLER_PAGE_GROUPS) { if (this->controller_page_label != group.first || page_hidden(group.second)) { continue; } // reflect the active sub-page so per-page side effects still fire *page_selected_new = group.second; switch (group.second) { case ControllerPage::CONTROLLER_PAGE_BUTTONS: { // help text for binding buttons, if the game has one const auto help_text = games::get_buttons_help(this->games_selected_name); if (!help_text.empty()) { ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Button Bindings"); ImGui::Spacing(); ImGui::TextWrapped("%s", help_text.c_str()); ImGui::TextUnformatted(""); } // game buttons this->build_buttons("Game", games::get_buttons(this->games_selected_name)); break; } case ControllerPage::CONTROLLER_PAGE_KEYPADS: { // keypad buttons this->build_keypad_warning(); auto keypad_buttons = games::get_buttons_keypads(this->games_selected_name); auto keypad_count = eamuse_get_game_keypads_name(); if (keypad_count == 1) { this->build_buttons("Keypad", keypad_buttons, 0, games::KeypadButtons::Size - 1); } else if (keypad_count >= 2) { this->build_buttons("Keypad", keypad_buttons); } break; } case ControllerPage::CONTROLLER_PAGE_ANALOGS: { // help text for binding analog, if the game has one const auto help_text = games::get_analogs_help(this->games_selected_name); if (!help_text.empty()) { ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Analog Bindings"); ImGui::Spacing(); ImGui::TextWrapped("%s", help_text.c_str()); ImGui::TextUnformatted(""); } this->build_analogs("Game", analogs); break; } case ControllerPage::CONTROLLER_PAGE_OVERLAY: { // overlay buttons this->build_buttons("Overlay", games::get_buttons_overlay(this->games_selected_name)); break; } case ControllerPage::CONTROLLER_PAGE_LIGHTS: { this->build_lights("Game", lights); break; } case ControllerPage::CONTROLLER_PAGE_PRESETS: { this->build_presets(); break; } default: break; } } ImGui::EndChild(); // ControllerContent } void Config::build_content() { // if standalone then fullscreen window if (cfg::CONFIGURATOR_STANDALONE) { ImGui::SetWindowPos(ImVec2(0, 0)); ImGui::SetWindowSize(ImGui::GetIO().DisplaySize); } // toolbar/menu int previous_games_selected = this->games_selected; this->build_menu(&this->games_selected); // remember selected game name if (this->games_selected >= 0 && this->games_selected < (int) games_list.size()) { this->games_selected_name = games_list.at(games_selected).getGameName(); // standalone configurator applies selected game if (cfg::CONFIGURATOR_STANDALONE) { eamuse_set_game(games_selected_name); } } else { // invalid selection this->games_selected_name = ""; } // display launcher if no game is selected if (this->games_selected_name.empty()) { this->build_launcher(); return; } // selected game changed if (previous_games_selected != this->games_selected) { read_card(); templates_cache_dirty = true; log_misc( "config", "game selection changed from {} to {}", previous_games_selected, this->games_selected); } // tab + controller sub-page selection auto tab_selected_new = ConfigTab::CONFIG_TAB_INVALID; auto controller_page_new = ControllerPage::CONTROLLER_PAGE_INVALID; if (ImGui::BeginPaddedTabBar( "Config Tabs", ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)) { // the padded tab bar is taller than a stock tab bar by twice the extra // FramePadding.y added in PaddedTabFramePadding() (top + bottom); include // it so the tab content child height accounts for the larger bar and // doesn't overflow the bottom of the window const int page_offset2 = overlay::apply_scaling(cfg::CONFIGURATOR_STANDALONE ? 65 : 87) + overlay::apply_scaling(2); if (ImGui::BeginPaddedTabItem("Controller")) { tab_selected_new = ConfigTab::CONFIG_TAB_CONTROLLER; this->build_controller_tab(page_offset2, &controller_page_new); ImGui::EndTabItem(); } if (ImGui::BeginPaddedTabItem("Cards")) { tab_selected_new = ConfigTab::CONFIG_TAB_CARDS; this->build_cards_tab(page_offset2); ImGui::EndTabItem(); } if (ImGui::BeginPaddedTabItem("Patches")) { tab_selected_new = ConfigTab::CONFIG_TAB_PATCHES; // initialization static std::once_flag initialized; static bool failure = false; std::call_once(initialized, [this] { if (cfg::CONFIGURATOR_STANDALONE) { // verify game is set, otherwise set failure flag if (strlen(avs::game::MODEL) != 3 || (strlen(avs::game::DEST) != 1) || (strlen(avs::game::SPEC) != 1) || (strlen(avs::game::REV) != 1) || (strlen(avs::game::EXT) != 10) || (strcmp(avs::game::MODEL, "000") == 0) || (strcmp(avs::game::EXT, "0000000000") == 0)) { failure = true; } } }); // display tab contents ImGui::BeginChild("Patches", ImVec2( 0, ImGui::GetWindowContentRegionMax().y - page_offset2), false); if (failure) { ImGui::TextColored(ImVec4(0.7f, 0.f, 0.f, 1.f), "Unable to detect the game version.\n" "Try to open Patch Manager using the game overlay."); } else { // allocate patch manager if (!patch_manager) { patch_manager.reset(new PatchManager(overlay)); } // display patch manager this->patch_manager->build_content(); } ImGui::EndChild(); ImGui::EndTabItem(); } if (ImGui::BeginPaddedTabItem("Options")) { tab_selected_new = ConfigTab::CONFIG_TAB_OPTIONS; this->build_options_tab(page_offset2); ImGui::EndTabItem(); } ImGui::EndTabBar(); } // did tab or controller sub-page selection change? if (this->tab_selected != tab_selected_new || this->controller_page_selected != controller_page_new) { log_misc( "config", "selection changed from tab {} page {} to tab {} page {}", static_cast(this->tab_selected), static_cast(this->controller_page_selected), static_cast(tab_selected_new), static_cast(controller_page_new)); stop_lights_test(); this->tab_selected = tab_selected_new; this->controller_page_selected = controller_page_new; buttons_many_active = false; buttons_many_index = -1; ImGui::CloseCurrentPopup(); } // disclaimer // note: distribution of modified version of this software without providing source is GPLv3 license violation. ImGui::TextColored( ImVec4(1, 0.5f, 0.5f, 1.f), "spice2x is free & open source software; if you paid for it, you were scammed."); if (cfg::CONFIGURATOR_STANDALONE) { ImGui::SameLine(); if (ImGui::TextLink(PROJECT_URL)) { launch_shell(PROJECT_URL); } } } void Config::build_keypad_warning() { // not checking the -iidxtdj option here, we could do that in the future const bool is_tdj = this->games_selected_name == "Beatmania IIDX"; // not using games::popn::is_pikapika_model() here since that always returns false on 32-bit const bool is_popn = this->games_selected_name == "Pop'n Music" && avs::game::SPEC[0] == 'D'; if (!is_tdj && !is_popn) { return; } ImGui::Indent(INDENT); if (is_tdj) { ImGui::TextColored( ImVec4(1, 0.5f, 0.5f, 1.f), "WARNING: Lightning Model (TDJ) I/O will ignore keypad number input!"); } else if (is_popn) { ImGui::TextColored( ImVec4(1, 0.5f, 0.5f, 1.f), "WARNING: PikaPika Pop-Kun model will ignore keypad number input!"); } ImGui::TextWrapped( "Use Toggle Sub Screen button (Controller -> Overlay) to show the overlay and use your mouse, " "connect using SpiceCompanion app, or connect a touch screen to enter " "the PIN."); ImGui::Unindent(INDENT); ImGui::TextUnformatted(""); } void Config::build_buttons(const std::string &name, std::vector