segatools-configurator/configurator.cpp

720 lines
23 KiB
C++

//
// Created by beerpsi on 4/14/2024.
//
#include <format>
#include <fstream>
#include <random>
#include <imgui.h>
#include "configurator.h"
#include "extensions/imgui.h"
#include "games/io.h"
#include "option.h"
#include "imgui_internal.h"
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
// Card-related fields are initialized by the read_card() function.
#pragma clang diagnostic push
#pragma ide diagnostic ignored "cppcoreguidelines-pro-type-member-init"
Configurator::Configurator() {
const std::vector<std::string>& names = games::get_games();
for (const std::string& game_name : names) {
this->game_names.push_back(game_name.c_str());
std::vector<std::string>* file_hints = games::get_game_file_hints(game_name);
if (!file_hints) {
continue;
}
for (std::string& file_hint : *file_hints) {
std::filesystem::file_status status = std::filesystem::status(std::filesystem::path(file_hint));
if (status.type() != std::filesystem::file_type::regular) {
continue;
}
this->game_selected = this->game_names.size() - 1;
this->game_selected_name = game_name;
}
}
read_card();
}
#pragma clang diagnostic pop
void Configurator::read_card() {
this->aime_gen = GetPrivateProfileIntA(
"aime",
"aimeGen",
1,
".\\segatools.ini"
);
GetPrivateProfileStringA(
"aime",
"aimePath",
"DEVICE\\aime.txt",
this->aime_card_path,
MAX_PATH,
".\\segatools.ini"
);
std::ifstream f(this->aime_card_path);
if (!f || !f.is_open()) {
this->aime_card_id[0] = 0;
} else {
f.seekg(0, std::ios::end);
auto length = (size_t)f.tellg();
f.seekg(0, std::ios::beg);
f.read(this->aime_card_id, 20);
this->aime_card_id[length < 20 ? length : 20] = 0;
f.close();
}
this->felica_gen = GetPrivateProfileIntA(
"aime",
"felicaGen",
0,
".\\segatools.ini"
);
GetPrivateProfileStringA(
"aime",
"felicaPath",
"DEVICE\\felica.txt",
this->felica_card_path,
MAX_PATH,
".\\segatools.ini"
);
std::ifstream f2(this->felica_card_path);
if (!f2 || !f2.is_open()) {
this->felica_card_id[0] = 0;
} else {
f2.seekg(0, std::ios::end);
auto length = (size_t)f2.tellg();
f2.seekg(0, std::ios::beg);
f2.read(this->felica_card_id, 16);
this->felica_card_id[length < 16 ? length : 16] = 0;
f2.close();
}
}
void Configurator::write_card() {
WritePrivateProfileStringA(
"aime",
"aimeGen",
this->aime_gen ? "1" : "0",
".\\segatools.ini"
);
WritePrivateProfileStringA(
"aime",
"aimePath",
this->aime_card_path,
".\\segatools.ini"
);
int id_len = strlen(this->aime_card_id);
if (id_len == 20) {
std::ofstream f(this->aime_card_path);
if (f) {
f.write(this->aime_card_id, id_len);
f.close();
}
}
WritePrivateProfileStringA(
"aime",
"felicaGen",
this->felica_gen ? "1" : "0",
".\\segatools.ini"
);
WritePrivateProfileStringA(
"aime",
"felicaPath",
this->felica_card_path,
".\\segatools.ini"
);
id_len = strlen(this->felica_card_id);
if (id_len == 16) {
std::ofstream f(this->felica_card_path);
if (f) {
f.write(this->felica_card_id, id_len);
f.close();
}
}
}
void Configurator::build_content() {
ImGui::PushItemWidth(MAX(1, 463 - MAX(0, 580 - ImGui::GetWindowSize().x)));
ImGui::Combo("Game Selection",
&this->game_selected, game_names.data(),
(int) game_names.size());
ImGui::PopItemWidth();
if (this->game_selected >= 0 && this->game_selected < game_names.size()) {
this->game_selected_name = std::string(game_names.at(this->game_selected));
} else {
this->game_selected_name = "";
}
if (this->game_selected_name.empty()) {
ImGui::TextUnformatted("Please select a game!");
return;
}
if (ImGui::BeginTabBar("Config Tabs", ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)) {
if (ImGui::BeginTabItem("Buttons")) {
build_buttons("SW", &games::get_sw_buttons(this->game_selected_name));
ImGui::TextUnformatted("");
build_buttons("Game", games::get_buttons(this->game_selected_name));
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Cards")) {
build_cards();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Options")) {
auto options = parse_options();
std::vector<std::string> categories = { "Game Options", "VFS", "Aime", "Network", "Graphics", "Keychip", "Clock", "Other" };
for (const auto& category : categories) {
build_options(options.get(), category);
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Advanced")) {
auto options = parse_options();
std::vector<std::string> categories = { "Game Options (Advanced)", "Graphics (Advanced)", "Keychip (Advanced)", "System (Advanced)", "ALLS (Advanced)", "AMEX (Advanced)" };
for (const auto& category : categories) {
build_options(options.get(), category);
}
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("About")) {
build_about();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
void Configurator::open_card_file_selector(const std::filesystem::path& pwd, const wchar_t* key) {
card_select_done = false;
card_select_thread = new std::thread([pwd, key, this] {
auto ofn_path = std::make_unique<wchar_t[]>(MAX_PATH);
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";
if (GetSaveFileNameW(&ofn)) {
// I don't know why but the PWD is changed lmao
std::filesystem::current_path(pwd);
WritePrivateProfileStringW(
L"aime",
key,
ofn_path.get(),
L".\\segatools.ini"
);
read_card();
} else {
auto error = CommDlgExtendedError();
OutputDebugStringA(std::format("Failed to get save file name: {}", error).c_str());
};
card_select_done = true;
});
}
void Configurator::build_cards() {
bool updated = false;
ImGui::PushID("CardAime");
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "AiMe");
ImGui::AlignTextToFramePadding();
ImGui::Text("Automatically generate if not exist when scanning");
ImGui::SameLine();
if (ImGui::Checkbox(this->aime_gen ? "Enabled" : "Disabled", &this->aime_gen)) {
updated = true;
}
if (ImGui::InputTextWithHint("Card Path", "DEVICE\\aime.txt", this->aime_card_path, sizeof(this->aime_card_path))) {
updated = true;
}
ImGui::SameLine();
if (ImGui::Button("Open...")) {
open_card_file_selector(std::filesystem::current_path(), L"aimePath");
}
bool card_valid = this->aime_card_id[0] == 0 || strlen(this->aime_card_id) == 20;
if (this->aime_card_id[0] != 0 && card_valid) {
for (int i = 0; i < 20; i++) {
char c = this->aime_card_id[i];
bool is_digit = '0' <= c && c <= '9';
if (!is_digit) {
card_valid = false;
}
}
}
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("Access Code", "01234567890123456789",
this->aime_card_id, sizeof(this->aime_card_id),
ImGuiInputTextFlags_CharsDecimal);
ImGui::PopStyleColor();
if (ImGui::IsItemDeactivatedAfterEdit()) {
updated = true;
}
ImGui::SameLine();
ImGui::HelpMarker("Click Generate to create a random access code and save it to the configured file.");
ImGui::SameLine();
if (ImGui::Button("Generate")) {
std::random_device rd;
std::mt19937 generator(rd());
std::uniform_int_distribution<> uniform(0, 9);
uint8_t card_bytes[10];
// AiMe IDs should not start with 3, due to a missing check for BananaPass IDs
do {
for (unsigned char & card_byte : card_bytes) {
card_byte = uniform(generator) << 4 | uniform(generator);
}
} while (card_bytes[0] >> 4 == 3);
sprintf(this->aime_card_id,
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
card_bytes[0], card_bytes[1], card_bytes[2], card_bytes[3], card_bytes[4],
card_bytes[5], card_bytes[6], card_bytes[7], card_bytes[8], card_bytes[9]);
write_card();
}
ImGui::PopID();
ImGui::Separator();
ImGui::PushID("CardFeliCa");
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "FeliCa");
ImGui::AlignTextToFramePadding();
ImGui::Text("Automatically generate if not exist when scanning");
ImGui::SameLine();
if (ImGui::Checkbox(this->felica_gen ? "Enabled" : "Disabled", &this->felica_gen)) {
updated = true;
}
if (ImGui::InputTextWithHint("Card Path", "DEVICE\\felica.txt", this->felica_card_path, sizeof(this->felica_card_path))) {
updated = true;
}
ImGui::SameLine();
if (ImGui::Button("Open...")) {
open_card_file_selector(std::filesystem::current_path(), L"felicaPath");
}
card_valid = this->felica_card_id[0] == 0 || strlen(this->felica_card_id) == 16;
if (this->felica_card_id[0] != 0 && card_valid) {
for (int i = 0; i < 20; i++) {
char c = this->aime_card_id[i];
bool is_digit = '0' <= c && c <= '9';
bool is_hex = ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F');
if (!is_digit && !is_hex) {
card_valid = false;
}
}
}
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", "0B01020304050607",
this->felica_card_id, sizeof(this->felica_card_id),
ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase);
ImGui::PopStyleColor();
if (ImGui::IsItemDeactivatedAfterEdit()) {
updated = true;
}
ImGui::SameLine();
ImGui::HelpMarker("Click Generate to create a random card number and save it to the configured file.");
ImGui::SameLine();
if (ImGui::Button("Generate")) {
std::random_device rd;
std::mt19937 generator(rd());
std::uniform_int_distribution<> uniform(0, 255);
uint8_t card_bytes[8];
for (unsigned char & card_byte : card_bytes) {
card_byte = uniform(generator);
}
// FeliCa IDm values should have a 0 in their high nibble. Apparently.
card_bytes[0] &= 0x0F;
sprintf(this->felica_card_id,
"%02X%02X%02X%02X%02X%02X%02X%02X",
card_bytes[0], card_bytes[1], card_bytes[2], card_bytes[3],
card_bytes[4], card_bytes[5], card_bytes[6], card_bytes[7]);
write_card();
}
ImGui::PopID();
// clean up thread when needed
if (card_select_done) {
card_select_done = false;
card_select_thread->join();
delete card_select_thread;
card_select_thread = nullptr;
}
if (updated) {
write_card();
read_card();
}
}
void Configurator::build_options(std::vector<Option> *options, const std::string &category) {
ImGui::Columns(3, "OptionsColumns", true);
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), category.c_str());
ImGui::NextColumn();
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Key");
ImGui::SameLine();
ImGui::HelpMarker("This is what the option corresponds to in segatools.ini.");
ImGui::NextColumn();
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1), "Setting");
ImGui::NextColumn();
ImGui::Separator();
if (options->empty()) {
ImGui::TextUnformatted("-");
ImGui::NextColumn();
ImGui::TextUnformatted("-");
ImGui::NextColumn();
ImGui::TextUnformatted("-");
ImGui::NextColumn();
}
games::HWFamily hw_family = games::get_hw_family(this->game_selected_name);
for (auto &option : *options) {
auto definition = option.get_definition();
// check category
if (!category.empty() && definition.category != category) {
continue;
}
if (!definition.game_name.empty() && definition.game_name != this->game_selected_name) {
continue;
}
if (definition.hw_family != games::HW_FAMILY_UNKNOWN && definition.hw_family != hw_family) {
continue;
}
// list entry
ImGui::PushID(&option);
ImGui::AlignTextToFramePadding();
ImGui::HelpMarker(definition.desc.c_str());
ImGui::SameLine();
if (option.value != definition.default_value) {
ImGui::TextColored(ImVec4(1.f, 0.7f, 0, 1.f), "%s", definition.title.c_str());
} else {
ImGui::Text("%s", definition.title.c_str());
}
ImGui::NextColumn();
ImGui::AlignTextToFramePadding();
ImGui::Text("%s.%s", definition.section.c_str(), definition.key.c_str());
ImGui::NextColumn();
if (option.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_bool();
if (ImGui::Checkbox(state ? "Enabled" : "Disabled", &state)) {
option.value_set(state ? "1" : "0");
option.update_config();
}
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;
};
const char *hint = definition.setting_name.empty() ? "Enter number..."
: definition.setting_name.c_str();
if (ImGui::InputTextWithHint("", hint,
buffer, sizeof(buffer) - 1,
ImGuiInputTextFlags_CallbackCharFilter, digits_filter)) {
option.value = buffer;
option.update_config();
}
break;
}
case OptionType::Text: {
char buffer[definition.max_string_length + 1];
strncpy(buffer, option.value.c_str(), sizeof(buffer));
buffer[sizeof(buffer) - 1] = '\0';
const char *hint = definition.setting_name.empty() ? "Enter value..."
: definition.setting_name.c_str();
if (ImGui::InputTextWithHint("", hint, buffer, sizeof(buffer))) {
option.value = buffer;
option.update_config();
}
break;
}
case OptionType::Enum: {
std::string current_item = option.value_text();
for (auto &element : definition.elements) {
if (element.first == current_item) {
current_item += std::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 += std::format(" ({})", element.second);
}
if (ImGui::Selectable(label.c_str(), selected)) {
option.value = element.first;
option.update_config();
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
break;
}
default: {
ImGui::Text("Unsupported option type");
break;
}
}
if (option.disabled) {
ImGui::PopItemFlag();
ImGui::PopStyleVar();
}
ImGui::PopID();
ImGui::NextColumn();
}
ImGui::Columns(1);
ImGui::TextUnformatted("");
}
void Configurator::build_buttons(const std::string &name, std::vector<Button> *buttons) {
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::NextColumn();
ImGui::Separator();
// check if empty
if (!buttons || buttons->empty()) {
ImGui::TextUnformatted("-");
ImGui::NextColumn();
ImGui::TextUnformatted("-");
ImGui::NextColumn();
ImGui::TextUnformatted("-");
ImGui::NextColumn();
ImGui::Columns();
return;
}
for (auto &button : *buttons) {
ImGui::PushID(&button);
bool button_pressed = (GetAsyncKeyState(button.vKey) & 0x8000) > 0;
if (button_pressed) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.7f, 0.f, 1.f));
}
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", button.name.c_str());
ImGui::NextColumn();
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", button.getVKeyName().c_str());
ImGui::NextColumn();
if (button_pressed) {
ImGui::PopStyleColor();
}
ImGui::AlignTextToFramePadding();
std::string bind_name = "Bind " + button.name;
if (ImGui::Button("Bind")) {
ImGui::OpenPopup(bind_name.c_str());
this->previous_vk = this->vk;
}
if (ImGui::BeginPopupModal(bind_name.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
bool isHovered = ImGui::IsItemHovered();
ImGui::Text("Please press any button.");
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Hint: Press ESC to cancel!");
if (this->game_selected_name == "O.N.G.E.K.I.") {
// XXX: How do I check if the mouse is hovering over the entire popup?
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Hint 2: Click on this popup's title bar to bind to a mouse button.");
}
if (ImGui::Button("Cancel")) {
ImGui::CloseCurrentPopup();
} else if (this->vk != this->previous_vk) {
if (this->vk != VK_ESCAPE) {
button.vKey = this->vk;
WritePrivateProfileStringA(
button.section.c_str(),
button.key.c_str(),
std::format("{:#x}", button.vKey).c_str(),
".\\segatools.ini"
);
}
ImGui::CloseCurrentPopup();
} else if (isHovered && this->game_selected_name == "O.N.G.E.K.I.") {
int vKey = -1;
const ImGuiIO& io = ImGui::GetIO();
if (io.MouseDown[0]) {
vKey = VK_LBUTTON;
} else if (io.MouseDown[1]) {
vKey = VK_RBUTTON;
} else if (io.MouseDown[2]) {
vKey = VK_MBUTTON;
}
if (vKey != -1) {
button.vKey = vKey;
WritePrivateProfileStringA(
button.section.c_str(),
button.key.c_str(),
std::format("{:#x}", button.vKey).c_str(),
".\\segatools.ini"
);
ImGui::CloseCurrentPopup();
}
}
ImGui::EndPopup();
}
ImGui::SameLine();
if (ImGui::Button("Default")) {
button.vKey = button.defaultVKey;
WritePrivateProfileStringA(
button.section.c_str(),
button.key.c_str(),
std::format("{:#x}", button.vKey).c_str(),
".\\segatools.ini"
);
}
ImGui::NextColumn();
ImGui::PopID();
}
ImGui::Columns();
}
void Configurator::build_about() {
ImGui::TextUnformatted("segatools-configurator");
ImGui::TextUnformatted("");
ImGui::TextUnformatted("Based on spice2x/SpiceTools Configurator. Check them out if you play 573 rhythm games:");
if (ImGui::Button("https://spice2x.github.io")) {
std::thread t([] {
ShellExecuteA(nullptr, "open", "https://spice2x.github.io", nullptr, nullptr, SW_SHOWNORMAL);
});
t.join();
}
}