mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
overlay: redesign Cards tab (#593)
* add `-card0` `-card1` options at the top of Cards tab to clearly indicate that they take precedence over card files * reorganize UI / clean up * remove ability to directly edit the contents of `card0.txt` / `card1.txt` as that wasn't very intuitive; instead, encourage users to use overrides directly or use an external text editor (`Edit` button opens notepad)
This commit is contained in:
@@ -58,6 +58,7 @@ namespace overlay::windows {
|
||||
const auto PROJECT_URL = "https://spice2x.github.io";
|
||||
|
||||
constexpr ImVec4 TEXT_COLOR_GREEN(0.f, 1.f, 0.f, 1.f);
|
||||
constexpr uint32_t OPTION_INPUT_TEXT_WIDTH = 512;
|
||||
|
||||
std::unique_ptr<AsioDriverList> asio_driver_list;
|
||||
|
||||
@@ -202,25 +203,6 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -3694,14 +3676,121 @@ namespace overlay::windows {
|
||||
|
||||
void Config::build_cards() {
|
||||
|
||||
constexpr float TEXT_INPUT_WIDTH = 240.f;
|
||||
|
||||
// early quit
|
||||
if (this->games_selected < 0 || this->games_selected_name.empty()) {
|
||||
ImGui::Text("Please select a game first.");
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Card overrides");
|
||||
ImGui::Spacing();
|
||||
ImGui::SeparatorText("Card files (card0.txt / card1.txt)");
|
||||
ImGui::TextUnformatted(
|
||||
"Specify hardcoded card numbers here. This will always take priority when Insert Card is pressed.");
|
||||
ImGui::Spacing();
|
||||
|
||||
// read in values from options
|
||||
auto options = games::get_options(this->games_selected_name);
|
||||
for (int player = 0; player < 2; player++) {
|
||||
ImGui::PushID(("OverrideCardP" + to_string(player)).c_str());
|
||||
|
||||
// intentionally using the same buffer length as options tab
|
||||
char buffer[OPTION_INPUT_TEXT_WIDTH];
|
||||
bool card_changed = false;
|
||||
|
||||
// read in values from options
|
||||
auto &option = (player == 0)
|
||||
? options->at(launcher::Options::Player1Card)
|
||||
: options->at(launcher::Options::Player2Card);
|
||||
if (option.is_active()) {
|
||||
strncpy(buffer, option.value.c_str(), sizeof(buffer) - 1);
|
||||
} else {
|
||||
buffer[0] = '\0';
|
||||
}
|
||||
|
||||
const bool tree_open = ImGui::TreeNodeEx("##OverrideCardTree", ImGuiTreeNodeFlags_DefaultOpen);
|
||||
if (this->keypads_card_override_valid[player]) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
|
||||
}
|
||||
ImGui::SameLine(0.0f, 0.0f);
|
||||
ImGui::Text("Player %i (-card%i)", player + 1, player);
|
||||
if (this->keypads_card_override_valid[player]) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
if (tree_open) {
|
||||
// card number box
|
||||
ImGui::PushStyleColor(
|
||||
ImGuiCol_Text,
|
||||
this->keypads_card_override_valid[player] ? ImVec4(1.f, 1.f, 1.f, 1.f) :
|
||||
ImVec4(1.f, 0.f, 0.f, 1.f));
|
||||
ImGui::SetNextItemWidth(TEXT_INPUT_WIDTH);
|
||||
ImGui::InputTextWithHint("##OverrideCard", "E004010000000000",
|
||||
buffer, sizeof(buffer) - 1,
|
||||
ImGuiInputTextFlags_CharsUppercase |
|
||||
ImGuiInputTextFlags_CharsHexadecimal |
|
||||
ImGuiInputTextFlags_EscapeClearsAll);
|
||||
ImGui::PopStyleColor();
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||||
card_changed = true;
|
||||
}
|
||||
|
||||
// check if valid on every frame;
|
||||
// do this after the widget and remember the result for next frame
|
||||
this->keypads_card_override_valid[player] = false;
|
||||
const auto buffer_len = strlen(buffer);
|
||||
if (buffer_len == 16) {
|
||||
this->keypads_card_override_valid[player] = validate_ea_card(buffer);
|
||||
} else if (buffer_len == 0) {
|
||||
this->keypads_card_override_valid[player] = false;
|
||||
}
|
||||
|
||||
// generate button
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Generate")) {
|
||||
generate_ea_card(buffer);
|
||||
card_changed = true;
|
||||
}
|
||||
|
||||
if (option.is_active()) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Clear")) {
|
||||
buffer[0] = '\0';
|
||||
card_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// bad card number warning
|
||||
if (!this->keypads_card_override_valid[player] && buffer_len > 0) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(1.f, 0.f, 0.f, 1.f), "Invalid card number");
|
||||
}
|
||||
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
if (player == 0) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
|
||||
if (card_changed) {
|
||||
this->options_dirty = true;
|
||||
option.value = buffer;
|
||||
::Config::getInstance().updateBinding(games_list[games_selected], option);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Card from text files");
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted(
|
||||
"Use text files on disk; its content will be read when Insert Card is pressed.");
|
||||
ImGui::Spacing();
|
||||
|
||||
// get bindings and copy paths
|
||||
@@ -3718,13 +3807,35 @@ namespace overlay::windows {
|
||||
// card settings for each player
|
||||
for (int player = 0; player < 2; player++) {
|
||||
|
||||
// check overrides first
|
||||
const auto &override = (player == 0)
|
||||
? options->at(launcher::Options::Player1Card)
|
||||
: options->at(launcher::Options::Player2Card);
|
||||
|
||||
// custom ID and title
|
||||
ImGui::PushID(("KeypadP" + to_string(player)).c_str());
|
||||
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Player %i", player + 1);
|
||||
const bool tree_open = ImGui::TreeNodeEx("##KeypadTreeNode", ImGuiTreeNodeFlags_DefaultOpen);
|
||||
const bool card_content_valid =
|
||||
!override.is_active() &&
|
||||
this->keypads_card_file_contents_valid[player] &&
|
||||
this->keypads_card_number[player][0] != 0;
|
||||
if (card_content_valid) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, TEXT_COLOR_GREEN);
|
||||
}
|
||||
ImGui::SameLine(0.0f, 0.0f);
|
||||
ImGui::Text("Player %i (card%i.txt)", player + 1, player);
|
||||
if (card_content_valid) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
if (tree_open) {
|
||||
if (override.is_active()) {
|
||||
ImGui::TextDisabled("Disabled - override is in use.");
|
||||
} else {
|
||||
// card path
|
||||
std::string hint = "card" + to_string(player) + ".txt";
|
||||
if (ImGui::InputTextWithHint("Card Path", hint.c_str(),
|
||||
std::string hint = "using .\\card" + to_string(player) + ".txt (default)";
|
||||
ImGui::SetNextItemWidth(TEXT_INPUT_WIDTH);
|
||||
if (ImGui::InputTextWithHint("File 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];
|
||||
@@ -3793,6 +3904,18 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Edit")) {
|
||||
std::filesystem::path path;
|
||||
if (!bindings.card_paths[player].empty()) {
|
||||
path = bindings.card_paths[player];
|
||||
} else {
|
||||
path = (player > 0) ? "card1.txt" : "card0.txt";
|
||||
}
|
||||
// using notepad here in case the file doesn't exist
|
||||
launch_shell("notepad.exe", path.string().c_str());
|
||||
}
|
||||
|
||||
// clear button
|
||||
if (!bindings.card_paths[player].empty()) {
|
||||
ImGui::SameLine();
|
||||
@@ -3803,48 +3926,29 @@ namespace overlay::windows {
|
||||
}
|
||||
|
||||
// verify card number
|
||||
auto card_valid = true;
|
||||
this->keypads_card_file_contents_valid[player] = 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;
|
||||
}
|
||||
}
|
||||
this->keypads_card_file_contents_valid[player] =
|
||||
validate_ea_card(this->keypads_card_number[player]);
|
||||
}
|
||||
|
||||
// card number box
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,
|
||||
card_valid ? ImVec4(1.f, 1.f, 1.f, 1.f) :
|
||||
ImGui::PushStyleColor(
|
||||
ImGuiCol_Text,
|
||||
this->keypads_card_file_contents_valid[player] ? 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::SetNextItemWidth(TEXT_INPUT_WIDTH);
|
||||
ImGui::BeginDisabled();
|
||||
if (this->keypads_card_number[player][0] != 0) {
|
||||
ImGui::Text("Card from file: %s", this->keypads_card_number[player]);
|
||||
} else {
|
||||
ImGui::TextUnformatted("Failed to read file.");
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
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);
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
// render card select browser
|
||||
@@ -3881,12 +3985,14 @@ namespace overlay::windows {
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::SeparatorText("NFC card reader status");
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "NFC card reader status");
|
||||
ImGui::Spacing();
|
||||
if (cfg::CONFIGURATOR_STANDALONE) {
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextWrapped("Test NFC card readers and card insertions over API");
|
||||
ImGui::TextWrapped("Test NFC card readers and card insertions over API.");
|
||||
ImGui::SameLine();
|
||||
ImGui::HelpMarker(
|
||||
"Enable card readers in Advanced tab, under Card Readers section, and restart. "
|
||||
@@ -3897,7 +4003,9 @@ namespace overlay::windows {
|
||||
// show scanned card numbers
|
||||
for (int player = 0; player < 2; player++) {
|
||||
ImGui::PushID(("CardReaderDisp" + to_string(player)).c_str());
|
||||
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Last card detected for player %i", player + 1);
|
||||
if (ImGui::TreeNodeEx(
|
||||
fmt::format("Last card detected for player {}", player + 1).c_str(),
|
||||
ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
|
||||
char card_uid[8];
|
||||
const bool card_present = eamuse_scanned_card_peek_noninvasive(player, card_uid);
|
||||
@@ -3923,6 +4031,8 @@ namespace overlay::windows {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextDisabled("Card not present");
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
if (player == 0) {
|
||||
@@ -3938,7 +4048,9 @@ namespace overlay::windows {
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::SeparatorText("More tips");
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "More tips");
|
||||
ImGui::Spacing();
|
||||
ImGui::BeginDisabled();
|
||||
ImGui::TextWrapped("To debug card reader issues, run spice.exe -cfg in command line and check the log.");
|
||||
@@ -3948,6 +4060,19 @@ namespace overlay::windows {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
bool Config::validate_ea_card(char card_number[16]) {
|
||||
for (int n = 0; n < 16; n++) {
|
||||
char c = card_number[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) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Config::build_option_value_picker_title(const OptionDefinition& definition) {
|
||||
// need to make these all unique since they are also used as ID
|
||||
// if not unique, append ## per ImGui rules to create unique ones
|
||||
@@ -4404,7 +4529,7 @@ namespace overlay::windows {
|
||||
break;
|
||||
}
|
||||
case OptionType::Text: {
|
||||
char buffer[512];
|
||||
char buffer[OPTION_INPUT_TEXT_WIDTH];
|
||||
strncpy(buffer, option.value.c_str(), sizeof(buffer) - 1);
|
||||
buffer[sizeof(buffer) - 1] = '\0';
|
||||
|
||||
|
||||
@@ -94,6 +94,8 @@ namespace overlay::windows {
|
||||
bool keypads_card_select_done = false;
|
||||
ImGui::FileBrowser keypads_card_select_browser[2];
|
||||
char keypads_card_number[2][18] {};
|
||||
bool keypads_card_override_valid[2] = { false, false };
|
||||
bool keypads_card_file_contents_valid[2] = { false, false };
|
||||
|
||||
// presets tab
|
||||
std::vector<ControllerTemplate> templates_cache;
|
||||
@@ -195,12 +197,13 @@ namespace overlay::windows {
|
||||
|
||||
void set_alternating_row_colors(const int row_index);
|
||||
|
||||
bool validate_ea_card(char card_number[16]);
|
||||
|
||||
public:
|
||||
Config(SpiceOverlay *overlay);
|
||||
~Config() override;
|
||||
|
||||
void read_card(int player = -1);
|
||||
void write_card(int player);
|
||||
void build_content() override;
|
||||
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user