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:
bicarus
2026-03-26 01:32:48 -07:00
committed by GitHub
parent 9db2db2340
commit 889e8b43eb
2 changed files with 287 additions and 159 deletions
+187 -62
View File
@@ -58,6 +58,7 @@ namespace overlay::windows {
const auto PROJECT_URL = "https://spice2x.github.io"; const auto PROJECT_URL = "https://spice2x.github.io";
constexpr ImVec4 TEXT_COLOR_GREEN(0.f, 1.f, 0.f, 1.f); constexpr ImVec4 TEXT_COLOR_GREEN(0.f, 1.f, 0.f, 1.f);
constexpr uint32_t OPTION_INPUT_TEXT_WIDTH = 512;
std::unique_ptr<AsioDriverList> asio_driver_list; 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() { void Config::build_content() {
// if standalone then fullscreen window // if standalone then fullscreen window
@@ -3694,14 +3676,121 @@ namespace overlay::windows {
void Config::build_cards() { void Config::build_cards() {
constexpr float TEXT_INPUT_WIDTH = 240.f;
// early quit // early quit
if (this->games_selected < 0 || this->games_selected_name.empty()) { if (this->games_selected < 0 || this->games_selected_name.empty()) {
ImGui::Text("Please select a game first."); ImGui::Text("Please select a game first.");
return; return;
} }
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Card overrides");
ImGui::Spacing(); 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(); ImGui::Spacing();
// get bindings and copy paths // get bindings and copy paths
@@ -3718,13 +3807,35 @@ namespace overlay::windows {
// card settings for each player // card settings for each player
for (int player = 0; player < 2; 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 // custom ID and title
ImGui::PushID(("KeypadP" + to_string(player)).c_str()); 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 // card path
std::string hint = "card" + to_string(player) + ".txt"; std::string hint = "using .\\card" + to_string(player) + ".txt (default)";
if (ImGui::InputTextWithHint("Card Path", hint.c_str(), 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)) this->keypads_card_path[player], sizeof(this->keypads_card_path[0]) - 1))
{ {
bindings.card_paths[player] = this->keypads_card_path[player]; 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 // clear button
if (!bindings.card_paths[player].empty()) { if (!bindings.card_paths[player].empty()) {
ImGui::SameLine(); ImGui::SameLine();
@@ -3803,48 +3926,29 @@ namespace overlay::windows {
} }
// verify card number // verify card number
auto card_valid = true; this->keypads_card_file_contents_valid[player] = true;
if (this->keypads_card_number[player][0] != 0) { if (this->keypads_card_number[player][0] != 0) {
for (int n = 0; n < 16; n++) { this->keypads_card_file_contents_valid[player] =
char c = this->keypads_card_number[player][n]; validate_ea_card(this->keypads_card_number[player]);
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 // card number box
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::PushStyleColor(
card_valid ? ImVec4(1.f, 1.f, 1.f, 1.f) : 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)); ImVec4(1.f, 0.f, 0.f, 1.f));
ImGui::InputTextWithHint("Card Number", "E004010000000000", ImGui::SetNextItemWidth(TEXT_INPUT_WIDTH);
this->keypads_card_number[player], sizeof(this->keypads_card_number[0]) - 1, ImGui::BeginDisabled();
ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsHexadecimal); 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(); ImGui::PopStyleColor();
// write card after edit
if (ImGui::IsItemDeactivatedAfterEdit()) {
write_card(player);
read_card(1 - player);
} }
// help marker ImGui::TreePop();
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 // render card select browser
@@ -3881,12 +3985,14 @@ namespace overlay::windows {
} }
ImGui::Spacing(); 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(); ImGui::Spacing();
if (cfg::CONFIGURATOR_STANDALONE) { if (cfg::CONFIGURATOR_STANDALONE) {
ImGui::AlignTextToFramePadding(); 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::SameLine();
ImGui::HelpMarker( ImGui::HelpMarker(
"Enable card readers in Advanced tab, under Card Readers section, and restart. " "Enable card readers in Advanced tab, under Card Readers section, and restart. "
@@ -3897,7 +4003,9 @@ namespace overlay::windows {
// show scanned card numbers // show scanned card numbers
for (int player = 0; player < 2; player++) { for (int player = 0; player < 2; player++) {
ImGui::PushID(("CardReaderDisp" + to_string(player)).c_str()); 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]; char card_uid[8];
const bool card_present = eamuse_scanned_card_peek_noninvasive(player, card_uid); const bool card_present = eamuse_scanned_card_peek_noninvasive(player, card_uid);
@@ -3923,6 +4031,8 @@ namespace overlay::windows {
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("Card not present"); ImGui::TextDisabled("Card not present");
} }
ImGui::TreePop();
}
ImGui::PopID(); ImGui::PopID();
if (player == 0) { if (player == 0) {
@@ -3938,7 +4048,9 @@ namespace overlay::windows {
} }
ImGui::Spacing(); ImGui::Spacing();
ImGui::SeparatorText("More tips"); ImGui::Separator();
ImGui::Spacing();
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "More tips");
ImGui::Spacing(); ImGui::Spacing();
ImGui::BeginDisabled(); ImGui::BeginDisabled();
ImGui::TextWrapped("To debug card reader issues, run spice.exe -cfg in command line and check the log."); 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(); 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) { std::string Config::build_option_value_picker_title(const OptionDefinition& definition) {
// need to make these all unique since they are also used as ID // need to make these all unique since they are also used as ID
// if not unique, append ## per ImGui rules to create unique ones // if not unique, append ## per ImGui rules to create unique ones
@@ -4404,7 +4529,7 @@ namespace overlay::windows {
break; break;
} }
case OptionType::Text: { case OptionType::Text: {
char buffer[512]; char buffer[OPTION_INPUT_TEXT_WIDTH];
strncpy(buffer, option.value.c_str(), sizeof(buffer) - 1); strncpy(buffer, option.value.c_str(), sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0'; buffer[sizeof(buffer) - 1] = '\0';
+4 -1
View File
@@ -94,6 +94,8 @@ namespace overlay::windows {
bool keypads_card_select_done = false; bool keypads_card_select_done = false;
ImGui::FileBrowser keypads_card_select_browser[2]; ImGui::FileBrowser keypads_card_select_browser[2];
char keypads_card_number[2][18] {}; 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 // presets tab
std::vector<ControllerTemplate> templates_cache; std::vector<ControllerTemplate> templates_cache;
@@ -195,12 +197,13 @@ namespace overlay::windows {
void set_alternating_row_colors(const int row_index); void set_alternating_row_colors(const int row_index);
bool validate_ea_card(char card_number[16]);
public: public:
Config(SpiceOverlay *overlay); Config(SpiceOverlay *overlay);
~Config() override; ~Config() override;
void read_card(int player = -1); void read_card(int player = -1);
void write_card(int player);
void build_content() override; void build_content() override;
}; };