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/icon.rc PROPERTIES LANGUAGE RC)
set_source_files_properties(cfg/Win32D.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(build/manifest64.rc PROPERTIES LANGUAGE RC)
set_source_files_properties(stubs/manifest.rc PROPERTIES LANGUAGE RC)
# sources # sources
######### #########
@@ -813,7 +814,8 @@ endif()
# nvcuda.dll # nvcuda.dll
set(SOURCE_FILES stubs/nvcuda.cpp) 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 PREFIX "")
set_target_properties(spicetools_stubs_nvcuda PROPERTIES OUTPUT_NAME "nvcuda") set_target_properties(spicetools_stubs_nvcuda PROPERTIES OUTPUT_NAME "nvcuda")
@@ -823,7 +825,8 @@ endif()
# nvcuvid.dll # nvcuvid.dll
set(SOURCE_FILES stubs/nvcuvid.cpp) 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 PREFIX "")
set_target_properties(spicetools_stubs_nvcuvid PROPERTIES OUTPUT_NAME "nvcuvid") set_target_properties(spicetools_stubs_nvcuvid PROPERTIES OUTPUT_NAME "nvcuvid")
@@ -833,7 +836,8 @@ endif()
# nvEncodeAPI64.dll # nvEncodeAPI64.dll
set(SOURCE_FILES stubs/nvEncodeAPI64.cpp) 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 PREFIX "")
set_target_properties(spicetools_stubs_nvEncodeAPI64 PROPERTIES OUTPUT_NAME "nvEncodeAPI64") set_target_properties(spicetools_stubs_nvEncodeAPI64 PROPERTIES OUTPUT_NAME "nvEncodeAPI64")
@@ -843,7 +847,8 @@ endif()
# cpusbxpkm.dll (32 bit) # cpusbxpkm.dll (32 bit)
set(SOURCE_FILES stubs/cpusbxpkm.cpp) 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 PREFIX "")
set_target_properties(spicetools_stubs_cpusbxpkm PROPERTIES OUTPUT_NAME "cpusbxpkm") set_target_properties(spicetools_stubs_cpusbxpkm PROPERTIES OUTPUT_NAME "cpusbxpkm")
+26 -2
View File
@@ -217,7 +217,23 @@ namespace avs {
ea3_config_name = "prop/eamuse-config.xml"; ea3_config_name = "prop/eamuse-config.xml";
} }
if (avs::core::file_exists(ea3_config_name)) {
log_info("avs-ea3", "booting (using {})", 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 // remember new config path
CFG_PATH = ea3_config_name; CFG_PATH = ea3_config_name;
@@ -554,9 +570,17 @@ namespace avs {
auto app_param = avs::core::property_search_safe(app_config, nullptr, "/param"); auto app_param = avs::core::property_search_safe(app_config, nullptr, "/param");
// call the game init // 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)) { 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 // accommodate changes to soft id code
+2
View File
@@ -1507,6 +1507,7 @@ int main_implementation(int argc, char *argv[]) {
// early hooks // early hooks
for (auto &hook : early_hooks) { for (auto &hook : early_hooks) {
log_info("launcher", "loading early hook DLL {}", hook); log_info("launcher", "loading early hook DLL {}", hook);
libutils::print_dll_info(hook);
HMODULE module; HMODULE module;
if (!(module = libutils::try_library(hook))) { if (!(module = libutils::try_library(hook))) {
log_warning("launcher", "failed to load early hook {}: {}", hook, get_last_error_string()); 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 // load hooks
for (auto &hook : game_hooks) { for (auto &hook : game_hooks) {
log_info("launcher", "loading hook DLL {}", hook); log_info("launcher", "loading hook DLL {}", hook);
libutils::print_dll_info(hook);
HMODULE module; HMODULE module;
if (!(module = libutils::try_library(hook))) { if (!(module = libutils::try_library(hook))) {
log_warning("launcher", "failed to load hook {}: {}", hook, get_last_error_string()); log_warning("launcher", "failed to load hook {}: {}", hook, get_last_error_string());
+13
View File
@@ -163,4 +163,17 @@ namespace ImGui {
ImGui::PopID(); ImGui::PopID();
return clicked; 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); void TextTruncated(const std::string& p_text, float p_truncated_width);
bool DeleteButton(const std::string& tooltip); bool DeleteButton(const std::string& tooltip);
bool ClearButton(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 // Text
colors[ImGuiCol_Text] = ImVec4(1.00f, 0.90f, 0.90f, 1.00f); // Slight pinkish tint to off-white 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 // Backgrounds
colors[ImGuiCol_WindowBg] = ImVec4(0.08f, 0.07f, 0.07f, 1.00f); // Deep charcoal colors[ImGuiCol_WindowBg] = ImVec4(0.08f, 0.07f, 0.07f, 1.00f); // Deep charcoal
+314 -74
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
@@ -703,12 +685,18 @@ namespace overlay::windows {
// primary // primary
build_button(name, primary_button, &primary_button, button_it, button_it_max, 0); build_button(name, primary_button, &primary_button, button_it, button_it_max, 0);
ImGui::PushID(&primary_button);
ImGui::InvisibleTableRowSelectable();
ImGui::PopID();
// alternatives // alternatives
int alt_index = 1; // 0 is primary int alt_index = 1; // 0 is primary
for (auto &alt : primary_button.getAlternatives()) { for (auto &alt : primary_button.getAlternatives()) {
if (alt.isValid()) { if (alt.isValid()) {
build_button(name, primary_button, &alt, button_it, button_it_max, alt_index); build_button(name, primary_button, &alt, button_it, button_it_max, alt_index);
ImGui::PushID(&alt);
ImGui::InvisibleTableRowSelectable();
ImGui::PopID();
} }
alt_index++; alt_index++;
} }
@@ -864,6 +852,51 @@ namespace overlay::windows {
ImGui::HelpTooltip("Bind a button to a device using Windows RawInput API."); 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); bind_button_popup(bind_name, button, button_it_max, alt_index);
// naive binding // naive binding
@@ -1208,6 +1241,22 @@ namespace overlay::windows {
buttons_bind_active = false; buttons_bind_active = false;
inc_buttons_many_index(button_it_max); inc_buttons_many_index(button_it_max);
RI_MGR->devices_midi_freeze(false); 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; break;
} else if (diff > 0.3f) { } else if (diff > 0.3f) {
@@ -2127,6 +2176,9 @@ namespace overlay::windows {
edit_analog_popup(analog); edit_analog_popup(analog);
// row hover detection (invisible selectable that spans entire row)
ImGui::InvisibleTableRowSelectable();
// clean up // clean up
ImGui::PopID(); ImGui::PopID();
} }
@@ -2747,13 +2799,21 @@ namespace overlay::windows {
} }
build_light(light, &light, i, 0); build_light(light, &light, i, 0);
ImGui::PushID(&light);
ImGui::InvisibleTableRowSelectable();
ImGui::PopID();
int alt_index = 1; int alt_index = 1;
for (auto &alt : light.getAlternatives()) { for (auto &alt : light.getAlternatives()) {
if (alt.isValid()) { if (alt.isValid()) {
build_light(light, &alt, i, alt_index); build_light(light, &alt, i, alt_index);
ImGui::PushID(&alt);
ImGui::InvisibleTableRowSelectable();
ImGui::PopID();
} }
alt_index++; alt_index++;
} }
} }
if (table_begin) { if (table_begin) {
ImGui::EndTable(); ImGui::EndTable();
@@ -3694,14 +3754,133 @@ 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::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(); ImGui::Spacing();
// get bindings and copy paths // get bindings and copy paths
@@ -3718,13 +3897,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 +3994,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 +4016,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 +4075,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 +4093,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 +4121,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 +4138,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 +4150,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
@@ -4213,16 +4428,18 @@ namespace overlay::windows {
// render table // render table
// tables must share the same ID to have synced column settings // tables must share the same ID to have synced column settings
if (ImGui::BeginTable("OptionsTable", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg)) { 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); ImGui::TableSetupColumn("Option", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn( widget_col_width = overlay::apply_scaling(220);
"CMD Line Parameter", } else {
ImGuiTableColumnFlags_WidthFixed, ImGui::TableSetupColumn("Option", ImGuiTableColumnFlags_WidthStretch);
overlay::apply_scaling(216)); widget_col_width = overlay::apply_scaling(264);
ImGui::TableSetupColumn( }
"Setting", ImGui::TableSetupColumn("Setting", ImGuiTableColumnFlags_WidthFixed, widget_col_width);
ImGuiTableColumnFlags_WidthFixed,
overlay::apply_scaling(240));
// iterate options // iterate options
options_count = 0; options_count = 0;
@@ -4271,10 +4488,24 @@ namespace overlay::windows {
ImGui::PushID(&option); ImGui::PushID(&option);
ImGui::TableNextRow(); 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 // option name
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
if (filter == nullptr) {
// add indent to make options align under category header
ImGui::Indent(INDENT); ImGui::Indent(INDENT);
}
if (option.is_active()) { if (option.is_active()) {
// active option // active option
if (option.disabled || definition.disabled) { if (option.disabled || definition.disabled) {
@@ -4290,21 +4521,22 @@ namespace overlay::windows {
// normal text // normal text
ImGui::TextUnformatted(definition.title.c_str()); ImGui::TextUnformatted(definition.title.c_str());
} }
if (filter == nullptr) {
ImGui::Unindent(INDENT); ImGui::Unindent(INDENT);
}
if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) { if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) {
ImGui::HelpTooltip(definition.desc.c_str()); ImGui::HelpTooltip(definition.desc.c_str());
} }
// command line parameter // command line parameter
ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();
std::string param = "-"; std::string param = "-";
if (definition.display_name.empty()) { if (definition.display_name.empty()) {
param += definition.name; param += definition.name;
} else { } else {
param += definition.display_name; 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)) { if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) {
const auto help = const auto help =
param + param +
@@ -4325,6 +4557,10 @@ namespace overlay::windows {
ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f); 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) { switch (definition.type) {
case OptionType::Bool: { case OptionType::Bool: {
bool state = !option.value.empty(); bool state = !option.value.empty();
@@ -4404,7 +4640,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';
@@ -4541,6 +4777,9 @@ namespace overlay::windows {
} }
} }
// row hover detection (invisible selectable that spans entire row)
ImGui::InvisibleTableRowSelectable();
// next item // next item
ImGui::PopID(); ImGui::PopID();
} }
@@ -5053,6 +5292,7 @@ namespace overlay::windows {
} }
} }
ImGui::InvisibleTableRowSelectable();
ImGui::PopID(); ImGui::PopID();
} }
+6 -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;
@@ -135,6 +137,8 @@ namespace overlay::windows {
std::thread *file_picker_thread = nullptr; std::thread *file_picker_thread = nullptr;
bool file_picker_done = false; 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_buttons(const std::string &name, std::vector<Button> *buttons, int min = 0, int max = -1);
void build_button( void build_button(
const std::string &name, const std::string &name,
@@ -195,12 +199,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;
}; };
@@ -844,6 +844,10 @@ namespace overlay::windows {
ImGui::TextUnformatted(patch_checked ? "ON" : "off"); ImGui::TextUnformatted(patch_checked ? "ON" : "off");
ImGui::EndDisabled(); ImGui::EndDisabled();
} }
// row hover detection (invisible selectable that spans entire row)
ImGui::InvisibleTableRowSelectable();
ImGui::PopID(); 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
+105
View File
@@ -9,6 +9,16 @@
#include "peb.h" #include "peb.h"
#include "util/fileutils.h" #include "util/fileutils.h"
#include "util/dependencies.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::filesystem::path libutils::module_file_name(HMODULE module) {
std::wstring buf; std::wstring buf;
@@ -361,11 +371,106 @@ void libutils::check_duplicate_dlls() {
void libutils::warn_if_dll_exists(const std::string &file_name) { void libutils::warn_if_dll_exists(const std::string &file_name) {
if (fileutils::file_exists(MODULE_PATH / file_name)) { if (fileutils::file_exists(MODULE_PATH / file_name)) {
log_info("libutils", "found user-supplied {} in modules directory", file_name); log_info("libutils", "found user-supplied {} in modules directory", file_name);
libutils::print_dll_info(MODULE_PATH / file_name);
return; return;
} }
const auto &spice_bin_path = libutils::module_file_name(nullptr).parent_path(); const auto &spice_bin_path = libutils::module_file_name(nullptr).parent_path();
if (fileutils::file_exists(spice_bin_path / file_name)) { if (fileutils::file_exists(spice_bin_path / file_name)) {
log_info("libutils", "found user-supplied {} next to spice executable 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; 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 rva2offset(const std::filesystem::path &path, intptr_t rva);
intptr_t offset2rva(IMAGE_NT_HEADERS *nt_headers, intptr_t offset); intptr_t offset2rva(IMAGE_NT_HEADERS *nt_headers, intptr_t offset);
intptr_t offset2rva(const std::filesystem::path &path, 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 += "Game has crashed.\n\n";
text += "Check log.txt and look for error messages near the end of file.\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 += "Unsure what to do next?\n";
text += " * update spice2x to the latest version\n"; text += " * Update spice2x to the latest version\n";
text += " * check the FAQ on spice2x github wiki\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 += " * 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."; text += "Press Enter, Esc, Alt+F4, or click OK to exit. Otherwise, game will close in 30 seconds.";
show_popup(text); show_popup(text);
} }
@@ -76,9 +76,9 @@ void show_popup_for_fatal_error(std::string message) {
text += message; text += message;
text += "----------------------------------------------------------\n\n"; text += "----------------------------------------------------------\n\n";
text += "Unsure what to do next?\n"; text += "Unsure what to do next?\n";
text += " * update spice2x to the latest version\n"; text += " * Update spice2x to the latest version\n";
text += " * check the FAQ on spice2x github wiki\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 += " * 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."; text += "Press Enter, Esc, Alt+F4, or click OK to exit. Otherwise, game will close in 30 seconds.";
show_popup(text); show_popup(text);
} }