diff --git a/src/spice2x/cfg/option.h b/src/spice2x/cfg/option.h index 6aa39c9..2c7d2bd 100644 --- a/src/spice2x/cfg/option.h +++ b/src/spice2x/cfg/option.h @@ -13,6 +13,15 @@ enum class OptionType { Hex, }; +enum class OptionPickerType { + None, + AsioDriver, + EACard, + CpuAffinity, + FilePath, + DirectoryPath, +}; + struct OptionDefinition { std::string title; // unique identifier used for flag matching but also stored in config files @@ -32,6 +41,10 @@ struct OptionDefinition { bool sensitive = false; std::vector> elements = {}; bool disabled = false; + OptionPickerType picker = OptionPickerType::None; + + // for OptionPickerType::FilePath + std::string file_extension = ""; }; class Option { diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index 47fa204..2da08c0 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -71,6 +71,7 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Text, .setting_name = "*.dll", .category = "Path Overrides", + // intentionally not setting a file picker here to discourage people setting this without a good reason }, { .title = "Open Configurator", @@ -121,6 +122,7 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "E004010000000000", .category = "Network", .sensitive = true, + .picker = OptionPickerType::EACard, }, { .title = "Player 2 Card", @@ -130,6 +132,7 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "E004010000000000", .category = "Network", .sensitive = true, + .picker = OptionPickerType::EACard, }, { // Player1PinMacro @@ -618,6 +621,7 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Text, .game_name = "Beatmania IIDX", .category = "Game Options", + .picker = OptionPickerType::AsioDriver, }, { .title = "IIDX BIO2 Firmware Update", @@ -778,6 +782,7 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Text, .game_name = "Sound Voltex", .category = "Game Options (Advanced)", + .picker = OptionPickerType::DirectoryPath, }, { .title = "SDVX Printer Output Clear", @@ -889,6 +894,7 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Text, .game_name = "Sound Voltex", .category = "Game Options", + .picker = OptionPickerType::AsioDriver, }, { // spice2x_SDVXSubPos @@ -1284,6 +1290,7 @@ static const std::vector OPTION_DEFINITIONS = { .desc = "Sets a custom path to the modules folder.", .type = OptionType::Text, .category = "Path Overrides", + // intentionally not setting a folder picker here to discourage people setting this without a good reason }, { .title = "Screenshot Folder Override", @@ -1291,6 +1298,7 @@ static const std::vector OPTION_DEFINITIONS = { .desc = "Sets a custom path to the screenshots folder.", .type = OptionType::Text, .category = "Path Overrides", + .picker = OptionPickerType::DirectoryPath, }, { .title = "Configuration Path Override", @@ -1310,6 +1318,8 @@ static const std::vector OPTION_DEFINITIONS = { "If left empty, %appdata%\\spice2x\\spicetools_screen_resize.json will be used.", .type = OptionType::Text, .category = "Path Overrides", + .picker = OptionPickerType::FilePath, + .file_extension = "JSON", }, { // PatchManagerConfigPath @@ -1319,6 +1329,8 @@ static const std::vector OPTION_DEFINITIONS = { "If left empty, %appdata%\\spice2x\\spicetools_patch_manager.json will be used.", .type = OptionType::Text, .category = "Path Overrides", + .picker = OptionPickerType::FilePath, + .file_extension = "JSON", }, { .title = "Intel SDE", @@ -1326,6 +1338,7 @@ static const std::vector OPTION_DEFINITIONS = { .desc = "Path to Intel SDE kit path for automatic attaching.", .type = OptionType::Text, .category = "Development", + .picker = OptionPickerType::DirectoryPath }, { .title = "ea3-config.xml Override", @@ -1333,6 +1346,8 @@ static const std::vector OPTION_DEFINITIONS = { .desc = "Sets a custom path to ea3-config.xml.", .type = OptionType::Text, .category = "Path Overrides", + .picker = OptionPickerType::FilePath, + .file_extension = "XML", }, { .title = "app-config.xml Override", @@ -1340,6 +1355,8 @@ static const std::vector OPTION_DEFINITIONS = { .desc = "Sets a custom path to app-config.xml.", .type = OptionType::Text, .category = "Path Overrides", + .picker = OptionPickerType::FilePath, + .file_extension = "XML", }, { .title = "avs-config.xml Override", @@ -1347,6 +1364,8 @@ static const std::vector OPTION_DEFINITIONS = { .desc = "Sets a custom path to avs-config.xml.", .type = OptionType::Text, .category = "Path Overrides", + .picker = OptionPickerType::FilePath, + .file_extension = "XML", }, { .title = "bootstrap.xml Override", @@ -1354,6 +1373,8 @@ static const std::vector OPTION_DEFINITIONS = { .desc = "Sets a custom path to bootstrap.xml.", .type = OptionType::Text, .category = "Path Overrides", + .picker = OptionPickerType::FilePath, + .file_extension = "XML", }, { .title = "log.txt Override", @@ -1361,6 +1382,8 @@ static const std::vector OPTION_DEFINITIONS = { .desc = "Sets a custom path to log.txt.", .type = OptionType::Text, .category = "Path Overrides", + .picker = OptionPickerType::FilePath, + .file_extension = "TXT", }, { .title = "API TCP Port", @@ -1670,6 +1693,7 @@ static const std::vector OPTION_DEFINITIONS = { "Must provide a hexadecimal mask (e.g., 0x1ff00).", .type = OptionType::Hex, .category = "Performance", + .picker = OptionPickerType::CpuAffinity, }, { // spice2x_ProcessorEfficiencyClass @@ -1774,6 +1798,7 @@ static const std::vector OPTION_DEFINITIONS = { "Does nothing for games that do not output to exclusive WASAPI.", .type = OptionType::Text, .category = "Audio", + .picker = OptionPickerType::AsioDriver, }, { .title = "WASAPI Dummy Context", @@ -2607,6 +2632,7 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Text, .game_name = "LovePlus", .category = "Game Options (Advanced)", + .picker = OptionPickerType::DirectoryPath, }, { .title = "LovePlus Printer Output Clear", diff --git a/src/spice2x/overlay/imgui/extensions.cpp b/src/spice2x/overlay/imgui/extensions.cpp index 341e939..57da808 100644 --- a/src/spice2x/overlay/imgui/extensions.cpp +++ b/src/spice2x/overlay/imgui/extensions.cpp @@ -142,7 +142,6 @@ namespace ImGui { ImGui::PopStyleVar(); ImGui::PopStyleColor(); if (!tooltip.empty() && ImGui::IsItemHovered(TOOLTIP_FLAGS)) { - ImGui::SameLine(); ImGui::HelpTooltip(tooltip.c_str()); } ImGui::PopID(); @@ -159,7 +158,6 @@ namespace ImGui { bool clicked = ImGui::Button("\u00D7"); // multiplication sign (×) ImGui::PopStyleColor(4); if (!tooltip.empty() && ImGui::IsItemHovered(TOOLTIP_FLAGS)) { - ImGui::SameLine(); ImGui::HelpTooltip(tooltip.c_str()); } ImGui::PopID(); diff --git a/src/spice2x/overlay/imgui/impl_spice.cpp b/src/spice2x/overlay/imgui/impl_spice.cpp index a09c384..09e307b 100644 --- a/src/spice2x/overlay/imgui/impl_spice.cpp +++ b/src/spice2x/overlay/imgui/impl_spice.cpp @@ -283,7 +283,7 @@ void ImGui_ImplSpice_NewFrame() { memset(io.MouseDown, false, sizeof(io.MouseDown)); // early quit if window not in focus - if (!superexit::has_focus()) { + if (!superexit::has_focus() || rawinput::OS_WINDOW_ACTIVE) { return; } diff --git a/src/spice2x/overlay/windows/config.cpp b/src/spice2x/overlay/windows/config.cpp index d30924b..084caba 100644 --- a/src/spice2x/overlay/windows/config.cpp +++ b/src/spice2x/overlay/windows/config.cpp @@ -4,13 +4,16 @@ #include #include +#include #include #include "build/defs.h" #include "build/resource.h" #include "cfg/config.h" #include "cfg/configurator.h" +#include "external/asio/asiolist.h" #include "external/imgui/imgui_internal.h" +#include "external/imgui/misc/cpp/imgui_stdlib.h" #include "games/io.h" #include "avs/core.h" #include "avs/ea3.h" @@ -26,6 +29,7 @@ #include "util/fileutils.h" #include "util/logging.h" #include "util/resutils.h" +#include "util/scope_guard.h" #include "util/time.h" #include "util/utils.h" @@ -33,6 +37,17 @@ #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 @@ -40,6 +55,8 @@ namespace overlay::windows { const auto PROJECT_URL = "https://spice2x.github.io"; constexpr ImVec4 TEXT_COLOR_GREEN(0.f, 1.f, 0.f, 1.f); + + std::unique_ptr asio_driver_list; Config::Config(overlay::SpiceOverlay *overlay) : Window(overlay) { this->title = "Configuration"; @@ -2707,17 +2724,17 @@ namespace overlay::windows { // open dialog to get path auto ofn_path = std::make_unique(512); + ofn_path[0] = L'\0'; OPENFILENAMEW ofn {}; - memset(&ofn, 0, sizeof(ofn)); ofn.lStructSize = sizeof(ofn); - ofn.hwndOwner = nullptr; - ofn.lpstrFilter = L""; ofn.lpstrFile = ofn_path.get(); ofn.nMaxFile = 512; - ofn.Flags = OFN_EXPLORER; + ofn.Flags = OFN_EXPLORER | OFN_NOCHANGEDIR; ofn.lpstrDefExt = L"txt"; + ofn.lpstrInitialDir = L"."; // check for success + auto guard = rawinput::set_os_window_focus_guard(); if (GetSaveFileNameW(&ofn)) { // update card path @@ -2905,6 +2922,246 @@ namespace overlay::windows { ImGui::EndDisabled(); } + std::string Config::build_option_value_picker_title(const OptionDefinition& definition) { + // need to make these all unique since they are also used as ID + // if not unique, append ## per ImGui rules to create unique ones + switch (definition.picker) { + case OptionPickerType::AsioDriver: + return "ASIO Driver Picker"; + case OptionPickerType::EACard: + return "EA Card Picker"; + case OptionPickerType::CpuAffinity: + return "CPU Affinity Picker"; + case OptionPickerType::FilePath: + return "File Picker"; + case OptionPickerType::DirectoryPath: + return "Folder Picker"; + default: + return "Unknown Picker"; + }; + } + + void Config::build_option_value_picker(Option& option) { + auto &definition = option.get_definition(); + if (definition.picker == OptionPickerType::AsioDriver) { + if (asio_driver_list == nullptr) { + asio_driver_list = std::make_unique(); + } + + if (asio_driver_list->driver_list.empty()) { + ImGui::TextUnformatted("No ASIO drivers found."); + } else { + ImGui::TextUnformatted("Pick from ASIO drivers:"); + ImGui::SetNextItemWidth(300.f); + if (ImGui::BeginListBox("##asiodrivers")) { + for (const auto &driver : asio_driver_list->driver_list) { + const bool is_selected = option.value == std::string(driver.name); + if (ImGui::Selectable(fmt::format("[{}] {}", driver.id, driver.name).c_str(), is_selected)) { + option.value = driver.name; + } + } + ImGui::EndListBox(); + } + } + } else if (definition.picker == OptionPickerType::EACard) { + ImGui::TextUnformatted("Generate a new card number:"); + if (ImGui::Button("Generate")) { + char new_card[17]; + generate_ea_card(new_card); + option.value = new_card; + } + } else if (definition.picker == OptionPickerType::CpuAffinity) { + ImGui::TextUnformatted("Requires restart! Showing all procs in Group 0."); + ImGui::TextUnformatted(""); + ImGui::TextUnformatted("Pick CPU cores to use:"); + const uint64_t cpu_count = GetActiveProcessorCount(0); + uint64_t affinity = 0; + if (!option.value.empty()) { + try { + affinity = std::stoull(option.value, nullptr, 16); + } catch (const std::exception &ex) { + option.value = ""; + affinity = 0; + } + } + ImGui::BeginChild( + "##cpuaffinity", + ImVec2( + 0, + ImGui::GetFrameHeightWithSpacing() * std::clamp(cpu_count / 4, 2ull, 4ull)) + ); + + bool selection_changed = false; + uint64_t set_bits = 0; + for (uint64_t i = 0; i < cpu_count; i++) { + if (i % 4 != 0) { + ImGui::SameLine(); + } + + bool selected = (affinity & (1ULL << i)) != 0; + if (ImGui::Checkbox(fmt::format("CPU {}", i).c_str(), &selected)) { + selection_changed = true; + if (selected) { + affinity |= (1ULL << i); + } else { + affinity &= ~(1ULL << i); + } + } + if (selected) { + set_bits++; + } + } + if (selection_changed) { + if (set_bits == 0) { + option.value = ""; + } else { + option.value = fmt::format("0x{:X}", affinity); + } + } + + ImGui::EndChild(); + if (option.value.empty()) { + ImGui::TextUnformatted("Using all CPUs (option default)."); + } else { + ImGui::TextUnformatted("Using selected CPUs."); + } + } else if (definition.picker == OptionPickerType::FilePath) { + if (!cfg::CONFIGURATOR_STANDALONE) { + ImGui::BeginDisabled(); + ImGui::TextUnformatted("File browser only works in spicecfg."); + } + if (ImGui::Button("Select File...")) { + // run in separate thread otherwise we get a crash + if (!file_picker_thread) { + file_picker_done = false; + file_picker_path = ""; + file_picker_thread = new std::thread([this, &definition] { + + std::wstring extensions; + if (!definition.file_extension.empty()) { + const std::wstring ext = s2ws(definition.file_extension); + // filter to file extension preferred by the option (e.g., DLL) + extensions = ext + L" Files (*." + ext + L")"; + extensions.push_back(L'\0'); + extensions += L"*." + ext; + extensions.push_back(L'\0'); + // also add "All files" filter + extensions += L"All Files (*.*)"; + extensions.push_back(L'\0'); + extensions += L"*.*"; + extensions.push_back(L'\0'); + // eol + extensions.push_back(L'\0'); + } else { + extensions = L"All Files (*.*)"; + extensions.push_back(L'\0'); + extensions += L"*.*"; + extensions.push_back(L'\0'); + extensions.push_back(L'\0'); + } + + // open dialog to get path + auto ofn_path = std::make_unique(512); + ofn_path[0] = L'\0'; + OPENFILENAMEW ofn{}; + ofn.lStructSize = sizeof(ofn); + ofn.lpstrFilter = extensions.c_str(); + ofn.lpstrFile = ofn_path.get(); + ofn.nMaxFile = 512; + ofn.nFilterIndex = 1; + ofn.Flags = OFN_EXPLORER | OFN_NOCHANGEDIR; + ofn.lpstrInitialDir = L"."; + + // check for success + auto guard = rawinput::set_os_window_focus_guard(); + if (GetSaveFileNameW(&ofn)) { + file_picker_path = std::filesystem::path(ofn_path.get()); + } + + // clean up + file_picker_done = true; + }); + } + } + + if (file_picker_done) { + file_picker_done = false; + file_picker_thread->join(); + delete file_picker_thread; + file_picker_thread = nullptr; + if (!file_picker_path.empty()) { + option.value = file_picker_path.string(); + } + } + + if (!cfg::CONFIGURATOR_STANDALONE) { + ImGui::EndDisabled(); + ImGui::TextUnformatted("File browser only works in spicecfg."); + } + } else if (definition.picker == OptionPickerType::DirectoryPath) { + if (!cfg::CONFIGURATOR_STANDALONE) { + ImGui::BeginDisabled(); + ImGui::TextUnformatted("File browser only works in spicecfg."); + } + if (ImGui::Button("Browse...")) { + // run in separate thread otherwise we get a crash + if (!file_picker_thread) { + file_picker_done = false; + file_picker_path = ""; + file_picker_thread = new std::thread([this] { + CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + const auto spice_bin_path = + libutils::module_file_name(nullptr).parent_path().wstring(); + + // SHBrowseForFolderW sucks, but the alternatives are: + // 1. Use IFileDialog, which requires pulling in more Windows dependencies + // 2. use ImGui::FileBrowser + // both are acceptable but sticking to legacy UI for now for simplicity + BROWSEINFOW info{}; + info.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE; + info.lpfn = BrowseCallbackProc; + info.lParam = reinterpret_cast(spice_bin_path.c_str()); + auto guard = rawinput::set_os_window_focus_guard(); + auto pidl = SHBrowseForFolderW(&info); + if (pidl) { + wchar_t path[MAX_PATH]; + std::filesystem::path result; + if (SHGetPathFromIDListW(pidl, path)) { + file_picker_path = path; + } + CoTaskMemFree(pidl); + } else { + file_picker_path = ""; + } + + // clean up + file_picker_done = true; + CoUninitialize(); + }); + } + } + + if (file_picker_done) { + file_picker_done = false; + file_picker_thread->join(); + delete file_picker_thread; + file_picker_thread = nullptr; + if (!file_picker_path.empty()) { + option.value = file_picker_path.string(); + } + } + + if (!cfg::CONFIGURATOR_STANDALONE) { + ImGui::EndDisabled(); + ImGui::TextUnformatted("File browser only works in spicecfg."); + } + + } else { + ImGui::TextUnformatted("No picker available for this option. How did you get here?"); + } + } + void Config::build_options( std::vector