Compare commits

...

9 Commits

Author SHA1 Message Date
bicarus 220b27159b ea3: don't treat failure from dll_entry_init as fatal (#601)
## Link to GitHub Issue or related Pull Request, if one exists
fixes #600
2026-03-30 02:31:08 -07:00
bicarus b06a0a7d47 overlay: card override input should be disabled when overridden with cmd line (#599) 2026-03-29 20:36:42 -07:00
bicarus d66af8e16b overlay: options tab(s) clean up (#598)
## Description of change
Merge option name and command line parameter columns.
Expand the size of widget column.
In Search tab, add a new column to display the category.
Add a highlight effect to rows when hovered over - apply this to all
tables in the configurator UI.
2026-03-28 18:21:53 -07:00
bicarus-dev ad366ecdd0 update fatal crash message 2026-03-27 18:46:08 -07:00
llm96 22cb4d689c utils: print DLL info for early hooks (#597)
## Link to GitHub Issue or related Pull Request, if one exists
#596

## Description of change
Adds call to `libutils::print_dll_info` for early hooks too.

## Testing
```
[2026/03/27 09:51:16] I:launcher: SpiceTools Bootstrap (x64) (spice2x)
[2026/03/27 09:51:16] I:launcher: 1.0-V-2026-03-27T07:46:10
[2026/03/27 09:51:16] I:launcher: spice2x is free & open source; if you paid money for it, you got scammed
[2026/03/27 09:51:16] I:launcher: visit https://spice2x.github.io to download the latest version for free
[2026/03/27 09:51:16] I:launcher: arguments:
                                      [...]
                                      -z C:\Users\Noe\AppData\Local\Programs\SpiceTools\hooks\64\mempatcher64.dll
                                      [...]
[2026/03/27 09:51:16] I:launcher: loading early hook DLL C:\Users\Noe\AppData\Local\Programs\SpiceTools\hooks\64\mempatcher64.dll
[2026/03/27 09:51:16] I:superexit: enabled
[2026/03/27 09:51:16] I:libutils: DLL info for mempatcher64.dll: CompanyName = aixxe, ProductName = mempatcher, Version = 4975386c637a86e8daa89d479a0c67ab28d792ec
```
2026-03-27 12:19:32 -07:00
bicarus b38ea463aa utils: print out DLL metadata (#596)
## Link to GitHub Issue or related Pull Request, if one exists
#592 

## Description of change
* when custom DLLs are detected (`d3d9.dll`) print out some basic data
from the PE header
* for our own NVIDIA DLL stubs, add manifest files to identify ourselves
* as a bonus, when DLL hooks are loaded from `-k`, print DLL info there
as well

This is mainly to detect when DXVK is in use.
2026-03-27 02:34:56 -07:00
bicarus 056d1b2d90 ea3: warn about missing ea3 config (#595)
Show better warning message when `ea3-config.xml` / `eamuse-config.xml`
is missing. Right now we just say `failed to load prop/eamuseconfig.xml`
which is really confusing when people look for `eamuse-config.xml` that
never existed in the first place.
2026-03-27 01:56:32 -07:00
bicarus 37693916d9 overlay: show warning when binding analog axis as a button for TT/knobs (#594)
## Link to GitHub Issue or related Pull Request, if one exists
#345 

## Description of change
When X or Y axis is being bound to any button labeled `TT` or `Knob` or
`VOL-`, show a warning and ask if the user really meant to do that,
encouraging them to use the Analog tab instead.
2026-03-26 21:17:51 -07:00
bicarus 889e8b43eb 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)
2026-03-26 01:32:48 -07:00
13 changed files with 612 additions and 191 deletions
+9 -4
View File
@@ -266,6 +266,7 @@ set_source_files_properties(cfg/manifest.rc PROPERTIES LANGUAGE RC)
set_source_files_properties(cfg/icon.rc PROPERTIES LANGUAGE RC)
set_source_files_properties(cfg/Win32D.rc PROPERTIES LANGUAGE RC)
set_source_files_properties(build/manifest64.rc PROPERTIES LANGUAGE RC)
set_source_files_properties(stubs/manifest.rc PROPERTIES LANGUAGE RC)
# sources
#########
@@ -813,7 +814,8 @@ endif()
# nvcuda.dll
set(SOURCE_FILES stubs/nvcuda.cpp)
add_library(spicetools_stubs_nvcuda SHARED ${SOURCE_FILES} stubs/nvcuda.def)
set(RESOURCE_FILES stubs/manifest.rc)
add_library(spicetools_stubs_nvcuda SHARED ${SOURCE_FILES} ${RESOURCE_FILES} stubs/nvcuda.def)
set_target_properties(spicetools_stubs_nvcuda PROPERTIES PREFIX "")
set_target_properties(spicetools_stubs_nvcuda PROPERTIES OUTPUT_NAME "nvcuda")
@@ -823,7 +825,8 @@ endif()
# nvcuvid.dll
set(SOURCE_FILES stubs/nvcuvid.cpp)
add_library(spicetools_stubs_nvcuvid SHARED ${SOURCE_FILES} stubs/nvcuvid.def)
set(RESOURCE_FILES stubs/manifest.rc)
add_library(spicetools_stubs_nvcuvid SHARED ${SOURCE_FILES} ${RESOURCE_FILES} stubs/nvcuvid.def)
set_target_properties(spicetools_stubs_nvcuvid PROPERTIES PREFIX "")
set_target_properties(spicetools_stubs_nvcuvid PROPERTIES OUTPUT_NAME "nvcuvid")
@@ -833,7 +836,8 @@ endif()
# nvEncodeAPI64.dll
set(SOURCE_FILES stubs/nvEncodeAPI64.cpp)
add_library(spicetools_stubs_nvEncodeAPI64 SHARED ${SOURCE_FILES} stubs/nvEncodeAPI64.def)
set(RESOURCE_FILES stubs/manifest.rc)
add_library(spicetools_stubs_nvEncodeAPI64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} stubs/nvEncodeAPI64.def)
set_target_properties(spicetools_stubs_nvEncodeAPI64 PROPERTIES PREFIX "")
set_target_properties(spicetools_stubs_nvEncodeAPI64 PROPERTIES OUTPUT_NAME "nvEncodeAPI64")
@@ -843,7 +847,8 @@ endif()
# cpusbxpkm.dll (32 bit)
set(SOURCE_FILES stubs/cpusbxpkm.cpp)
add_library(spicetools_stubs_cpusbxpkm SHARED ${SOURCE_FILES} stubs/cpusbxpkm.def)
set(RESOURCE_FILES stubs/manifest.rc)
add_library(spicetools_stubs_cpusbxpkm SHARED ${SOURCE_FILES} ${RESOURCE_FILES} stubs/cpusbxpkm.def)
set_target_properties(spicetools_stubs_cpusbxpkm PROPERTIES PREFIX "")
set_target_properties(spicetools_stubs_cpusbxpkm PROPERTIES OUTPUT_NAME "cpusbxpkm")
+27 -3
View File
@@ -217,7 +217,23 @@ namespace avs {
ea3_config_name = "prop/eamuse-config.xml";
}
log_info("avs-ea3", "booting (using {})", ea3_config_name);
if (avs::core::file_exists(ea3_config_name)) {
log_info("avs-ea3", "booting (using {})", ea3_config_name);
} else {
log_warning("avs-ea3", "looked for the following files in order:");
log_warning("avs-ea3", " * prop/ea3-config.xml");
log_warning("avs-ea3", " * prop/ea3-cfg.xml");
if (avs::game::DLL_NAME == "beatstream1.dll") {
log_warning("avs-ea3", " * prop/ea3-config-1.xml");
}
if (avs::game::DLL_NAME == "beatstream2.dll") {
log_warning("avs-ea3", " * prop/ea3-config-2.xml");
}
log_warning("avs-ea3", " * prop/eamuse-config.xml");
log_warning("avs-ea3", "none of the files above were found, game will fail to boot");
log_warning("avs-ea3", "usually, this means your game data is incomplete");
log_fatal("avs-ea3", "no ea3 config file found in prop directory; check log messages");
}
// remember new config path
CFG_PATH = ea3_config_name;
@@ -554,9 +570,17 @@ namespace avs {
auto app_param = avs::core::property_search_safe(app_config, nullptr, "/param");
// call the game init
log_info("avs-ea3", "calling entry init (init code = {})", init_code_str);
log_info("avs-ea3", "calling dll_entry_init (init code = {})", init_code_str);
if (!avs::game::entry_init(init_code_str.data(), app_param)) {
log_fatal("avs-ea3", "entry init failed :(");
for (size_t i = 0; i < 255; i++) {
log_warning("avs-ea3", "dll_entry_init failed :(");
}
deferredlogs::defer_error_messages({
"dll_entry_init failed",
" init code: " + init_code_str,
" this is a fatal error that causes boot failure",
" most likely, your prop files have errors"
});
}
// accommodate changes to soft id code
+2
View File
@@ -1507,6 +1507,7 @@ int main_implementation(int argc, char *argv[]) {
// early hooks
for (auto &hook : early_hooks) {
log_info("launcher", "loading early hook DLL {}", hook);
libutils::print_dll_info(hook);
HMODULE module;
if (!(module = libutils::try_library(hook))) {
log_warning("launcher", "failed to load early hook {}: {}", hook, get_last_error_string());
@@ -2307,6 +2308,7 @@ int main_implementation(int argc, char *argv[]) {
// load hooks
for (auto &hook : game_hooks) {
log_info("launcher", "loading hook DLL {}", hook);
libutils::print_dll_info(hook);
HMODULE module;
if (!(module = libutils::try_library(hook))) {
log_warning("launcher", "failed to load hook {}: {}", hook, get_last_error_string());
+13
View File
@@ -163,4 +163,17 @@ namespace ImGui {
ImGui::PopID();
return clicked;
}
void InvisibleTableRowSelectable() {
ImGui::TableSetColumnIndex(0);
ImGui::PushStyleColor(ImGuiCol_Header, 0);
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, 0);
ImGui::PushStyleColor(ImGuiCol_HeaderActive, 0);
ImGui::Selectable("##row", false,
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap);
ImGui::PopStyleColor(3);
if (ImGui::IsItemHovered()) {
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, IM_COL32(200, 200, 200, 24));
}
}
}
+1
View File
@@ -20,4 +20,5 @@ namespace ImGui {
void TextTruncated(const std::string& p_text, float p_truncated_width);
bool DeleteButton(const std::string& tooltip);
bool ClearButton(const std::string& tooltip);
void InvisibleTableRowSelectable();
}
+1 -1
View File
@@ -194,7 +194,7 @@ void overlay::SpiceOverlay::init() {
// Text
colors[ImGuiCol_Text] = ImVec4(1.00f, 0.90f, 0.90f, 1.00f); // Slight pinkish tint to off-white
colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.40f, 0.40f, 1.00f);
colors[ImGuiCol_TextDisabled] = ImVec4(0.35f, 0.26f, 0.26f, 1.00f);
// Backgrounds
colors[ImGuiCol_WindowBg] = ImVec4(0.08f, 0.07f, 0.07f, 1.00f); // Deep charcoal
+415 -175
View File
@@ -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
@@ -703,12 +685,18 @@ namespace overlay::windows {
// primary
build_button(name, primary_button, &primary_button, button_it, button_it_max, 0);
ImGui::PushID(&primary_button);
ImGui::InvisibleTableRowSelectable();
ImGui::PopID();
// alternatives
int alt_index = 1; // 0 is primary
for (auto &alt : primary_button.getAlternatives()) {
if (alt.isValid()) {
build_button(name, primary_button, &alt, button_it, button_it_max, alt_index);
ImGui::PushID(&alt);
ImGui::InvisibleTableRowSelectable();
ImGui::PopID();
}
alt_index++;
}
@@ -864,6 +852,51 @@ namespace overlay::windows {
ImGui::HelpTooltip("Bind a button to a device using Windows RawInput API.");
}
if (this->analog_as_button_warning_show_next_frame.first == button->getName() &&
this->analog_as_button_warning_show_next_frame.second == alt_index) {
this->analog_as_button_warning_show_next_frame = std::make_pair("", 0);
ImGui::OpenPopup("Warning##AnalogAsButtonWarning");
// stop bind-many
RI_MGR->devices_midi_freeze(false);
this->buttons_bind_active = false;
this->buttons_many_active = false;
}
if (ImGui::BeginPopupModal("Warning##AnalogAsButtonWarning", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::TextUnformatted(
"You just bound an analog axis to a digital button.\n"
"\n"
"If your controller supports analog input (turntable,\n"
"knob, slider) you should instead use the Analog tab and\n"
"bind as an analog axis.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// yes
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.2f, 0.9f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.7f, 0.2f, 1.0f));
ImGui::AlignTextToFramePadding();
ImGui::Bullet();
ImGui::SameLine();
if (ImGui::Button("I understand, undo binding")) {
clear_button(button, alt_index);
ImGui::CloseCurrentPopup();
}
ImGui::PopStyleColor(3);
// no
ImGui::AlignTextToFramePadding();
ImGui::Bullet();
ImGui::SameLine();
if (ImGui::Button("Ignore and continue anyway")) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
bind_button_popup(bind_name, button, button_it_max, alt_index);
// naive binding
@@ -1208,6 +1241,22 @@ namespace overlay::windows {
buttons_bind_active = false;
inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false);
// usually, turntables are X, knobs are X and Y
// gamepad triggers are Z, and Rx/Ry are right thumb stick
// one day we will label all I/O modules and flag which one are button-as-analog
// so that we don't have to do string comparions like below
if ((value_name == "X" || value_name == "Y") &&
(button->getName().find("Press") == std::string::npos) && // museca
(button->getName().find("Slowdown") == std::string::npos) && // bishibashi
(button->getName().find("TT+") != std::string::npos ||
button->getName().find("TT-") != std::string::npos || // iidx
button->getName().find("Knob") != std::string::npos || // gitadora guitar
button->getName().find("Disk") != std::string::npos || // museca, bishibashi
button->getName().find("VOL-") != std::string::npos)) { // sdvx
this->analog_as_button_warning_show_next_frame =
std::make_pair(button->getName(), alt_index);
}
break;
} else if (diff > 0.3f) {
@@ -2127,6 +2176,9 @@ namespace overlay::windows {
edit_analog_popup(analog);
// row hover detection (invisible selectable that spans entire row)
ImGui::InvisibleTableRowSelectable();
// clean up
ImGui::PopID();
}
@@ -2747,13 +2799,21 @@ namespace overlay::windows {
}
build_light(light, &light, i, 0);
ImGui::PushID(&light);
ImGui::InvisibleTableRowSelectable();
ImGui::PopID();
int alt_index = 1;
for (auto &alt : light.getAlternatives()) {
if (alt.isValid()) {
build_light(light, &alt, i, alt_index);
ImGui::PushID(&alt);
ImGui::InvisibleTableRowSelectable();
ImGui::PopID();
}
alt_index++;
}
}
if (table_begin) {
ImGui::EndTable();
@@ -3694,14 +3754,133 @@ 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::BeginDisabled(option.disabled);
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;
}
ImGui::EndDisabled();
if (!option.disabled) {
// generate button
ImGui::SameLine();
if (ImGui::Button("Generate")) {
generate_ea_card(buffer);
card_changed = true;
}
// clear button
if (option.is_active()) {
ImGui::SameLine();
if (ImGui::Button("Clear")) {
buffer[0] = '\0';
card_changed = true;
}
}
} else {
if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) {
ImGui::HelpTooltip(
"This option cannot 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.");
}
}
// 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,133 +3897,148 @@ 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);
// 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;
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();
}
// 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!");
if (tree_open) {
if (override.is_active()) {
ImGui::TextDisabled("Disabled - override is in use.");
} else {
// card path
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];
bindings_updated = true;
}
// card path file selector
ImGui::SameLine();
if (ImGui::Button("Open...")) {
// 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!");
// standalone version opens native file browser
if (cfg::CONFIGURATOR_STANDALONE && !keypads_card_select) {
// card path file selector
ImGui::SameLine();
if (ImGui::Button("Open...")) {
// run in separate thread otherwise we get a crash
keypads_card_select_done = false;
keypads_card_select = new std::thread([this, bindings, player, game] {
// standalone version opens native file browser
if (cfg::CONFIGURATOR_STANDALONE && !keypads_card_select) {
// open dialog to get path
auto ofn_path = std::make_unique<wchar_t[]>(512);
ofn_path[0] = L'\0';
OPENFILENAMEW ofn {};
ofn.lStructSize = sizeof(ofn);
ofn.lpstrFile = ofn_path.get();
ofn.nMaxFile = 512;
ofn.Flags = OFN_EXPLORER | OFN_NOCHANGEDIR;
ofn.lpstrDefExt = L"txt";
ofn.lpstrInitialDir = L".";
// run in separate thread otherwise we get a crash
keypads_card_select_done = false;
keypads_card_select = new std::thread([this, bindings, player, game] {
// check for success
auto guard = rawinput::set_os_window_focus_guard();
if (GetSaveFileNameW(&ofn)) {
// open dialog to get path
auto ofn_path = std::make_unique<wchar_t[]>(512);
ofn_path[0] = L'\0';
OPENFILENAMEW ofn {};
ofn.lStructSize = sizeof(ofn);
ofn.lpstrFile = ofn_path.get();
ofn.nMaxFile = 512;
ofn.Flags = OFN_EXPLORER | OFN_NOCHANGEDIR;
ofn.lpstrDefExt = L"txt";
ofn.lpstrInitialDir = L".";
// 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();
// check for success
auto guard = rawinput::set_os_window_focus_guard();
if (GetSaveFileNameW(&ofn)) {
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");
}
// 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;
});
}
// 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;
// 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();
}
}
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();
if (ImGui::Button("Clear")) {
bindings.card_paths[player] = "";
bindings_updated = true;
}
}
// verify card number
this->keypads_card_file_contents_valid[player] = true;
if (this->keypads_card_number[player][0] != 0) {
this->keypads_card_file_contents_valid[player] =
validate_ea_card(this->keypads_card_number[player]);
}
// card number box
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::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();
}
}
// 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);
ImGui::TreePop();
}
// render card select browser
@@ -3881,12 +4075,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,31 +4093,35 @@ 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);
if (card_present) {
ImGui::AlignTextToFramePadding();
const auto card_str = bin2hex(card_uid, 8);
ImGui::Text(
"%s %s %s %s",
card_str.substr(0, 4).c_str(),
card_str.substr(4, 4).c_str(),
card_str.substr(8, 4).c_str(),
card_str.substr(12, 4).c_str()
);
ImGui::SameLine();
if (ImGui::Button("Copy")) {
clipboard::copy_text(card_str);
char card_uid[8];
const bool card_present = eamuse_scanned_card_peek_noninvasive(player, card_uid);
if (card_present) {
ImGui::AlignTextToFramePadding();
const auto card_str = bin2hex(card_uid, 8);
ImGui::Text(
"%s %s %s %s",
card_str.substr(0, 4).c_str(),
card_str.substr(4, 4).c_str(),
card_str.substr(8, 4).c_str(),
card_str.substr(12, 4).c_str()
);
ImGui::SameLine();
if (ImGui::Button("Copy")) {
clipboard::copy_text(card_str);
}
ImGui::SameLine();
if (ImGui::Button("Clear")) {
eamuse_scanned_card_clear(player);
}
} else {
ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("Card not present");
}
ImGui::SameLine();
if (ImGui::Button("Clear")) {
eamuse_scanned_card_clear(player);
}
} else {
ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("Card not present");
ImGui::TreePop();
}
ImGui::PopID();
@@ -3938,7 +4138,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 +4150,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
@@ -4213,17 +4428,19 @@ namespace overlay::windows {
// render table
// tables must share the same ID to have synced column settings
if (ImGui::BeginTable("OptionsTable", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Option", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn(
"CMD Line Parameter",
ImGuiTableColumnFlags_WidthFixed,
overlay::apply_scaling(216));
ImGui::TableSetupColumn(
"Setting",
ImGuiTableColumnFlags_WidthFixed,
overlay::apply_scaling(240));
const int num_columns = (filter != nullptr) ? 3 : 2;
if (ImGui::BeginTable("OptionsTable", num_columns, ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg)) {
auto widget_col_width = 0;
if (filter != nullptr) {
ImGui::TableSetupColumn("Category", ImGuiTableColumnFlags_WidthFixed, overlay::apply_scaling(160));
ImGui::TableSetupColumn("Option", ImGuiTableColumnFlags_WidthStretch);
widget_col_width = overlay::apply_scaling(220);
} else {
ImGui::TableSetupColumn("Option", ImGuiTableColumnFlags_WidthStretch);
widget_col_width = overlay::apply_scaling(264);
}
ImGui::TableSetupColumn("Setting", ImGuiTableColumnFlags_WidthFixed, widget_col_width);
// iterate options
options_count = 0;
for (auto &option : *options) {
@@ -4271,10 +4488,24 @@ namespace overlay::windows {
ImGui::PushID(&option);
ImGui::TableNextRow();
// category (only shown for search results)
if (filter != nullptr) {
ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
if (definition.category.empty()) {
ImGui::TextDisabled("-");
} else {
ImGui::TextDisabled(definition.category.c_str());
}
}
// option name
ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
ImGui::Indent(INDENT);
if (filter == nullptr) {
// add indent to make options align under category header
ImGui::Indent(INDENT);
}
if (option.is_active()) {
// active option
if (option.disabled || definition.disabled) {
@@ -4290,21 +4521,22 @@ namespace overlay::windows {
// normal text
ImGui::TextUnformatted(definition.title.c_str());
}
ImGui::Unindent(INDENT);
if (filter == nullptr) {
ImGui::Unindent(INDENT);
}
if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) {
ImGui::HelpTooltip(definition.desc.c_str());
}
// command line parameter
ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
std::string param = "-";
if (definition.display_name.empty()) {
param += definition.name;
} else {
param += definition.display_name;
}
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.f), "%s", param.c_str());
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.27f, 0.27f, 0.27f, 1.f), " %s", param.c_str());
if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) {
const auto help =
param +
@@ -4325,6 +4557,10 @@ namespace overlay::windows {
ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
}
if (definition.type != OptionType::Bool) {
// take up width of column, minus some room for clear button and Pick button
ImGui::SetNextItemWidth(widget_col_width - overlay::apply_scaling(70));
}
switch (definition.type) {
case OptionType::Bool: {
bool state = !option.value.empty();
@@ -4404,7 +4640,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';
@@ -4499,7 +4735,7 @@ namespace overlay::windows {
if (option.disabled && !definition.disabled) {
ImGui::SameLine();
ImGui::HelpMarker(
"This option can not be edited because it was overriden by command-line options.\n"
"This option cannot 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.");
}
@@ -4541,6 +4777,9 @@ namespace overlay::windows {
}
}
// row hover detection (invisible selectable that spans entire row)
ImGui::InvisibleTableRowSelectable();
// next item
ImGui::PopID();
}
@@ -5053,6 +5292,7 @@ namespace overlay::windows {
}
}
ImGui::InvisibleTableRowSelectable();
ImGui::PopID();
}
+6 -1
View File
@@ -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;
@@ -135,6 +137,8 @@ namespace overlay::windows {
std::thread *file_picker_thread = nullptr;
bool file_picker_done = false;
std::pair<std::string, int> analog_as_button_warning_show_next_frame = { "", 0 };
void build_buttons(const std::string &name, std::vector<Button> *buttons, int min = 0, int max = -1);
void build_button(
const std::string &name,
@@ -195,12 +199,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;
};
@@ -844,6 +844,10 @@ namespace overlay::windows {
ImGui::TextUnformatted(patch_checked ? "ON" : "off");
ImGui::EndDisabled();
}
// row hover detection (invisible selectable that spans entire row)
ImGui::InvisibleTableRowSelectable();
ImGui::PopID();
}
+20
View File
@@ -0,0 +1,20 @@
1 VERSIONINFO
FILEVERSION 1,0,0,0
PRODUCTVERSION 1,0,0,0
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904E4"
BEGIN
VALUE "CompanyName", "SpiceTools"
VALUE "FileDescription", "SpiceTools DLL Stub"
VALUE "FileVersion", "1.0.0.0"
VALUE "ProductName", "SpiceTools DLL Stub"
VALUE "ProductVersion", "1.0.0.0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x0409, 1252
END
END
+106 -1
View File
@@ -9,6 +9,16 @@
#include "peb.h"
#include "util/fileutils.h"
#include "util/dependencies.h"
#include "util/unique_plain_ptr.h"
#define DEBUG_VERBOSE 0
#if DEBUG_VERBOSE
#define log_debug(module, format_str, ...) logger::push( \
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
#else
#define log_debug(module, format_str, ...)
#endif
std::filesystem::path libutils::module_file_name(HMODULE module) {
std::wstring buf;
@@ -361,11 +371,106 @@ void libutils::check_duplicate_dlls() {
void libutils::warn_if_dll_exists(const std::string &file_name) {
if (fileutils::file_exists(MODULE_PATH / file_name)) {
log_info("libutils", "found user-supplied {} in modules directory", file_name);
libutils::print_dll_info(MODULE_PATH / file_name);
return;
}
const auto &spice_bin_path = libutils::module_file_name(nullptr).parent_path();
if (fileutils::file_exists(spice_bin_path / file_name)) {
log_info("libutils", "found user-supplied {} next to spice executable path", file_name);
libutils::print_dll_info(spice_bin_path / file_name);
return;
}
}
}
void libutils::print_dll_info(std::filesystem::path filename) {
DWORD handle;
const auto size = GetFileVersionInfoSizeA(filename.string().c_str(), &handle);
if (size == 0) {
log_debug(
"libutils",
"GetFileVersionInfoSizeA failed for {}: {}",
filename.filename().string(),
get_last_error_string());
return;
}
auto data = util::make_unique_plain<VOID>(size);
if (!GetFileVersionInfoA(filename.string().c_str(), handle, size, data.get())) {
log_debug(
"libutils",
"GetFileVersionInfoA failed for {}: {}",
filename.filename().string(),
get_last_error_string());
return;
}
struct LANGANDCODEPAGE {
WORD wLanguage;
WORD wCodePage;
} *lpTranslate = nullptr;
UINT cbTranslate = 0;
if (!VerQueryValueA(data.get(), "\\VarFileInfo\\Translation", (LPVOID*)&lpTranslate, &cbTranslate)) {
log_debug(
"libutils",
"VerQueryValueA failed for {}: {}",
filename.filename().string(),
get_last_error_string());
return;
}
if (cbTranslate == 0 || lpTranslate == nullptr) {
log_debug(
"libutils",
"VerQueryValueA returned invalid results for {}",
filename.filename().string());
return;
}
// pick language - prefer English
WORD lang = 0, codepage = 0;
for (UINT i = 0; i < cbTranslate / sizeof(*lpTranslate); i++) {
if (lpTranslate[i].wLanguage == 0x0409) { // en-us
lang = lpTranslate[i].wLanguage;
codepage = lpTranslate[i].wCodePage;
break;
}
}
if (lang == 0) {
lang = lpTranslate[0].wLanguage;
codepage = lpTranslate[0].wCodePage;
}
// StringFileInfo helper
auto query_string = [&](const char* key) -> std::string {
std::string subBlock = fmt::format(
"\\StringFileInfo\\{:04x}{:04x}\\{}",
lang, codepage, key
);
char* value = nullptr;
UINT size_out = 0;
if (VerQueryValueA(data.get(), subBlock.c_str(), (LPVOID*)&value, &size_out) && value) {
return value;
}
log_debug(
"libutils",
"VerQueryValueA({}) failed for {}: {}",
subBlock,
filename.filename().string(),
get_last_error_string());
return "";
};
const auto company_name = query_string("CompanyName");
const auto product_name = query_string("ProductName");
const auto version_str = query_string("FileVersion");
log_info(
"libutils",
"DLL info for {}: CompanyName = {}, ProductName = {}, Version = {}",
filename.filename().string(),
company_name.empty() ? "?" : company_name,
product_name.empty() ? "?" : product_name,
version_str.empty() ? "?" : version_str);
}
+2
View File
@@ -77,4 +77,6 @@ namespace libutils {
intptr_t rva2offset(const std::filesystem::path &path, intptr_t rva);
intptr_t offset2rva(IMAGE_NT_HEADERS *nt_headers, intptr_t offset);
intptr_t offset2rva(const std::filesystem::path &path, intptr_t offset);
void print_dll_info(std::filesystem::path filename);
}
+6 -6
View File
@@ -62,9 +62,9 @@ void show_popup_for_crash() {
text += "Game has crashed.\n\n";
text += "Check log.txt and look for error messages near the end of file.\n\n";
text += "Unsure what to do next?\n";
text += " * update spice2x to the latest version\n";
text += " * check the FAQ on spice2x github wiki\n";
text += " * do NOT screenshot this, instead, share log.txt with someone and ask for help\n\n";
text += " * Update spice2x to the latest version\n";
text += " * Check the FAQ on spice2x github wiki\n";
text += " * Do NOT screenshot this, instead, share log.txt with someone and ask for help\n\n";
text += "Press Enter, Esc, Alt+F4, or click OK to exit. Otherwise, game will close in 30 seconds.";
show_popup(text);
}
@@ -76,9 +76,9 @@ void show_popup_for_fatal_error(std::string message) {
text += message;
text += "----------------------------------------------------------\n\n";
text += "Unsure what to do next?\n";
text += " * update spice2x to the latest version\n";
text += " * check the FAQ on spice2x github wiki\n";
text += " * do NOT screenshot this, instead, share log.txt with someone and ask for help\n\n";
text += " * Update spice2x to the latest version\n";
text += " * Check the FAQ on spice2x github wiki\n";
text += " * Do NOT screenshot this, instead, share log.txt with someone and ask for help\n\n";
text += "Press Enter, Esc, Alt+F4, or click OK to exit. Otherwise, game will close in 30 seconds.";
show_popup(text);
}