2606 lines
119 KiB
C++
2606 lines
119 KiB
C++
|
#include "config.h"
|
||
|
|
||
|
#include <thread>
|
||
|
|
||
|
#include <windows.h>
|
||
|
#include <shellapi.h>
|
||
|
#include <commdlg.h>
|
||
|
|
||
|
#include "build/defs.h"
|
||
|
#include "build/resource.h"
|
||
|
#include "cfg/config.h"
|
||
|
#include "cfg/configurator.h"
|
||
|
#include "external/imgui/imgui_internal.h"
|
||
|
#include "games/io.h"
|
||
|
#include "avs/core.h"
|
||
|
#include "avs/ea3.h"
|
||
|
#include "avs/game.h"
|
||
|
#include "launcher/launcher.h"
|
||
|
#include "launcher/shutdown.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 "util/fileutils.h"
|
||
|
#include "util/logging.h"
|
||
|
#include "util/resutils.h"
|
||
|
#include "util/time.h"
|
||
|
#include "util/utils.h"
|
||
|
|
||
|
#ifdef min
|
||
|
#undef min
|
||
|
#endif
|
||
|
|
||
|
namespace overlay::windows {
|
||
|
|
||
|
const auto PROJECT_URL = "https://spice2x.github.io";
|
||
|
|
||
|
Config::Config(overlay::SpiceOverlay *overlay) : Window(overlay) {
|
||
|
this->title = "Configuration";
|
||
|
this->toggle_button = games::OverlayButtons::ToggleConfig;
|
||
|
this->init_size = ImVec2(800, 600);
|
||
|
this->size_min = 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;
|
||
|
}
|
||
|
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) {
|
||
|
auto file_hints = games::get_game_file_hints(game_name);
|
||
|
if (file_hints) {
|
||
|
for (auto &file_hint : *file_hints) {
|
||
|
if (fileutils::file_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))
|
||
|
{
|
||
|
this->games_selected = games_list.size() - 1;
|
||
|
this->games_selected_name = game_name;
|
||
|
eamuse_set_game(game_name);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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);
|
||
|
|
||
|
if (!config.getStatus()) {
|
||
|
log_warning("config", "failure adding game: {}", game.getGameName());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void Config::write_card(int player) {
|
||
|
|
||
|
// get path
|
||
|
auto bindings = ::Config::getInstance().getKeypadBindings(this->games_selected_name);
|
||
|
std::filesystem::path path;
|
||
|
if (!bindings.card_paths[player].empty()) {
|
||
|
path = bindings.card_paths[player];
|
||
|
} else {
|
||
|
path = player > 0 ? "card1.txt" : "card0.txt";
|
||
|
}
|
||
|
|
||
|
// write file
|
||
|
std::ofstream f(path);
|
||
|
if (f) {
|
||
|
f.write(this->keypads_card_number[player], strlen(this->keypads_card_number[player]));
|
||
|
f.close();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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();
|
||
|
}
|
||
|
|
||
|
// tab selection
|
||
|
auto tab_selected_new = ConfigTab::CONFIG_TAB_INVALID;
|
||
|
if (ImGui::BeginTabBar("Config Tabs", ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)) {
|
||
|
const int page_offset = cfg::CONFIGURATOR_STANDALONE ? 88 : 106;
|
||
|
const int page_offset2 = cfg::CONFIGURATOR_STANDALONE ? 65 : 83;
|
||
|
|
||
|
if (ImGui::BeginTabItem("Buttons")) {
|
||
|
tab_selected_new = ConfigTab::CONFIG_TAB_BUTTONS;
|
||
|
ImGui::BeginChild("Buttons", ImVec2(
|
||
|
0, ImGui::GetWindowContentRegionMax().y - page_offset), false);
|
||
|
|
||
|
// game buttons
|
||
|
this->build_buttons("Game", games::get_buttons(this->games_selected_name));
|
||
|
|
||
|
// keypad buttons
|
||
|
ImGui::TextUnformatted("");
|
||
|
if (this->games_selected_name == "Beatmania IIDX") {
|
||
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8);
|
||
|
ImGui::TextColored(
|
||
|
ImVec4(1, 0.5f, 0.5f, 1.f),
|
||
|
"WARNING: Lightning Model (TDJ) I/O will ignore the keypad!");
|
||
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8);
|
||
|
ImGui::TextWrapped(
|
||
|
"Use Toggle Sub Screen button to show the overlay and use your mouse, "
|
||
|
"connect using SpiceCompanion app, or connect a touch screen to enter "
|
||
|
"the PIN.");
|
||
|
ImGui::TextUnformatted("");
|
||
|
}
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
ImGui::EndChild();
|
||
|
|
||
|
// page selector
|
||
|
this->build_page_selector(&this->buttons_page);
|
||
|
|
||
|
// bind many
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Checkbox("Bind Many", &buttons_many_active)) {
|
||
|
buttons_many_index = -1;
|
||
|
buttons_many_delay = 0;
|
||
|
}
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker("Immediately query for the next button after binding one.");
|
||
|
|
||
|
ImGui::EndTabItem();
|
||
|
}
|
||
|
if (ImGui::BeginTabItem("Analogs")) {
|
||
|
tab_selected_new = ConfigTab::CONFIG_TAB_ANALOGS;
|
||
|
ImGui::BeginChild("Analogs", ImVec2(
|
||
|
0, ImGui::GetWindowContentRegionMax().y - page_offset2), false);
|
||
|
this->build_analogs("Game", games::get_analogs(this->games_selected_name));
|
||
|
ImGui::EndChild();
|
||
|
ImGui::EndTabItem();
|
||
|
}
|
||
|
if (ImGui::BeginTabItem("Overlay")) {
|
||
|
tab_selected_new = ConfigTab::CONFIG_TAB_OVERLAY;
|
||
|
ImGui::BeginChild("Overlay", ImVec2(
|
||
|
0, ImGui::GetWindowContentRegionMax().y - page_offset), false);
|
||
|
|
||
|
// overlay buttons
|
||
|
this->build_buttons("Overlay", games::get_buttons_overlay(this->games_selected_name));
|
||
|
ImGui::EndChild();
|
||
|
|
||
|
// page selector
|
||
|
this->build_page_selector(&this->buttons_page);
|
||
|
|
||
|
// standalone configurator extras
|
||
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
||
|
ImGui::SameLine();
|
||
|
ImGui::Checkbox("Enable Overlay in Config", &OVERLAY->hotkeys_enable);
|
||
|
}
|
||
|
ImGui::EndTabItem();
|
||
|
}
|
||
|
if (ImGui::BeginTabItem("Lights")) {
|
||
|
tab_selected_new = ConfigTab::CONFIG_TAB_LIGHTS;
|
||
|
ImGui::BeginChild("Lights", ImVec2(
|
||
|
0, ImGui::GetWindowContentRegionMax().y - page_offset), false);
|
||
|
this->build_lights("Game", games::get_lights(this->games_selected_name));
|
||
|
ImGui::EndChild();
|
||
|
this->build_page_selector(&this->lights_page);
|
||
|
ImGui::EndTabItem();
|
||
|
}
|
||
|
if (ImGui::BeginTabItem("Cards")) {
|
||
|
tab_selected_new = ConfigTab::CONFIG_TAB_CARDS;
|
||
|
ImGui::BeginChild("Cards", ImVec2(
|
||
|
0, ImGui::GetWindowContentRegionMax().y - page_offset2), false);
|
||
|
this->build_cards();
|
||
|
ImGui::EndChild();
|
||
|
ImGui::EndTabItem();
|
||
|
}
|
||
|
if (ImGui::BeginTabItem("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::BeginTabItem("API")) {
|
||
|
tab_selected_new = ConfigTab::CONFIG_TAB_API;
|
||
|
|
||
|
// API options list
|
||
|
ImGui::BeginChild("ApiTab", ImVec2(
|
||
|
0, ImGui::GetWindowContentRegionMax().y - page_offset2), false);
|
||
|
auto options = games::get_options(this->games_selected_name);
|
||
|
for (auto category : launcher::get_categories(launcher::Options::OptionsCategory::API)) {
|
||
|
this->build_options(options, category);
|
||
|
}
|
||
|
ImGui::EndChild();
|
||
|
ImGui::EndTabItem();
|
||
|
}
|
||
|
if (ImGui::BeginTabItem("Options")) {
|
||
|
tab_selected_new = ConfigTab::CONFIG_TAB_OPTIONS;
|
||
|
|
||
|
// options list
|
||
|
ImGui::BeginChild("Options", ImVec2(
|
||
|
0, ImGui::GetWindowContentRegionMax().y - page_offset), false);
|
||
|
auto options = games::get_options(this->games_selected_name);
|
||
|
for (auto category : launcher::get_categories(launcher::Options::OptionsCategory::Basic)) {
|
||
|
this->build_options(options, category);
|
||
|
}
|
||
|
|
||
|
ImGui::EndChild();
|
||
|
|
||
|
// hidden options checkbox
|
||
|
ImGui::Checkbox("Show Hidden Options", &this->options_show_hidden);
|
||
|
if (!cfg::CONFIGURATOR_STANDALONE && this->options_dirty) {
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Button("Restart Game")) {
|
||
|
launcher::restart();
|
||
|
}
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker("You need to restart the game to apply the changed settings.");
|
||
|
}
|
||
|
|
||
|
ImGui::EndTabItem();
|
||
|
}
|
||
|
if (ImGui::BeginTabItem("Advanced")) {
|
||
|
tab_selected_new = ConfigTab::CONFIG_TAB_ADVANCED;
|
||
|
|
||
|
// advanced options list
|
||
|
ImGui::BeginChild("AdvancedOptions", ImVec2(
|
||
|
0, ImGui::GetWindowContentRegionMax().y - page_offset), false);
|
||
|
auto options = games::get_options(this->games_selected_name);
|
||
|
for (auto category : launcher::get_categories(launcher::Options::OptionsCategory::Advanced)) {
|
||
|
this->build_options(options, category);
|
||
|
}
|
||
|
ImGui::EndChild();
|
||
|
|
||
|
// hidden options checkbox
|
||
|
ImGui::Checkbox("Show Hidden Options", &this->options_show_hidden);
|
||
|
if (!cfg::CONFIGURATOR_STANDALONE && this->options_dirty) {
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Button("Restart Game")) {
|
||
|
launcher::restart();
|
||
|
}
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker("You need to restart the game to apply the changed settings.");
|
||
|
}
|
||
|
|
||
|
// reset configuration button
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Button("Reset Configuration")) {
|
||
|
ImGui::OpenPopup("Reset Config");
|
||
|
}
|
||
|
if (ImGui::BeginPopupModal("Reset Config", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||
|
ImGui::TextColored(ImVec4(1, 0.5f, 0.5f, 1.f),
|
||
|
"Do you really want to reset your configuration for all games?\n"
|
||
|
"Warning: This can't be reverted!");
|
||
|
if (ImGui::Button("Yes")) {
|
||
|
::Config::getInstance().createConfigFile();
|
||
|
launcher::restart();
|
||
|
}
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Button("Nope")) {
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
}
|
||
|
ImGui::EndPopup();
|
||
|
}
|
||
|
|
||
|
ImGui::EndTabItem();
|
||
|
}
|
||
|
if (ImGui::BeginTabItem("Development")) {
|
||
|
tab_selected_new = ConfigTab::CONFIG_TAB_DEV;
|
||
|
|
||
|
// dev options list
|
||
|
ImGui::BeginChild("DevOptions", ImVec2(
|
||
|
0, ImGui::GetWindowContentRegionMax().y - page_offset), false);
|
||
|
auto options = games::get_options(this->games_selected_name);
|
||
|
for (auto category : launcher::get_categories(launcher::Options::OptionsCategory::Dev)) {
|
||
|
this->build_options(options, category);
|
||
|
}
|
||
|
|
||
|
ImGui::EndChild();
|
||
|
|
||
|
// hidden options checkbox
|
||
|
ImGui::Checkbox("Show Hidden Options", &this->options_show_hidden);
|
||
|
if (!cfg::CONFIGURATOR_STANDALONE && this->options_dirty) {
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Button("Restart Game")) {
|
||
|
launcher::restart();
|
||
|
}
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker("You need to restart the game to apply the changed settings.");
|
||
|
}
|
||
|
|
||
|
ImGui::EndTabItem();
|
||
|
}
|
||
|
if (ImGui::BeginTabItem("Search")) {
|
||
|
tab_selected_new = ConfigTab::CONFIG_TAB_SEARCH;
|
||
|
|
||
|
ImGui::BeginChild("SearchOptions", ImVec2(
|
||
|
0, ImGui::GetWindowContentRegionMax().y - page_offset), false);
|
||
|
|
||
|
// search from all options
|
||
|
ImGui::Spacing();
|
||
|
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 all options
|
||
|
auto options = games::get_options(this->games_selected_name);
|
||
|
if (!this->search_filter.empty()) {
|
||
|
for (auto category : launcher::get_categories(launcher::Options::OptionsCategory::Everything)) {
|
||
|
this->build_options(
|
||
|
options,
|
||
|
category,
|
||
|
const_cast<std::string *>(&this->search_filter_in_lower_case));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ImGui::EndChild();
|
||
|
|
||
|
// hidden options checkbox
|
||
|
ImGui::Checkbox("Show Hidden Options", &this->options_show_hidden);
|
||
|
if (!cfg::CONFIGURATOR_STANDALONE && this->options_dirty) {
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Button("Restart Game")) {
|
||
|
launcher::restart();
|
||
|
}
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker("You need to restart the game to apply the changed settings.");
|
||
|
}
|
||
|
|
||
|
ImGui::EndTabItem();
|
||
|
}
|
||
|
ImGui::EndTabBar();
|
||
|
}
|
||
|
|
||
|
// did tab selection change?
|
||
|
if (this->tab_selected != tab_selected_new) {
|
||
|
this->tab_selected = tab_selected_new;
|
||
|
this->buttons_page = 0;
|
||
|
this->lights_page = 0;
|
||
|
buttons_many_active = false;
|
||
|
buttons_many_index = -1;
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
}
|
||
|
|
||
|
// disclaimer
|
||
|
ImGui::TextColored(
|
||
|
ImVec4(1, 0.5f, 0.5f, 1.f),
|
||
|
"Do NOT stream or upload game data anywhere public! Support arcades when you can. Thanks.");
|
||
|
}
|
||
|
|
||
|
void Config::build_buttons(const std::string &name, std::vector<Button> *buttons, int min, int max) {
|
||
|
ImGui::Columns(3, "ButtonsColumns", true);
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "%s Button", name.c_str());
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Binding");
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Actions");
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker("Use 'Bind' to bind a button to a device using RawInput.\n"
|
||
|
"Use 'Naive' for device independent binding using GetAsyncKeyState.");
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::Separator();
|
||
|
|
||
|
// check if empty
|
||
|
if (!buttons || buttons->empty()) {
|
||
|
ImGui::TextUnformatted("-");
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::TextUnformatted("-");
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::TextUnformatted("-");
|
||
|
ImGui::NextColumn();
|
||
|
}
|
||
|
|
||
|
// check buttons
|
||
|
if (buttons) {
|
||
|
int button_it_max = max < 0 ? buttons->size() - 1 : std::min((int) buttons->size() - 1, max);
|
||
|
for (int button_it = min; button_it <= button_it_max; button_it++) {
|
||
|
auto &button_in = buttons->at(button_it);
|
||
|
|
||
|
// get button based on page
|
||
|
auto button = &button_in;
|
||
|
auto &button_alternatives = button->getAlternatives();
|
||
|
if (this->buttons_page > 0) {
|
||
|
while ((int) button_alternatives.size() < this->buttons_page) {
|
||
|
button_alternatives.emplace_back(button->getName());
|
||
|
}
|
||
|
button = &button_alternatives.at(this->buttons_page - 1);
|
||
|
}
|
||
|
|
||
|
// get button info
|
||
|
ImGui::PushID(button);
|
||
|
auto button_name = button->getName();
|
||
|
auto button_display = button->getDisplayString(RI_MGR.get());
|
||
|
auto button_state = GameAPI::Buttons::getState(RI_MGR, *button);
|
||
|
auto button_velocity = GameAPI::Buttons::getVelocity(RI_MGR, *button);
|
||
|
|
||
|
// list entry
|
||
|
bool style_color = false;
|
||
|
if (button_state == GameAPI::Buttons::State::BUTTON_PRESSED) {
|
||
|
style_color = true;
|
||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.7f, 0.f, 1.f));
|
||
|
} else if (button_display.empty()) {
|
||
|
style_color = true;
|
||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.7f, 0.7f, 0.7f, 1.f));
|
||
|
}
|
||
|
ImGui::AlignTextToFramePadding();
|
||
|
ImGui::Text("%s", button_name.c_str());
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::AlignTextToFramePadding();
|
||
|
ImGui::Text("%s", button_display.empty() ? "None" : button_display.c_str());
|
||
|
ImGui::NextColumn();
|
||
|
if (style_color) {
|
||
|
ImGui::PopStyleColor();
|
||
|
}
|
||
|
|
||
|
// normal button binding
|
||
|
std::string bind_name = "Bind " + button_name;
|
||
|
if (ImGui::Button("Bind")
|
||
|
|| (buttons_many_active && buttons_many_active_section == name && !buttons_bind_active
|
||
|
&& !buttons_many_naive && buttons_many_index == button_it
|
||
|
&& ++buttons_many_delay > 25)) {
|
||
|
ImGui::OpenPopup(bind_name.c_str());
|
||
|
buttons_bind_active = true;
|
||
|
if (buttons_many_active) {
|
||
|
buttons_many_delay = 0;
|
||
|
buttons_many_index = button_it;
|
||
|
buttons_many_naive = false;
|
||
|
buttons_many_active_section = name;
|
||
|
}
|
||
|
|
||
|
// midi freeze
|
||
|
RI_MGR->devices_midi_freeze(true);
|
||
|
|
||
|
// reset updated devices
|
||
|
RI_MGR->devices_get_updated();
|
||
|
|
||
|
// remember start values in bind data
|
||
|
for (auto device : RI_MGR->devices_get()) {
|
||
|
switch (device.type) {
|
||
|
case rawinput::MOUSE: {
|
||
|
memcpy(device.mouseInfo->key_states_bind,
|
||
|
device.mouseInfo->key_states,
|
||
|
sizeof(device.mouseInfo->key_states_bind));
|
||
|
break;
|
||
|
}
|
||
|
case rawinput::HID: {
|
||
|
for (size_t i = 0; i < device.hidInfo->value_states.size(); i++)
|
||
|
device.hidInfo->bind_value_states[i] = device.hidInfo->value_states[i];
|
||
|
break;
|
||
|
}
|
||
|
case rawinput::MIDI: {
|
||
|
for (size_t i = 0; i < device.midiInfo->states.size(); i++)
|
||
|
device.midiInfo->bind_states[i] = device.midiInfo->states[i];
|
||
|
for (size_t i = 0; i < device.midiInfo->controls_precision.size(); i++)
|
||
|
device.midiInfo->controls_precision_bind[i] =
|
||
|
device.midiInfo->controls_precision[i];
|
||
|
for (size_t i = 0; i < device.midiInfo->controls_single.size(); i++)
|
||
|
device.midiInfo->controls_single_bind[i] = device.midiInfo->controls_single[i];
|
||
|
for (size_t i = 0; i < device.midiInfo->controls_onoff.size(); i++)
|
||
|
device.midiInfo->controls_onoff_bind[i] = device.midiInfo->controls_onoff[i];
|
||
|
break;
|
||
|
}
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (ImGui::BeginPopupModal(bind_name.c_str(), NULL,
|
||
|
ImGuiWindowFlags_AlwaysAutoResize)) {
|
||
|
|
||
|
// modal content
|
||
|
ImGui::Text("Please press any button.");
|
||
|
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Hint: Press ESC to cancel!");
|
||
|
if (ImGui::Button("Cancel")) {
|
||
|
RI_MGR->devices_midi_freeze(false);
|
||
|
buttons_bind_active = false;
|
||
|
buttons_many_index = -1;
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
} else {
|
||
|
|
||
|
// iterate updated devices
|
||
|
auto updated_devices = RI_MGR->devices_get_updated();
|
||
|
for (auto device : updated_devices) {
|
||
|
std::lock_guard<std::mutex> lock(*device->mutex);
|
||
|
switch (device->type) {
|
||
|
case rawinput::MOUSE: {
|
||
|
auto mouse = device->mouseInfo;
|
||
|
for (size_t i = 0; i < sizeof(mouse->key_states_bind); i++) {
|
||
|
if (mouse->key_states[i] && !mouse->key_states_bind[i]
|
||
|
&& !ImGui::IsItemHovered()) {
|
||
|
|
||
|
// bind key
|
||
|
button->setDeviceIdentifier(device->name);
|
||
|
button->setVKey(static_cast<unsigned short>(i));
|
||
|
button->setAnalogType(BAT_NONE);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *button, buttons_page - 1);
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
buttons_bind_active = false;
|
||
|
inc_buttons_many_index(button_it_max);
|
||
|
RI_MGR->devices_midi_freeze(false);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case rawinput::KEYBOARD: {
|
||
|
auto kb = device->keyboardInfo;
|
||
|
for (unsigned short vkey = 0; vkey < 1024; vkey++) {
|
||
|
|
||
|
// check if key is down
|
||
|
if (vkey != VK_NUMLOCK && kb->key_states[vkey]) {
|
||
|
|
||
|
// cancel on escape key
|
||
|
if (vkey == VK_ESCAPE) {
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
buttons_bind_active = false;
|
||
|
buttons_many_index = -1;
|
||
|
RI_MGR->devices_midi_freeze(false);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// bind key
|
||
|
button->setDeviceIdentifier(device->name);
|
||
|
button->setVKey(vkey);
|
||
|
button->setAnalogType(BAT_NONE);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *button, buttons_page - 1);
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
buttons_bind_active = false;
|
||
|
inc_buttons_many_index(button_it_max);
|
||
|
RI_MGR->devices_midi_freeze(false);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case rawinput::HID: {
|
||
|
auto hid = device->hidInfo;
|
||
|
|
||
|
// ignore touchscreen and digitizer button inputs
|
||
|
// digitizer has funky stuff like "Touch Valid" "Data Valid" always held high
|
||
|
if (!rawinput::touch::is_touchscreen(device) &&
|
||
|
hid->caps.UsagePage != 0x0D) {
|
||
|
|
||
|
// button caps
|
||
|
auto button_states_list = &hid->button_states;
|
||
|
size_t button_index = 0;
|
||
|
for (auto &button_states : *button_states_list) {
|
||
|
for (size_t i = 0; i < button_states.size(); i++) {
|
||
|
|
||
|
// check if button is down
|
||
|
if (button_states[i]) {
|
||
|
|
||
|
// bind key
|
||
|
button->setDeviceIdentifier(device->name);
|
||
|
button->setVKey(static_cast<unsigned short>(button_index + i));
|
||
|
button->setAnalogType(BAT_NONE);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *button,
|
||
|
buttons_page - 1);
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
buttons_bind_active = false;
|
||
|
inc_buttons_many_index(button_it_max);
|
||
|
RI_MGR->devices_midi_freeze(false);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
button_index += button_states.size();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// value caps
|
||
|
auto value_states = &hid->value_states;
|
||
|
auto bind_value_states = &hid->bind_value_states;
|
||
|
auto value_names = &hid->value_caps_names;
|
||
|
for (size_t i = 0; i < value_states->size(); i++) {
|
||
|
auto &state = value_states->at(i);
|
||
|
auto &bind_state = bind_value_states->at(i);
|
||
|
auto &value_name = value_names->at(i);
|
||
|
|
||
|
// check for valid axis names
|
||
|
if (value_name == "X" ||
|
||
|
value_name == "Y" ||
|
||
|
value_name == "Rx" ||
|
||
|
value_name == "Ry" ||
|
||
|
value_name == "Z")
|
||
|
{
|
||
|
// check if axis is in activation area
|
||
|
float normalized = (state - 0.5f) * 2.f;
|
||
|
float diff = std::fabs(state - bind_state);
|
||
|
if (std::fabs(normalized) > 0.9f && diff > 0.1f) {
|
||
|
auto bat = normalized > 0 ? BAT_POSITIVE : BAT_NEGATIVE;
|
||
|
|
||
|
// bind value
|
||
|
button->setDeviceIdentifier(device->name);
|
||
|
button->setVKey(static_cast<unsigned short>(i));
|
||
|
button->setAnalogType(bat);
|
||
|
button->setDebounceUp(0.0);
|
||
|
button->setDebounceDown(0.0);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *button,
|
||
|
buttons_page - 1);
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
buttons_bind_active = false;
|
||
|
inc_buttons_many_index(button_it_max);
|
||
|
RI_MGR->devices_midi_freeze(false);
|
||
|
break;
|
||
|
|
||
|
} else if (diff > 0.3f) {
|
||
|
bind_state = state;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// hat switch
|
||
|
if (value_name == "Hat switch") {
|
||
|
|
||
|
// get hat switch values
|
||
|
ButtonAnalogType buffer[3], buffer_bind[3];
|
||
|
Button::getHatSwitchValues(state, buffer);
|
||
|
Button::getHatSwitchValues(bind_state, buffer_bind);
|
||
|
|
||
|
// check the first entry only
|
||
|
if (buffer[0] != BAT_NONE && buffer[0] != buffer_bind[0]) {
|
||
|
|
||
|
// bind value
|
||
|
button->setDeviceIdentifier(device->name);
|
||
|
button->setVKey(static_cast<unsigned short>(i));
|
||
|
button->setAnalogType(buffer[0]);
|
||
|
button->setDebounceUp(0.0);
|
||
|
button->setDebounceDown(0.0);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *button,
|
||
|
buttons_page - 1);
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
buttons_bind_active = false;
|
||
|
inc_buttons_many_index(button_it_max);
|
||
|
RI_MGR->devices_midi_freeze(false);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case rawinput::MIDI: {
|
||
|
auto midi = device->midiInfo;
|
||
|
|
||
|
// iterate all 128 notes on 16 channels
|
||
|
for (unsigned short index = 0; index < 16 * 128; index++) {
|
||
|
|
||
|
// check if note is down
|
||
|
if (midi->states[index]) {
|
||
|
|
||
|
// check if it wasn't down before
|
||
|
if (!midi->bind_states[index]) {
|
||
|
|
||
|
// bind key
|
||
|
button->setDeviceIdentifier(device->name);
|
||
|
button->setVKey(index);
|
||
|
button->setAnalogType(BAT_NONE);
|
||
|
button->setDebounceUp(0.0);
|
||
|
button->setDebounceDown(0.0);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *button,
|
||
|
buttons_page - 1);
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
buttons_bind_active = false;
|
||
|
inc_buttons_many_index(button_it_max);
|
||
|
RI_MGR->devices_midi_freeze(false);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
} else {
|
||
|
|
||
|
// note was on when dialog opened, is now off
|
||
|
midi->bind_states[index] = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// check precision controls
|
||
|
for (unsigned short index = 0; index < 16 * 32; index++) {
|
||
|
if (midi->controls_precision[index] > 0) {
|
||
|
if (midi->controls_precision_bind[index] == 0) {
|
||
|
|
||
|
// bind control
|
||
|
button->setDeviceIdentifier(device->name);
|
||
|
button->setVKey(index);
|
||
|
button->setAnalogType(BAT_MIDI_CTRL_PRECISION);
|
||
|
button->setDebounceUp(0.0);
|
||
|
button->setDebounceDown(0.0);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *button,
|
||
|
buttons_page - 1);
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
buttons_bind_active = false;
|
||
|
inc_buttons_many_index(button_it_max);
|
||
|
RI_MGR->devices_midi_freeze(false);
|
||
|
break;
|
||
|
}
|
||
|
} else {
|
||
|
midi->controls_precision_bind[index] = 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// check single controls
|
||
|
for (unsigned short index = 0; index < 16 * 44; index++) {
|
||
|
if (midi->controls_single[index] > 0) {
|
||
|
if (midi->controls_single_bind[index] == 0) {
|
||
|
|
||
|
// bind control
|
||
|
button->setDeviceIdentifier(device->name);
|
||
|
button->setVKey(index);
|
||
|
button->setAnalogType(BAT_MIDI_CTRL_SINGLE);
|
||
|
button->setDebounceUp(0.0);
|
||
|
button->setDebounceDown(0.0);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *button,
|
||
|
buttons_page - 1);
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
buttons_bind_active = false;
|
||
|
inc_buttons_many_index(button_it_max);
|
||
|
RI_MGR->devices_midi_freeze(false);
|
||
|
break;
|
||
|
}
|
||
|
} else {
|
||
|
midi->controls_single_bind[index] = 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// check on/off controls
|
||
|
for (unsigned short index = 0; index < 16 * 6; index++) {
|
||
|
if (midi->controls_onoff[index]) {
|
||
|
if (!midi->controls_onoff_bind[index]) {
|
||
|
|
||
|
// bind control
|
||
|
button->setDeviceIdentifier(device->name);
|
||
|
button->setVKey(index);
|
||
|
button->setAnalogType(BAT_MIDI_CTRL_ONOFF);
|
||
|
button->setDebounceUp(0.0);
|
||
|
button->setDebounceDown(0.0);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *button,
|
||
|
buttons_page - 1);
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
buttons_bind_active = false;
|
||
|
inc_buttons_many_index(button_it_max);
|
||
|
RI_MGR->devices_midi_freeze(false);
|
||
|
break;
|
||
|
}
|
||
|
} else {
|
||
|
midi->controls_onoff_bind[index] = 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// check pitch bend down
|
||
|
if (midi->pitch_bend < 0x2000) {
|
||
|
|
||
|
// bind control
|
||
|
button->setDeviceIdentifier(device->name);
|
||
|
button->setVKey(0);
|
||
|
button->setAnalogType(BAT_MIDI_PITCH_DOWN);
|
||
|
button->setDebounceUp(0.0);
|
||
|
button->setDebounceDown(0.0);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *button,
|
||
|
buttons_page - 1);
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
buttons_bind_active = false;
|
||
|
inc_buttons_many_index(button_it_max);
|
||
|
RI_MGR->devices_midi_freeze(false);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// check pitch bend up
|
||
|
if (midi->pitch_bend > 0x2000) {
|
||
|
|
||
|
// bind control
|
||
|
button->setDeviceIdentifier(device->name);
|
||
|
button->setVKey(0);
|
||
|
button->setAnalogType(BAT_MIDI_PITCH_UP);
|
||
|
button->setDebounceUp(0.0);
|
||
|
button->setDebounceDown(0.0);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *button,
|
||
|
buttons_page - 1);
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
buttons_bind_active = false;
|
||
|
inc_buttons_many_index(button_it_max);
|
||
|
RI_MGR->devices_midi_freeze(false);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
case rawinput::PIUIO_DEVICE: {
|
||
|
auto piuio_dev = device->piuioDev;
|
||
|
|
||
|
// iterate all PIUIO inputs
|
||
|
for (int i = 0; i < rawinput::PIUIO::PIUIO_MAX_NUM_OF_INPUTS; i++) {
|
||
|
|
||
|
// check for down event
|
||
|
if (piuio_dev->IsPressed(i) && !piuio_dev->WasPressed(i)) {
|
||
|
|
||
|
// bind key
|
||
|
button->setDeviceIdentifier(device->name);
|
||
|
button->setVKey(i);
|
||
|
button->setAnalogType(BAT_NONE);
|
||
|
button->setDebounceUp(0.0);
|
||
|
button->setDebounceDown(0.0);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *button,
|
||
|
buttons_page - 1);
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
buttons_bind_active = false;
|
||
|
inc_buttons_many_index(button_it_max);
|
||
|
RI_MGR->devices_midi_freeze(false);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// clean up
|
||
|
ImGui::EndPopup();
|
||
|
}
|
||
|
|
||
|
// naive binding
|
||
|
ImGui::SameLine();
|
||
|
std::string naive_string = "Naive " + button_name;
|
||
|
if (ImGui::Button("Naive")
|
||
|
|| (buttons_many_active && buttons_many_active_section == name && !buttons_bind_active
|
||
|
&& buttons_many_naive && buttons_many_index == button_it
|
||
|
&& ++buttons_many_delay > 25)) {
|
||
|
ImGui::OpenPopup(naive_string.c_str());
|
||
|
if (buttons_many_active) {
|
||
|
buttons_many_index = button_it;
|
||
|
buttons_many_naive = true;
|
||
|
buttons_many_delay = 0;
|
||
|
buttons_many_active_section = name;
|
||
|
}
|
||
|
|
||
|
// grab current keyboard state
|
||
|
for (unsigned short int i = 0x01; i < 0xFF; i++) {
|
||
|
buttons_keyboard_state[i] = GetAsyncKeyState(i) != 0;
|
||
|
}
|
||
|
}
|
||
|
if (ImGui::BeginPopupModal(naive_string.c_str(), NULL,
|
||
|
ImGuiWindowFlags_AlwaysAutoResize)) {
|
||
|
buttons_bind_active = true;
|
||
|
|
||
|
// modal content
|
||
|
ImGui::Text("Please press any button.");
|
||
|
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Hint: Press ESC to cancel!");
|
||
|
if (ImGui::Button("Cancel")) {
|
||
|
buttons_bind_active = false;
|
||
|
buttons_many_index = -1;
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
} else {
|
||
|
|
||
|
// get new keyboard state
|
||
|
bool keyboard_state_new[sizeof(buttons_keyboard_state)];
|
||
|
for (size_t i = 0; i < sizeof(keyboard_state_new); i++) {
|
||
|
keyboard_state_new[i] = GetAsyncKeyState(i) != 0;
|
||
|
}
|
||
|
|
||
|
// detect key presses
|
||
|
for (unsigned short int vKey = 0x01; vKey < sizeof(buttons_keyboard_state); vKey++) {
|
||
|
|
||
|
// ignore num lock escape sequence
|
||
|
if (vKey != VK_NUMLOCK) {
|
||
|
|
||
|
// check for key press
|
||
|
if (keyboard_state_new[vKey] && !buttons_keyboard_state[vKey]) {
|
||
|
|
||
|
// ignore escape
|
||
|
if (vKey != VK_ESCAPE && (vKey != VK_LBUTTON || !ImGui::IsItemHovered())) {
|
||
|
|
||
|
// bind key
|
||
|
button->setDeviceIdentifier("");
|
||
|
button->setVKey(vKey);
|
||
|
button->setDebounceUp(0.0);
|
||
|
button->setDebounceDown(0.0);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *button,
|
||
|
buttons_page - 1);
|
||
|
inc_buttons_many_index(button_it_max);
|
||
|
} else {
|
||
|
buttons_many_index = -1;
|
||
|
}
|
||
|
|
||
|
buttons_bind_active = false;
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// clean up
|
||
|
memcpy(buttons_keyboard_state, keyboard_state_new, sizeof(buttons_keyboard_state));
|
||
|
}
|
||
|
ImGui::EndPopup();
|
||
|
}
|
||
|
|
||
|
// edit button
|
||
|
ImGui::SameLine();
|
||
|
std::string edit_name = "Edit " + button->getName();
|
||
|
if (ImGui::Button("Edit")) {
|
||
|
ImGui::OpenPopup(edit_name.c_str());
|
||
|
}
|
||
|
if (ImGui::BeginPopupModal(edit_name.c_str(), NULL, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||
|
bool dirty = false;
|
||
|
|
||
|
// binding
|
||
|
ImGui::Text("Binding");
|
||
|
|
||
|
// device identifier
|
||
|
auto device_id = button->getDeviceIdentifier();
|
||
|
if (device_id.empty()) device_id = "Empty (Naive)";
|
||
|
if (ImGui::BeginCombo("Device Identifier", device_id.c_str())) {
|
||
|
bool empty_selected = device_id == "Empty (Naive)";
|
||
|
if (ImGui::Selectable("Empty (Naive)", empty_selected)) {
|
||
|
button->setDeviceIdentifier("");
|
||
|
dirty = true;
|
||
|
}
|
||
|
if (empty_selected) {
|
||
|
ImGui::SetItemDefaultFocus();
|
||
|
}
|
||
|
for (auto &device : RI_MGR->devices_get()) {
|
||
|
bool selected = device_id == device.desc.c_str();
|
||
|
if (ImGui::Selectable(device.desc.c_str(), selected)) {
|
||
|
button->setDeviceIdentifier(device.desc);
|
||
|
dirty = true;
|
||
|
}
|
||
|
if (selected) {
|
||
|
ImGui::SetItemDefaultFocus();
|
||
|
}
|
||
|
}
|
||
|
ImGui::EndCombo();
|
||
|
}
|
||
|
|
||
|
// analog type
|
||
|
auto bat = button->getAnalogType();
|
||
|
if (ImGui::BeginCombo("Analog Type", ButtonAnalogTypeStr[bat])) {
|
||
|
for (int i = 0; i <= 16; i++) {
|
||
|
bool selected = (int) bat == i;
|
||
|
if (ImGui::Selectable(ButtonAnalogTypeStr[i], selected)) {
|
||
|
button->setAnalogType((ButtonAnalogType) i);
|
||
|
dirty = true;
|
||
|
}
|
||
|
if (selected) {
|
||
|
ImGui::SetItemDefaultFocus();
|
||
|
}
|
||
|
}
|
||
|
ImGui::EndCombo();
|
||
|
}
|
||
|
|
||
|
// virtual key
|
||
|
int vKey = button->getVKey();
|
||
|
if (ImGui::InputInt(device_id == "Empty (Naive)" ? "Virtual Key" : "Index", &vKey, 1, 1)) {
|
||
|
button->setVKey(vKey);
|
||
|
}
|
||
|
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||
|
dirty = true;
|
||
|
}
|
||
|
|
||
|
// preview
|
||
|
if (!button_display.empty()) {
|
||
|
ImGui::TextUnformatted("\nPreview");
|
||
|
ImGui::Separator();
|
||
|
ImGui::Text("%s\n\n", button_display.c_str());
|
||
|
} else {
|
||
|
ImGui::TextUnformatted("");
|
||
|
}
|
||
|
|
||
|
// options
|
||
|
ImGui::Text("Options");
|
||
|
|
||
|
// check for debounce
|
||
|
auto device = RI_MGR->devices_get(button->getDeviceIdentifier());
|
||
|
if (button->getDebounceUp() || button->getDebounceDown()
|
||
|
|| (device != nullptr && (
|
||
|
device->type == rawinput::MOUSE ||
|
||
|
device->type == rawinput::KEYBOARD ||
|
||
|
(device->type == rawinput::HID && button->getAnalogType() == BAT_NONE)
|
||
|
))) {
|
||
|
|
||
|
// debounce up
|
||
|
auto debounce_up = button->getDebounceUp() * 1000;
|
||
|
if (ImGui::InputDouble("Debounce Up (ms)", &debounce_up, 1, 1, "%.2f")) {
|
||
|
debounce_up = std::max(0.0, debounce_up);
|
||
|
button->setDebounceUp(debounce_up * 0.001);
|
||
|
}
|
||
|
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||
|
dirty = true;
|
||
|
}
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker("Time a button needs to be up to be detected as up.\n"
|
||
|
"Can solve micro switch issues with long notes for example.");
|
||
|
|
||
|
// debounce down
|
||
|
auto debounce_down = button->getDebounceDown() * 1000;
|
||
|
if (ImGui::InputDouble("Debounce Down (ms)", &debounce_down, 1, 1, "%.2f")) {
|
||
|
debounce_down = std::max(0.0, debounce_down);
|
||
|
button->setDebounceDown(debounce_down * 0.001);
|
||
|
}
|
||
|
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||
|
dirty = true;
|
||
|
}
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker("Time a button needs to be down to be detected as down.\n"
|
||
|
"This setting will add noticable input lag.");
|
||
|
}
|
||
|
|
||
|
// invert
|
||
|
bool invert = button->getInvert();
|
||
|
if (ImGui::Checkbox("Invert", &invert)) {
|
||
|
button->setInvert(invert);
|
||
|
dirty = true;
|
||
|
}
|
||
|
|
||
|
// state display
|
||
|
ImGui::TextUnformatted("");
|
||
|
ImGui::Text("State");
|
||
|
ImGui::ProgressBar(button_velocity);
|
||
|
|
||
|
// check if dirty
|
||
|
if (dirty) {
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *button, buttons_page - 1);
|
||
|
}
|
||
|
|
||
|
// close button
|
||
|
ImGui::TextUnformatted("");
|
||
|
if (ImGui::Button("Save & Close")) {
|
||
|
buttons_many_active = false;
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
}
|
||
|
ImGui::EndPopup();
|
||
|
}
|
||
|
|
||
|
// clear button
|
||
|
if (button_display.size() > 0) {
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Button("Clear")) {
|
||
|
button->setDeviceIdentifier("");
|
||
|
button->setVKey(0xFF);
|
||
|
button->setAnalogType(BAT_NONE);
|
||
|
button->setDebounceUp(0.0);
|
||
|
button->setDebounceDown(0.0);
|
||
|
button->setInvert(false);
|
||
|
button->setLastState(GameAPI::Buttons::BUTTON_NOT_PRESSED);
|
||
|
button->setLastVelocity(0);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *button,
|
||
|
buttons_page - 1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// clean up
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::PopID();
|
||
|
}
|
||
|
}
|
||
|
ImGui::Columns();
|
||
|
}
|
||
|
|
||
|
void Config::build_analogs(const std::string &name, std::vector<Analog> *analogs) {
|
||
|
ImGui::Columns(3, "AnalogsColumns", true);
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "%s Analog", name.c_str()); ImGui::NextColumn();
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Binding"); ImGui::NextColumn();
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Actions"); ImGui::NextColumn();
|
||
|
ImGui::Separator();
|
||
|
|
||
|
// check if empty
|
||
|
if (!analogs || analogs->empty()) {
|
||
|
ImGui::TextUnformatted("-");
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::TextUnformatted("-");
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::TextUnformatted("-");
|
||
|
ImGui::NextColumn();
|
||
|
}
|
||
|
|
||
|
// check analogs
|
||
|
if (analogs) {
|
||
|
for (auto &analog : *analogs) {
|
||
|
|
||
|
// get analog info
|
||
|
ImGui::PushID(&analog);
|
||
|
auto analog_name = analog.getName();
|
||
|
auto analog_display = analog.getDisplayString(RI_MGR.get());
|
||
|
auto analog_state = GameAPI::Analogs::getState(RI_MGR, analog);
|
||
|
|
||
|
// list entry
|
||
|
ImGui::ProgressBar(analog_state, ImVec2(32.f, 0));
|
||
|
ImGui::SameLine();
|
||
|
if (analog_display.empty()) {
|
||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.7f, 0.7f, 0.7f, 1.f));
|
||
|
}
|
||
|
ImGui::Text("%s", analog_name.c_str());
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::AlignTextToFramePadding();
|
||
|
ImGui::Text("%s", analog_display.empty() ? "None" : analog_display.c_str());
|
||
|
ImGui::NextColumn();
|
||
|
if (analog_display.empty()) {
|
||
|
ImGui::PopStyleColor();
|
||
|
}
|
||
|
|
||
|
// analog binding
|
||
|
if (ImGui::Button("Bind")) {
|
||
|
ImGui::OpenPopup("Analog Binding");
|
||
|
|
||
|
// get devices
|
||
|
this->analogs_devices.clear();
|
||
|
for (auto &device : RI_MGR->devices_get()) {
|
||
|
switch (device.type) {
|
||
|
case rawinput::MOUSE:
|
||
|
this->analogs_devices.emplace_back(&device);
|
||
|
break;
|
||
|
case rawinput::HID:
|
||
|
if (!device.hidInfo->value_caps_names.empty())
|
||
|
this->analogs_devices.emplace_back(&device);
|
||
|
break;
|
||
|
case rawinput::MIDI:
|
||
|
this->analogs_devices.emplace_back(&device);
|
||
|
break;
|
||
|
default:
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// check if this is the current device
|
||
|
if (device.name == analog.getDeviceIdentifier()) {
|
||
|
analogs_devices_selected = this->analogs_devices.size() - 1;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (ImGui::BeginPopupModal("Analog Binding", NULL,
|
||
|
ImGuiWindowFlags_AlwaysAutoResize)) {
|
||
|
|
||
|
// device selector
|
||
|
auto analog_device_changed = ImGui::Combo(
|
||
|
"Device",
|
||
|
&this->analogs_devices_selected,
|
||
|
[](void* data, int i, const char **item) {
|
||
|
*item = ((std::vector<rawinput::Device*>*) data)->at(i)->desc.c_str();
|
||
|
return true;
|
||
|
},
|
||
|
&this->analogs_devices, (int) this->analogs_devices.size());
|
||
|
|
||
|
// obtain controls
|
||
|
std::vector<std::string> control_names;
|
||
|
std::vector<int> analogs_midi_indices;
|
||
|
if (this->analogs_devices_selected >= 0) {
|
||
|
auto device = this->analogs_devices.at(this->analogs_devices_selected);
|
||
|
switch (device->type) {
|
||
|
case rawinput::MOUSE: {
|
||
|
|
||
|
// add X/Y axis and mouse wheel
|
||
|
control_names.push_back("X");
|
||
|
control_names.push_back("Y");
|
||
|
control_names.push_back("Scroll Wheel");
|
||
|
break;
|
||
|
}
|
||
|
case rawinput::HID: {
|
||
|
|
||
|
// add value names
|
||
|
for (auto &analog_name : device->hidInfo->value_caps_names) {
|
||
|
control_names.push_back(analog_name);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case rawinput::MIDI: {
|
||
|
|
||
|
// add pitch bend
|
||
|
control_names.push_back("Pitch Bend");
|
||
|
analogs_midi_indices.push_back(device->midiInfo->controls_precision.size()
|
||
|
+ device->midiInfo->controls_single.size()
|
||
|
+ device->midiInfo->controls_onoff.size()
|
||
|
);
|
||
|
|
||
|
// add precision values
|
||
|
auto precision = device->midiInfo->controls_precision;
|
||
|
for (size_t i = 0; i < precision.size(); i++) {
|
||
|
if (device->midiInfo->controls_precision_set[i]) {
|
||
|
control_names.push_back("PREC 0x" + bin2hex((uint8_t) i));
|
||
|
analogs_midi_indices.push_back(i);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// add single values
|
||
|
auto single = device->midiInfo->controls_single;
|
||
|
for (size_t i = 0; i < single.size(); i++) {
|
||
|
if (device->midiInfo->controls_single_set[i]) {
|
||
|
control_names.push_back("CTRL 0x" + bin2hex((uint8_t) i));
|
||
|
analogs_midi_indices.push_back(i + device->midiInfo->controls_precision.size());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// add onoff values
|
||
|
auto onoff = device->midiInfo->controls_onoff;
|
||
|
for (size_t i = 0; i < onoff.size(); i++) {
|
||
|
if (device->midiInfo->controls_onoff_set[i]) {
|
||
|
control_names.push_back("ONOFF 0x" + bin2hex((uint8_t) i));
|
||
|
analogs_midi_indices.push_back(i
|
||
|
+ device->midiInfo->controls_precision.size()
|
||
|
+ device->midiInfo->controls_single.size());
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// select the previously chosen value
|
||
|
auto selected_control = 0;
|
||
|
if (!analog_device_changed) {
|
||
|
if (analogs_midi_indices.empty()) {
|
||
|
selected_control = analog.getIndex();
|
||
|
} else {
|
||
|
for (size_t i = 0; i < analogs_midi_indices.size(); i++) {
|
||
|
if (analog.getIndex() == analogs_midi_indices.at(i)) {
|
||
|
selected_control = i;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (0 <= selected_control && selected_control < static_cast<int>(control_names.size())) {
|
||
|
this->analogs_devices_control_selected = selected_control;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// controls
|
||
|
ImGui::Combo("Control",
|
||
|
&this->analogs_devices_control_selected,
|
||
|
[](void* data, int i, const char **item) {
|
||
|
*item = ((std::vector<std::string>*) data)->at(i).c_str();
|
||
|
return true;
|
||
|
},
|
||
|
&control_names, control_names.size());
|
||
|
|
||
|
// multiplier/sensitivity/deadzone
|
||
|
if (this->analogs_devices_selected >= 0) {
|
||
|
auto device = this->analogs_devices.at(this->analogs_devices_selected);
|
||
|
if (device->type == rawinput::HID) {
|
||
|
auto multiplier = analog.getMultiplier();
|
||
|
int multiplier_index = 7; // 1:1
|
||
|
if (multiplier < -1) {
|
||
|
// turn -8 into [0], -2 into [6], and so on
|
||
|
multiplier_index = multiplier + 8;
|
||
|
} else if (1 < multiplier) {
|
||
|
// turn 2 into [8], 8 into [14], etc
|
||
|
multiplier_index = multiplier + 6;
|
||
|
}
|
||
|
const bool value_changed = ImGui::Combo(
|
||
|
"Multiplier",
|
||
|
&multiplier_index,
|
||
|
"1/8\0" // [0]
|
||
|
"1/7\0" // [1]
|
||
|
"1/6\0" // [2]
|
||
|
"1/5\0" // [3]
|
||
|
"1/4\0" // [4]
|
||
|
"1/3\0" // [5]
|
||
|
"1/2\0" // [6]
|
||
|
"1:1\0" // [7] <-- default
|
||
|
"2x\0" // [8]
|
||
|
"3x\0" // [9]
|
||
|
"4x\0" // [10]
|
||
|
"5x\0" // [11]
|
||
|
"6x\0" // [12]
|
||
|
"7x\0" // [13]
|
||
|
"8x\0" // [14]
|
||
|
"\0");
|
||
|
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker("Apply a static integer multiplier or divisor to the input.");
|
||
|
if (value_changed) {
|
||
|
if (multiplier_index < 7) {
|
||
|
// turn [0] into -8, [6] to -2, etc
|
||
|
multiplier = multiplier_index - 8;
|
||
|
} else if (7 < multiplier_index) {
|
||
|
// turn [8] to 2, [14] to 8, etc
|
||
|
multiplier = multiplier_index - 6;
|
||
|
} else {
|
||
|
multiplier = 1;
|
||
|
}
|
||
|
analog.setMultiplier(multiplier);
|
||
|
}
|
||
|
}
|
||
|
if (device->type == rawinput::MOUSE || device->type == rawinput::HID) {
|
||
|
auto sensitivity = sqrtf(analog.getSensitivity());
|
||
|
const bool value_changed =
|
||
|
ImGui::SliderFloat("Sensitivity", &sensitivity, 0.f, 2.f, "%.3f");
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker(
|
||
|
"Adjust floating point multiplier to relative movement.\n\n"
|
||
|
"Value is squared before being multiplied (e.g., 1.44 is 2x sensitivity, 2.00 is 4x).\n\n"
|
||
|
"Dependent on how often the game polls for input. Intended for angular input (knobs, turntables)");
|
||
|
if (value_changed) {
|
||
|
analog.setSensitivity(sensitivity * sensitivity);
|
||
|
}
|
||
|
}
|
||
|
if (device->type == rawinput::HID || device->type == rawinput::MIDI) {
|
||
|
auto deadzone = analog.getDeadzone();
|
||
|
const bool value_changed =
|
||
|
ImGui::SliderFloat("Deadzone", &deadzone, -0.999f, 0.999f, "%.3f");
|
||
|
if (value_changed) {
|
||
|
analog.setDeadzone(deadzone);
|
||
|
}
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker("Positive values specify a deadzone around the middle.\n"
|
||
|
"Negative values specify a deadzone from the minimum value.");
|
||
|
|
||
|
// deadzone mirror
|
||
|
bool deadzone_mirror = analog.getDeadzoneMirror();
|
||
|
ImGui::Checkbox("Deadzone Mirror", &deadzone_mirror);
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker("Positive deadzone values cut off at edges instead.\n"
|
||
|
"Negative deadzone values cut off at maximum value instead.");
|
||
|
if (deadzone_mirror != analog.getDeadzoneMirror()) {
|
||
|
analog.setDeadzoneMirror(deadzone_mirror);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// invert axis
|
||
|
bool invert = analog.getInvert();
|
||
|
ImGui::Checkbox("Invert Axis", &invert);
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker("Flip the direction of analog input.");
|
||
|
if (invert != analog.getInvert()) {
|
||
|
analog.setInvert(invert);
|
||
|
}
|
||
|
|
||
|
if (this->analogs_devices_selected >= 0) {
|
||
|
const auto device = this->analogs_devices.at(this->analogs_devices_selected);
|
||
|
if (device->type == rawinput::HID) {
|
||
|
// smoothing
|
||
|
bool smoothing = analog.getSmoothing();
|
||
|
ImGui::BeginDisabled(analog.isRelativeMode());
|
||
|
ImGui::Checkbox("Smooth Axis (adds latency)", &smoothing);
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker(
|
||
|
"Apply a moving average algorithm; intended for angular input (knobs, turntables). "
|
||
|
"Adds a slight bit of latency to input as the algorithm averages out recent input. "
|
||
|
"Only use in dire situations where the input is too jittery for the game.");
|
||
|
ImGui::EndDisabled();
|
||
|
if (smoothing != analog.getSmoothing()) {
|
||
|
analog.setSmoothing(smoothing);
|
||
|
}
|
||
|
|
||
|
// relative input mode
|
||
|
bool relative_analog = analog.isRelativeMode();
|
||
|
ImGui::Checkbox("Relative Axis (experimental)", &relative_analog);
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker(
|
||
|
"Use relative directional input instead of positional values.\n\n"
|
||
|
"Can be used to translate analog sticks to knob input, for example.\n\n"
|
||
|
"WARNING: speed depends on how often the game polls for input! "
|
||
|
"Strongly recommended that you go into the game's test menu instead "
|
||
|
"of adjusting in spicecfg.");
|
||
|
if (relative_analog != analog.isRelativeMode()) {
|
||
|
analog.setRelativeMode(relative_analog);
|
||
|
}
|
||
|
|
||
|
// delay buffer
|
||
|
int delay = analog.getDelayBufferDepth();
|
||
|
ImGui::InputInt("Delay (experimental)", &delay, 1, 10);
|
||
|
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||
|
delay = CLAMP(delay, 0, 256);
|
||
|
analog.setDelayBufferDepth(delay);
|
||
|
}
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker(
|
||
|
"Adds a delay to input. This is poll-based, not time-based.\n\n"
|
||
|
"WARNING: delay depends on how often the game polls for input! "
|
||
|
"Strongly recommended that you go into the game's test menu instead "
|
||
|
"of adjusting in spicecfg.");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// current state
|
||
|
ImGui::Separator();
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.7f, 0.f, 1.f), "Preview");
|
||
|
float value = GameAPI::Analogs::getState(RI_MGR, analog);
|
||
|
ImGui::ProgressBar(value);
|
||
|
|
||
|
// centered knob preview
|
||
|
const float knob_size = 64.f;
|
||
|
auto width = ImGui::GetContentRegionAvail().x - knob_size;
|
||
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (width / 2));
|
||
|
ImGui::Knob(value, knob_size);
|
||
|
|
||
|
// update analog
|
||
|
if (analogs_devices_selected >= 0 && analogs_devices_selected < (int) analogs_devices.size()) {
|
||
|
|
||
|
// update identifier on change
|
||
|
auto identifier = this->analogs_devices.at(this->analogs_devices_selected)->name;
|
||
|
if (identifier != analog.getDeviceIdentifier()) {
|
||
|
analog.setDeviceIdentifier(identifier);
|
||
|
}
|
||
|
|
||
|
// update control
|
||
|
if (this->analogs_devices_control_selected >= 0) {
|
||
|
|
||
|
// MIDI devices have their own dynamic indices
|
||
|
auto index = this->analogs_devices_control_selected;
|
||
|
if (!analogs_midi_indices.empty()) {
|
||
|
if (this->analogs_devices_control_selected < (int) analogs_midi_indices.size()) {
|
||
|
index = analogs_midi_indices[this->analogs_devices_control_selected];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// update index on change
|
||
|
if ((int) analog.getIndex() != index) {
|
||
|
analog.setIndex(index);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// close button
|
||
|
ImGui::Separator();
|
||
|
if (ImGui::Button("Save & Close")) {
|
||
|
::Config::getInstance().updateBinding(games_list[games_selected], analog);
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
}
|
||
|
|
||
|
// clean up
|
||
|
ImGui::EndPopup();
|
||
|
}
|
||
|
|
||
|
// clear analog
|
||
|
if (analog_display.size() > 0) {
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Button("Clear")) {
|
||
|
analog.clearBindings();
|
||
|
analog.setLastState(0.f);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], analog);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// clean up
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::PopID();
|
||
|
}
|
||
|
}
|
||
|
ImGui::Columns();
|
||
|
}
|
||
|
|
||
|
void Config::build_lights(const std::string &name, std::vector<Light> *lights) {
|
||
|
ImGui::Columns(3, "LightsColumns", true);
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Name"); ImGui::NextColumn();
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Binding"); ImGui::NextColumn();
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Actions"); ImGui::SameLine();
|
||
|
ImGui::HelpMarker("Use 'Bind' to redirect cabinet light outputs to HID-compatible value output devices.");
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::Separator();
|
||
|
|
||
|
// check if empty
|
||
|
if (!lights || lights->empty()) {
|
||
|
ImGui::TextUnformatted("-");
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::TextUnformatted("-");
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::TextUnformatted("-");
|
||
|
ImGui::NextColumn();
|
||
|
}
|
||
|
|
||
|
// check lights
|
||
|
if (lights) {
|
||
|
for (auto &light_in : *lights) {
|
||
|
|
||
|
// get light based on page
|
||
|
auto light = &light_in;
|
||
|
auto &light_alternatives = light->getAlternatives();
|
||
|
if (this->lights_page > 0) {
|
||
|
while ((int) light_alternatives.size() < this->lights_page) {
|
||
|
light_alternatives.emplace_back(light->getName());
|
||
|
}
|
||
|
light = &light_alternatives.at(this->lights_page - 1);
|
||
|
}
|
||
|
|
||
|
// get light info
|
||
|
auto light_name = light->getName();
|
||
|
auto light_display = light->getDisplayString(RI_MGR.get());
|
||
|
auto light_state = GameAPI::Lights::readLight(RI_MGR, *light);
|
||
|
|
||
|
// list entry
|
||
|
if (light_display.empty()) {
|
||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.7f, 0.7f, 0.7f, 1.f));
|
||
|
}
|
||
|
ImGui::PushID(light);
|
||
|
ImGui::ProgressBar(light_state, ImVec2(32.f, 0));
|
||
|
ImGui::SameLine();
|
||
|
ImGui::Text("%s", light_name.c_str());
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::AlignTextToFramePadding();
|
||
|
ImGui::Text("%s", light_display.empty() ? "None" : light_display.c_str());
|
||
|
ImGui::NextColumn();
|
||
|
if (light_display.empty()) {
|
||
|
ImGui::PopStyleColor();
|
||
|
}
|
||
|
|
||
|
// light binding
|
||
|
if (ImGui::Button("Bind")) {
|
||
|
ImGui::OpenPopup("Light Binding");
|
||
|
light->override_enabled = true;
|
||
|
|
||
|
// get devices
|
||
|
this->lights_devices.clear();
|
||
|
for (auto &device : RI_MGR->devices_get()) {
|
||
|
switch (device.type) {
|
||
|
case rawinput::HID:
|
||
|
if (!device.hidInfo->button_output_caps_list.empty()
|
||
|
|| !device.hidInfo->value_output_caps_list.empty())
|
||
|
this->lights_devices.emplace_back(&device);
|
||
|
break;
|
||
|
case rawinput::SEXTET_OUTPUT:
|
||
|
case rawinput::PIUIO_DEVICE:
|
||
|
case rawinput::SMX_STAGE:
|
||
|
this->lights_devices.emplace_back(&device);
|
||
|
break;
|
||
|
default:
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// check if this is the current device
|
||
|
if (device.name == light->getDeviceIdentifier()) {
|
||
|
this->lights_devices_selected = (int) this->lights_devices.size() - 1;
|
||
|
this->lights_devices_control_selected = light->getIndex();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (ImGui::BeginPopupModal("Light Binding", NULL,
|
||
|
ImGuiWindowFlags_AlwaysAutoResize)) {
|
||
|
|
||
|
// device selector
|
||
|
bool control_changed = false;
|
||
|
if (ImGui::Combo("Device",
|
||
|
&this->lights_devices_selected,
|
||
|
[] (void* data, int i, const char **item) {
|
||
|
*item = ((std::vector<rawinput::Device*>*) data)->at(i)->desc.c_str();
|
||
|
return true;
|
||
|
},
|
||
|
&this->lights_devices, (int) this->lights_devices.size())) {
|
||
|
this->lights_devices_control_selected = 0;
|
||
|
control_changed = true;
|
||
|
}
|
||
|
|
||
|
// obtain controls
|
||
|
std::vector<std::string> control_names;
|
||
|
if (lights_devices_selected >= 0 && lights_devices_selected < (int) lights_devices.size()) {
|
||
|
auto device = lights_devices[lights_devices_selected];
|
||
|
switch (device->type) {
|
||
|
case rawinput::HID: {
|
||
|
size_t index = 0;
|
||
|
|
||
|
// add button names
|
||
|
for (auto &button_name : device->hidInfo->button_output_caps_names) {
|
||
|
|
||
|
// build name
|
||
|
std::string name = button_name;
|
||
|
if (index > 0xFF)
|
||
|
name += " (0x" + bin2hex(&((char*) &index)[1], 1) + bin2hex(&((char*) &index)[0], 1) + ")";
|
||
|
else
|
||
|
name += " (0x" + bin2hex(&((char*) &index)[0], 1) + ")";
|
||
|
|
||
|
// add name
|
||
|
control_names.push_back(name);
|
||
|
index++;
|
||
|
}
|
||
|
|
||
|
// add value names
|
||
|
for (auto &value_name : device->hidInfo->value_output_caps_names) {
|
||
|
|
||
|
// build name
|
||
|
std::string name = value_name;
|
||
|
if (index > 0xFF)
|
||
|
name += " (0x" + bin2hex(&((char*) &index)[1], 1)
|
||
|
+ bin2hex(&((char*) &index)[0], 1)
|
||
|
+ ", value cap)";
|
||
|
else
|
||
|
name += " (0x" + bin2hex(&((char*) &index)[0], 1) + ", value cap)";
|
||
|
|
||
|
// add name
|
||
|
control_names.push_back(name);
|
||
|
index++;
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
case rawinput::SEXTET_OUTPUT: {
|
||
|
|
||
|
// add all names of sextet device
|
||
|
for (int i = 0; i < rawinput::SextetDevice::LIGHT_COUNT; i++) {
|
||
|
std::string name(rawinput::SextetDevice::LIGHT_NAMES[i]);
|
||
|
|
||
|
// add name
|
||
|
control_names.push_back(name);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case rawinput::PIUIO_DEVICE: {
|
||
|
|
||
|
// add all names of PIUIO device
|
||
|
for (int i = 0; i < rawinput::PIUIO::PIUIO_MAX_NUM_OF_LIGHTS; i++) {
|
||
|
std::string name(rawinput::PIUIO::LIGHT_NAMES[i]);
|
||
|
|
||
|
// add name
|
||
|
control_names.push_back(name);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case rawinput::SMX_STAGE: {
|
||
|
|
||
|
// add all names of SMX device
|
||
|
for (int i = 0; i < rawinput::SmxStageDevice::TOTAL_LIGHT_COUNT; i++) {
|
||
|
control_names.push_back(rawinput::SmxStageDevice::GetLightNameByIndex(i));
|
||
|
}
|
||
|
}
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// controls
|
||
|
if (ImGui::Combo("Light Control",
|
||
|
&this->lights_devices_control_selected,
|
||
|
[] (void* data, int i, const char **item) {
|
||
|
*item = ((std::vector<std::string> *) data)->at(i).c_str();
|
||
|
return true;
|
||
|
},
|
||
|
&control_names, control_names.size())) {
|
||
|
control_changed = true;
|
||
|
}
|
||
|
|
||
|
// update light
|
||
|
if (lights_devices_selected >= 0 && lights_devices_selected < (int) lights_devices.size()) {
|
||
|
auto identifier = this->lights_devices[lights_devices_selected]->name;
|
||
|
if (identifier != light->getDeviceIdentifier()) {
|
||
|
light->setDeviceIdentifier(identifier);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *light,
|
||
|
lights_page - 1);
|
||
|
}
|
||
|
if (this->lights_devices_control_selected >= 0) {
|
||
|
if ((int) light->getIndex() != this->lights_devices_control_selected) {
|
||
|
light->setIndex(this->lights_devices_control_selected);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *light,
|
||
|
lights_page - 1);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// value preview
|
||
|
ImGui::Separator();
|
||
|
float value_orig = GameAPI::Lights::readLight(RI_MGR, *light);
|
||
|
float value = value_orig;
|
||
|
ImGui::SliderFloat("Preview", &value, 0.f, 1.f);
|
||
|
|
||
|
// manual button controls
|
||
|
if (ImGui::Button("Turn On")) {
|
||
|
value = 1.f;
|
||
|
}
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Button("Turn Off")) {
|
||
|
value = 0.f;
|
||
|
}
|
||
|
|
||
|
// manual lock
|
||
|
if (!cfg::CONFIGURATOR_STANDALONE) {
|
||
|
ImGui::SameLine();
|
||
|
ImGui::Checkbox("Lock", &light->override_enabled);
|
||
|
}
|
||
|
|
||
|
// apply new value
|
||
|
if (value != value_orig || control_changed) {
|
||
|
if (light->override_enabled) {
|
||
|
light->override_state = value;
|
||
|
}
|
||
|
auto ident = light->getDeviceIdentifier();
|
||
|
GameAPI::Lights::writeLight(RI_MGR->devices_get(ident), light->getIndex(), value);
|
||
|
RI_MGR->devices_flush_output();
|
||
|
}
|
||
|
|
||
|
// close button
|
||
|
ImGui::Separator();
|
||
|
if (ImGui::Button("Close")) {
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
light->override_enabled = false;
|
||
|
}
|
||
|
|
||
|
// clean up
|
||
|
ImGui::EndPopup();
|
||
|
}
|
||
|
|
||
|
// clear light
|
||
|
if (light_display.size() > 0) {
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Button("Clear")) {
|
||
|
light->setDeviceIdentifier("");
|
||
|
light->setIndex(0xFF);
|
||
|
::Config::getInstance().updateBinding(
|
||
|
games_list[games_selected], *light,
|
||
|
lights_page - 1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// clean up
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::PopID();
|
||
|
}
|
||
|
}
|
||
|
ImGui::Columns();
|
||
|
}
|
||
|
|
||
|
void Config::build_cards() {
|
||
|
|
||
|
// early quit
|
||
|
if (this->games_selected < 0 || this->games_selected_name.empty()) {
|
||
|
ImGui::Text("Please select a game first.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// get bindings and copy paths
|
||
|
auto game = games_list[games_selected];
|
||
|
auto bindings = ::Config::getInstance().getKeypadBindings(this->games_selected_name);
|
||
|
bool bindings_updated = false;
|
||
|
for (int player = 0; player < 2; player++) {
|
||
|
strncpy(this->keypads_card_path[player],
|
||
|
bindings.card_paths[player].string().c_str(),
|
||
|
sizeof(this->keypads_card_path[0]) - 1);
|
||
|
this->keypads_card_path[player][sizeof(this->keypads_card_path[0]) - 1] = '\0';
|
||
|
}
|
||
|
|
||
|
// card settings for each player
|
||
|
for (int player = 0; player < 2; player++) {
|
||
|
|
||
|
// custom ID and title
|
||
|
ImGui::PushID(("KeypadP" + to_string(player)).c_str());
|
||
|
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Player %i", player + 1);
|
||
|
|
||
|
// card path
|
||
|
std::string hint = "card" + to_string(player) + ".txt";
|
||
|
if (ImGui::InputTextWithHint("Card Path", hint.c_str(),
|
||
|
this->keypads_card_path[player], sizeof(this->keypads_card_path[0]) - 1))
|
||
|
{
|
||
|
bindings.card_paths[player] = this->keypads_card_path[player];
|
||
|
bindings_updated = true;
|
||
|
}
|
||
|
|
||
|
// help marker
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker("Leave this empty to use the card file in your game directory.\n"
|
||
|
"Hint: You can place 'card0.txt' (P1) / 'card1.txt' (P2) into the root of your USB "
|
||
|
"flash drive and it will trigger a card insert when you connect it!");
|
||
|
|
||
|
// card path file selector
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Button("Open...")) {
|
||
|
|
||
|
// standalone version opens native file browser
|
||
|
if (cfg::CONFIGURATOR_STANDALONE && !keypads_card_select) {
|
||
|
|
||
|
// run in separate thread otherwise we get a crash
|
||
|
keypads_card_select_done = false;
|
||
|
keypads_card_select = new std::thread([this, bindings, player, game] {
|
||
|
|
||
|
// open dialog to get path
|
||
|
auto ofn_path = std::make_unique<wchar_t[]>(512);
|
||
|
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.lpstrDefExt = L"txt";
|
||
|
|
||
|
// check for success
|
||
|
if (GetSaveFileNameW(&ofn)) {
|
||
|
|
||
|
// update card path
|
||
|
auto new_bindings = bindings;
|
||
|
new_bindings.card_paths[player] = std::filesystem::path(ofn_path.get());
|
||
|
::Config::getInstance().updateBinding(game, new_bindings);
|
||
|
eamuse_update_keypad_bindings();
|
||
|
|
||
|
read_card(player);
|
||
|
} else {
|
||
|
auto error = CommDlgExtendedError();
|
||
|
if (error) {
|
||
|
log_warning("cfg", "failed to get save file name: {}", error);
|
||
|
} else {
|
||
|
log_warning("cfg", "failed to get save file name");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// clean up
|
||
|
keypads_card_select_done = true;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// in-game version opens ImGui file browser
|
||
|
if (!cfg::CONFIGURATOR_STANDALONE && !this->keypads_card_select_browser[player].IsOpened()) {
|
||
|
this->keypads_card_select_browser[player].SetTitle("Card Select");
|
||
|
this->keypads_card_select_browser[player].SetTypeFilters({".txt", "*"});
|
||
|
// this->keypads_card_select_browser[player].flags_ |= ImGuiFileBrowserFlags_EnterNewFilename;
|
||
|
this->keypads_card_select_browser[player].Open();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// clear button
|
||
|
if (!bindings.card_paths[player].empty()) {
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Button("Clear")) {
|
||
|
bindings.card_paths[player] = "";
|
||
|
bindings_updated = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// verify card number
|
||
|
auto card_valid = true;
|
||
|
if (this->keypads_card_number[player][0] != 0) {
|
||
|
for (int n = 0; n < 16; n++) {
|
||
|
char c = this->keypads_card_number[player][n];
|
||
|
bool digit = c >= '0' && c <= '9';
|
||
|
bool character_big = c >= 'A' && c <= 'F';
|
||
|
bool character_small = c >= 'a' && c <= 'f';
|
||
|
if (!digit && !character_big && !character_small) {
|
||
|
card_valid = false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// card number box
|
||
|
ImGui::PushStyleColor(ImGuiCol_Text,
|
||
|
card_valid ? ImVec4(1.f, 1.f, 1.f, 1.f) :
|
||
|
ImVec4(1.f, 0.f, 0.f, 1.f));
|
||
|
ImGui::InputTextWithHint("Card Number", "E004010000000000",
|
||
|
this->keypads_card_number[player], sizeof(this->keypads_card_number[0]) - 1,
|
||
|
ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsHexadecimal);
|
||
|
ImGui::PopStyleColor();
|
||
|
|
||
|
// write card after edit
|
||
|
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||
|
write_card(player);
|
||
|
read_card(1 - player);
|
||
|
}
|
||
|
|
||
|
// help marker
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker(
|
||
|
"Click on Generate button to randomize a valid card number and automatically it save to specified file.");
|
||
|
|
||
|
// generate button
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Button("Generate")) {
|
||
|
// don't know why this file insists on using 18 chars to store the card ID
|
||
|
char new_card[17];
|
||
|
generate_ea_card(new_card);
|
||
|
strcpy_s(this->keypads_card_number[player], new_card);
|
||
|
write_card(player);
|
||
|
read_card(1 - player);
|
||
|
}
|
||
|
|
||
|
// render card select browser
|
||
|
this->keypads_card_select_browser[player].Display();
|
||
|
if (this->keypads_card_select_browser[player].HasSelected()) {
|
||
|
auto selected = keypads_card_select_browser[player].GetSelected();
|
||
|
this->keypads_card_select_browser[player].ClearSelected();
|
||
|
bindings.card_paths[player] = selected;
|
||
|
bindings_updated = true;
|
||
|
}
|
||
|
|
||
|
// clean up thread when needed
|
||
|
if (keypads_card_select_done) {
|
||
|
keypads_card_select_done = false;
|
||
|
keypads_card_select->join();
|
||
|
delete keypads_card_select;
|
||
|
keypads_card_select = nullptr;
|
||
|
}
|
||
|
|
||
|
// clean up
|
||
|
ImGui::PopID();
|
||
|
ImGui::Separator();
|
||
|
}
|
||
|
|
||
|
// check for binding update and save
|
||
|
if (bindings_updated) {
|
||
|
::Config::getInstance().updateBinding(game, bindings);
|
||
|
eamuse_update_keypad_bindings();
|
||
|
read_card();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void Config::build_options(
|
||
|
std::vector<Option> *options, const std::string &category, const std::string *filter) {
|
||
|
int options_count;
|
||
|
|
||
|
ImGui::Columns(3, "OptionsColumns", true);
|
||
|
if (!category.empty() ) {
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), category.c_str());
|
||
|
} else if (filter != nullptr) {
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Search results");
|
||
|
} else {
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Options");
|
||
|
}
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Parameter");
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker(
|
||
|
"These are the command-line parameters you can use in your .bat file to set the options.\n"
|
||
|
"Example: spice.exe -w -ea\n"
|
||
|
" spice64.exe -api 1337 -apipass changeme");
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Setting");
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::Separator();
|
||
|
|
||
|
// iterate options
|
||
|
options_count = 0;
|
||
|
for (auto &option : *options) {
|
||
|
// get option definition
|
||
|
auto &definition = option.get_definition();
|
||
|
|
||
|
// check category
|
||
|
if (!category.empty() && definition.category != category) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// check hidden option
|
||
|
if (!this->options_show_hidden && option.value.empty()) {
|
||
|
|
||
|
// skip hidden entries
|
||
|
if (definition.hidden) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// check for game exclusivity
|
||
|
if (!definition.game_name.empty()) {
|
||
|
if (definition.game_name != this->games_selected_name) {
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// filter
|
||
|
if (filter != nullptr) {
|
||
|
if (filter->empty()) {
|
||
|
continue;
|
||
|
}
|
||
|
if (!option.search_match(*filter)) {
|
||
|
continue;
|
||
|
}
|
||
|
// limit to 30 results
|
||
|
if (30 < options_count) {
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
options_count += 1;
|
||
|
|
||
|
// list entry
|
||
|
ImGui::PushID(&option);
|
||
|
ImGui::AlignTextToFramePadding();
|
||
|
ImGui::HelpMarker(definition.desc.c_str());
|
||
|
ImGui::SameLine();
|
||
|
if (option.is_active()) {
|
||
|
if (option.disabled || definition.disabled) {
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.4f, 0.f, 1.f), "%s", definition.title.c_str());
|
||
|
} else {
|
||
|
ImGui::TextColored(ImVec4(1.f, 0.7f, 0.f, 1.f), "%s", definition.title.c_str());
|
||
|
}
|
||
|
} else if (definition.hidden
|
||
|
|| (!definition.game_name.empty() && definition.game_name != this->games_selected_name)) {
|
||
|
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.f), "%s", definition.title.c_str());
|
||
|
} else if (definition.game_name == this->games_selected_name) {
|
||
|
ImGui::TextColored(ImVec4(0.8f, 0, 0.8f, 1.f), "%s", definition.title.c_str());
|
||
|
} else {
|
||
|
ImGui::Text("%s", definition.title.c_str());
|
||
|
}
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::AlignTextToFramePadding();
|
||
|
if (definition.display_name.empty()) {
|
||
|
ImGui::Text("-%s", definition.name.c_str());
|
||
|
} else {
|
||
|
ImGui::Text("-%s", definition.display_name.c_str());
|
||
|
}
|
||
|
ImGui::NextColumn();
|
||
|
if (option.disabled || definition.disabled) {
|
||
|
ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
|
||
|
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
|
||
|
}
|
||
|
switch (definition.type) {
|
||
|
case OptionType::Bool: {
|
||
|
bool state = !option.value.empty();
|
||
|
if (ImGui::Checkbox(state ? "ON" : "off", &state)) {
|
||
|
this->options_dirty = true;
|
||
|
option.value = state ? "/ENABLED" : "";
|
||
|
::Config::getInstance().updateBinding(games_list[games_selected], option);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case OptionType::Integer: {
|
||
|
char buffer[512];
|
||
|
strncpy(buffer, option.value.c_str(), sizeof(buffer) - 1);
|
||
|
buffer[sizeof(buffer) - 1] = '\0';
|
||
|
auto digits_filter = [](ImGuiInputTextCallbackData* data) {
|
||
|
if ('0' <= data->EventChar && data->EventChar <= '9') {
|
||
|
return 0;
|
||
|
}
|
||
|
return 1; // discard
|
||
|
};
|
||
|
|
||
|
const char *hint = definition.setting_name.empty() ? "Enter number..."
|
||
|
: definition.setting_name.c_str();
|
||
|
|
||
|
ImGui::InputTextWithHint(
|
||
|
"", hint,
|
||
|
buffer, sizeof(buffer) - 1,
|
||
|
ImGuiInputTextFlags_CallbackCharFilter, digits_filter);
|
||
|
// would like to use IsItemDeactivatedAfterEdit but can't handle the case when window is closed while editing
|
||
|
if (ImGui::IsItemEdited()) {
|
||
|
this->options_dirty = true;
|
||
|
option.value = buffer;
|
||
|
::Config::getInstance().updateBinding(games_list[games_selected], option);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case OptionType::Hex: {
|
||
|
char buffer[512];
|
||
|
strncpy(buffer, option.value.c_str(), sizeof(buffer) - 1);
|
||
|
buffer[sizeof(buffer) - 1] = '\0';
|
||
|
auto digits_filter = [](ImGuiInputTextCallbackData* data) {
|
||
|
if ('0' <= data->EventChar && data->EventChar <= '9') {
|
||
|
return 0;
|
||
|
}
|
||
|
if ('a' <= data->EventChar && data->EventChar <= 'f') {
|
||
|
return 0;
|
||
|
}
|
||
|
if ('A' <= data->EventChar && data->EventChar <= 'F') {
|
||
|
return 0;
|
||
|
}
|
||
|
if (data->EventChar == 'x' || data->EventChar == 'X') {
|
||
|
return 0;
|
||
|
}
|
||
|
return 1; // discard
|
||
|
};
|
||
|
const char *hint = definition.setting_name.empty() ? "Enter hex..."
|
||
|
: definition.setting_name.c_str();
|
||
|
|
||
|
ImGui::InputTextWithHint("", hint,
|
||
|
buffer, sizeof(buffer) - 1,
|
||
|
ImGuiInputTextFlags_CallbackCharFilter, digits_filter);
|
||
|
// would like to use IsItemDeactivatedAfterEdit but can't handle the case when window is closed while editing
|
||
|
if (ImGui::IsItemEdited()) {
|
||
|
this->options_dirty = true;
|
||
|
option.value = buffer;
|
||
|
::Config::getInstance().updateBinding(games_list[games_selected], option);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case OptionType::Text: {
|
||
|
char buffer[512];
|
||
|
strncpy(buffer, option.value.c_str(), sizeof(buffer) - 1);
|
||
|
buffer[sizeof(buffer) - 1] = '\0';
|
||
|
|
||
|
const char *hint = definition.setting_name.empty() ? "Enter value..."
|
||
|
: definition.setting_name.c_str();
|
||
|
|
||
|
ImGui::InputTextWithHint("", hint, buffer, sizeof(buffer) - 1);
|
||
|
// would like to use IsItemDeactivatedAfterEdit but can't handle the case when window is closed while editing
|
||
|
if (ImGui::IsItemEdited()) {
|
||
|
this->options_dirty = true;
|
||
|
option.value = buffer;
|
||
|
::Config::getInstance().updateBinding(games_list[games_selected], option);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case OptionType::Enum: {
|
||
|
std::string current_item = option.value_text();
|
||
|
for (auto &element : definition.elements) {
|
||
|
if (element.first == current_item) {
|
||
|
current_item += fmt::format(" ({})", element.second);
|
||
|
}
|
||
|
}
|
||
|
if (current_item.empty()) {
|
||
|
current_item = "Default";
|
||
|
}
|
||
|
if (ImGui::BeginCombo("##combo", current_item.c_str(), 0)) {
|
||
|
for (auto &element : definition.elements) {
|
||
|
bool selected = current_item == element.first;
|
||
|
std::string label = element.first;
|
||
|
if (!element.second.empty()) {
|
||
|
label += fmt::format(" ({})", element.second);
|
||
|
}
|
||
|
if (ImGui::Selectable(label.c_str(), selected)) {
|
||
|
this->options_dirty = true;
|
||
|
option.value = element.first;
|
||
|
::Config::getInstance().updateBinding(games_list[games_selected], option);
|
||
|
}
|
||
|
if (selected) {
|
||
|
ImGui::SetItemDefaultFocus();
|
||
|
}
|
||
|
}
|
||
|
ImGui::EndCombo();
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
default: {
|
||
|
ImGui::Text("Unknown option type");
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// clear button
|
||
|
if (!option.disabled && !definition.disabled && option.is_active() && option.get_definition().type != OptionType::Bool) {
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Button("Clear")) {
|
||
|
this->options_dirty = true;
|
||
|
option.value = "";
|
||
|
::Config::getInstance().updateBinding(games_list[games_selected], option);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// clean up disabled item flags
|
||
|
if (option.disabled || definition.disabled) {
|
||
|
ImGui::PopItemFlag();
|
||
|
ImGui::PopStyleVar();
|
||
|
}
|
||
|
|
||
|
// disabled help
|
||
|
if (option.disabled && !definition.disabled) {
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker(
|
||
|
"This option can not be edited because it was overriden by command-line options.\n"
|
||
|
"Run spicecfg.exe to configure the options and then run spice(64).exe directly.");
|
||
|
}
|
||
|
|
||
|
// next item
|
||
|
ImGui::PopID();
|
||
|
ImGui::NextColumn();
|
||
|
}
|
||
|
|
||
|
// check if empty
|
||
|
if (options_count == 0) {
|
||
|
ImGui::TextUnformatted("-");
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::TextUnformatted("-");
|
||
|
ImGui::NextColumn();
|
||
|
ImGui::TextUnformatted("-");
|
||
|
ImGui::NextColumn();
|
||
|
}
|
||
|
|
||
|
ImGui::Columns(1);
|
||
|
ImGui::TextUnformatted("");
|
||
|
}
|
||
|
|
||
|
void Config::build_about() {
|
||
|
ImGui::TextUnformatted(std::string(
|
||
|
"spice2x (a fork of SpiceTools)\r\n"
|
||
|
"=========================\r\n" +
|
||
|
to_string(VERSION_STRING)).c_str());
|
||
|
|
||
|
ImGui::TextUnformatted("");
|
||
|
|
||
|
if (ImGui::Button(PROJECT_URL)) {
|
||
|
launch_shell(PROJECT_URL, nullptr);
|
||
|
}
|
||
|
|
||
|
ImGui::TextUnformatted("");
|
||
|
ImGui::TextUnformatted(std::string(
|
||
|
resutil::load_file_string_crlf(IDR_README) +
|
||
|
"\r\n" +
|
||
|
"Changelog (Highlights):\r\n" +
|
||
|
resutil::load_file_string_crlf(IDR_CHANGELOG)).c_str());
|
||
|
}
|
||
|
|
||
|
void Config::build_licenses() {
|
||
|
ImGui::TextUnformatted(resutil::load_file_string_crlf(IDR_LICENSES).c_str());
|
||
|
}
|
||
|
|
||
|
void Config::build_launcher() {
|
||
|
ImGui::TextUnformatted("Please select a game!");
|
||
|
ImGui::Separator();
|
||
|
ImGui::BeginChild("Launcher");
|
||
|
this->build_about();
|
||
|
ImGui::EndChild();
|
||
|
}
|
||
|
|
||
|
void Config::build_page_selector(int *page) {
|
||
|
|
||
|
// left page selector
|
||
|
if (ImGui::Button("<")) {
|
||
|
*page = *page - 1;
|
||
|
if (*page < 0) {
|
||
|
*page = 99;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// page display and reset
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Button(("Page " + to_string(*page)).c_str())) {
|
||
|
*page = 0;
|
||
|
}
|
||
|
|
||
|
// right page selector
|
||
|
ImGui::SameLine();
|
||
|
if (ImGui::Button(">")) {
|
||
|
*page = *page + 1;
|
||
|
if (*page > 99) {
|
||
|
*page = 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// help
|
||
|
ImGui::SameLine();
|
||
|
ImGui::HelpMarker("You can bind buttons/lights to multiple inputs/outputs "
|
||
|
"at the same time using pages.");
|
||
|
}
|
||
|
|
||
|
void Config::launch_shell(LPCSTR app, LPCSTR file) {
|
||
|
// doing this on a separate thread to avoid polluting ImGui context
|
||
|
std::thread t([app, file] {
|
||
|
ShellExecuteA(NULL, "open", app, file, NULL, SW_SHOWNORMAL);
|
||
|
});
|
||
|
t.join();
|
||
|
}
|
||
|
|
||
|
void Config::build_menu(int *game_selected) {
|
||
|
bool about_popup = false;
|
||
|
bool licenses_popup = false;
|
||
|
bool shutdown_popup = false;
|
||
|
if (ImGui::BeginMenuBar()) {
|
||
|
|
||
|
// [spice2x]
|
||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.f, 0.f, 1.f));
|
||
|
ImGui::PushStyleColor(ImGuiCol_TextDisabled, ImVec4(1.f, 0.f, 0.f, 1.f));
|
||
|
ImGui::BeginDisabled(!cfg::CONFIGURATOR_STANDALONE);
|
||
|
if (ImGui::MenuItem("[spice2x]")) {
|
||
|
launch_shell(PROJECT_URL, nullptr);
|
||
|
}
|
||
|
ImGui::EndDisabled();
|
||
|
ImGui::PopStyleColor();
|
||
|
ImGui::PopStyleColor();
|
||
|
|
||
|
ImGui::Separator();
|
||
|
|
||
|
// game selector
|
||
|
ImGui::PushItemWidth(MIN(580, MAX(80, ImGui::GetWindowSize().x - 520)));
|
||
|
ImGui::Combo("##game_selector", game_selected, games_names.data(), (int)games_list.size());
|
||
|
ImGui::PopItemWidth();
|
||
|
|
||
|
ImGui::Separator();
|
||
|
|
||
|
// shortcuts
|
||
|
if (ImGui::BeginMenu("Shortcuts", cfg::CONFIGURATOR_STANDALONE)) {
|
||
|
if (ImGui::MenuItem("USB Game Controllers")) {
|
||
|
launch_shell("control.exe", "joy.cpl");
|
||
|
}
|
||
|
if (ImGui::MenuItem("Audio Playback Devices")) {
|
||
|
launch_shell("control.exe", "mmsys.cpl,,0");
|
||
|
}
|
||
|
if (ImGui::MenuItem("Tablet PC Settings")) {
|
||
|
launch_shell("explorer.exe", "shell:::{80F3F1D5-FECA-45F3-BC32-752C152E456E}");
|
||
|
}
|
||
|
ImGui::EndMenu();
|
||
|
}
|
||
|
|
||
|
// popup menus
|
||
|
if (ImGui::MenuItem("Licenses")) {
|
||
|
licenses_popup = true;
|
||
|
}
|
||
|
if (ImGui::MenuItem("About")) {
|
||
|
about_popup = true;
|
||
|
}
|
||
|
|
||
|
// power - only active in games
|
||
|
if (ImGui::BeginMenu("Power", !cfg::CONFIGURATOR_STANDALONE)) {
|
||
|
if (ImGui::MenuItem("Restart Game")) {
|
||
|
launcher::restart();
|
||
|
}
|
||
|
if (ImGui::MenuItem("Exit Game")) {
|
||
|
launcher::shutdown();
|
||
|
}
|
||
|
ImGui::Spacing();
|
||
|
ImGui::Separator();
|
||
|
ImGui::Spacing();
|
||
|
if (ImGui::MenuItem("Shutdown PC")) {
|
||
|
shutdown_popup = true;
|
||
|
}
|
||
|
ImGui::EndMenu();
|
||
|
}
|
||
|
|
||
|
ImGui::Separator();
|
||
|
|
||
|
ImGui::BeginDisabled();
|
||
|
if (!avs::game::is_model("000")) {
|
||
|
ImGui::Text("%s", avs::game::get_identifier().c_str());
|
||
|
}
|
||
|
ImGui::EndDisabled();
|
||
|
|
||
|
ImGui::EndMenuBar();
|
||
|
}
|
||
|
|
||
|
// workaround for popups triggered by menu, see https://github.com/ocornut/imgui/issues/331
|
||
|
if (about_popup) {
|
||
|
ImGui::OpenPopup("About");
|
||
|
}
|
||
|
if (licenses_popup) {
|
||
|
ImGui::OpenPopup("Licenses");
|
||
|
}
|
||
|
if (shutdown_popup) {
|
||
|
ImGui::OpenPopup("System");
|
||
|
}
|
||
|
|
||
|
// draw popups
|
||
|
{
|
||
|
// unused_open is needed for close button to appear on the popup
|
||
|
bool unused_open = true;
|
||
|
|
||
|
if (ImGui::BeginPopupModal(
|
||
|
"System",
|
||
|
&unused_open,
|
||
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize)) {
|
||
|
|
||
|
static bool force_shutdown = false;
|
||
|
ImGui::Spacing();
|
||
|
ImGui::Checkbox("Force", &force_shutdown);
|
||
|
ImGui::Spacing();
|
||
|
|
||
|
const ImVec2 button_size(100.f, 0.f);
|
||
|
|
||
|
ImGui::Spacing();
|
||
|
if (ImGui::Button("Shutdown PC", button_size)) {
|
||
|
this->shutdown_system(force_shutdown, false);
|
||
|
}
|
||
|
ImGui::Spacing();
|
||
|
if (ImGui::Button("Reboot PC", button_size)) {
|
||
|
this->shutdown_system(force_shutdown, true);
|
||
|
}
|
||
|
ImGui::Spacing();
|
||
|
ImGui::Separator();
|
||
|
ImGui::Spacing();
|
||
|
if (ImGui::Button("Cancel", button_size)) {
|
||
|
ImGui::CloseCurrentPopup();
|
||
|
}
|
||
|
ImGui::Spacing();
|
||
|
ImGui::EndPopup();
|
||
|
}
|
||
|
|
||
|
const ImVec2 popup_size(
|
||
|
std::min(ImGui::GetIO().DisplaySize.x * 0.9f, 800.f),
|
||
|
std::min(ImGui::GetIO().DisplaySize.y * 0.9f, 800.f));
|
||
|
|
||
|
const ImVec2 popup_pos(
|
||
|
ImGui::GetIO().DisplaySize.x / 2 - popup_size.x / 2,
|
||
|
ImGui::GetIO().DisplaySize.y / 2 - popup_size.y / 2);
|
||
|
|
||
|
ImGui::SetNextWindowSize(popup_size, ImGuiCond_Appearing);
|
||
|
ImGui::SetNextWindowPos(popup_pos, ImGuiCond_Appearing);
|
||
|
if (ImGui::BeginPopupModal("About", &unused_open)) {
|
||
|
this->build_about();
|
||
|
ImGui::EndPopup();
|
||
|
}
|
||
|
ImGui::SetNextWindowSize(popup_size, ImGuiCond_Appearing);
|
||
|
ImGui::SetNextWindowPos(popup_pos, ImGuiCond_Appearing);
|
||
|
if (ImGui::BeginPopupModal("Licenses", &unused_open)) {
|
||
|
this->build_licenses();
|
||
|
ImGui::EndPopup();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void Config::shutdown_system(bool force, bool reboot_instead) {
|
||
|
if (!acquire_shutdown_privs()) {
|
||
|
return;
|
||
|
}
|
||
|
UINT flags = 0;
|
||
|
if (force) {
|
||
|
flags |= EWX_FORCE;
|
||
|
}
|
||
|
if (reboot_instead) {
|
||
|
flags |= EWX_REBOOT;
|
||
|
} else {
|
||
|
flags |= EWX_SHUTDOWN | EWX_HYBRID_SHUTDOWN;
|
||
|
}
|
||
|
if (!ExitWindowsEx(flags, SHTDN_REASON_MAJOR_APPLICATION | SHTDN_REASON_MINOR_MAINTENANCE)) {
|
||
|
return;
|
||
|
}
|
||
|
launcher::shutdown(0);
|
||
|
}
|
||
|
}
|