Initial re-upload of spice2x-24-08-24

This commit is contained in:
2024-08-28 11:10:34 -04:00
commit caa9e02285
1181 changed files with 380065 additions and 0 deletions

1971
launcher/launcher.cpp Normal file

File diff suppressed because it is too large Load Diff

27
launcher/launcher.h Normal file
View File

@@ -0,0 +1,27 @@
#pragma once
#include <filesystem>
#include <memory>
#include <string>
#include <windows.h>
#include "cfg/option.h"
namespace rawinput {
class RawInputManager;
}
namespace api {
class Controller;
}
extern std::filesystem::path MODULE_PATH;
extern HANDLE LOG_FILE;
extern std::string LOG_FILE_PATH;
extern int LAUNCHER_ARGC;
extern char **LAUNCHER_ARGV;
extern std::unique_ptr<std::vector<Option>> LAUNCHER_OPTIONS;
extern std::unique_ptr<api::Controller> API_CONTROLLER;
extern std::unique_ptr<rawinput::RawInputManager> RI_MGR;
extern int main_implementation(int argc, char *argv[]);

272
launcher/logger.cpp Normal file
View File

@@ -0,0 +1,272 @@
#include "logger.h"
#include <algorithm>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <vector>
#include <windows.h>
#include "avs/ea3.h"
#include "launcher/launcher.h"
#include "util/utils.h"
#define FOREGROUND_GREY (8)
#define FOREGROUND_WHITE (FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_GREEN)
#define FOREGROUND_YELLOW (FOREGROUND_RED | FOREGROUND_GREEN)
namespace logger {
// settings
bool BLOCKING = false;
bool COLOR = true;
// state
static bool RUNNING = false;
static WORD DEFAULT_ATTRIBUTES = 0;
static std::mutex EVENT_MUTEX;
static std::condition_variable EVENT_CV;
static std::thread *THREAD = nullptr;
static std::mutex OUTPUT_MUTEX;
static bool OUTPUT_BUFFER_HOT = false;
static std::vector<std::pair<std::string, Style>> OUTPUT_BUFFER1;
static std::vector<std::pair<std::string, Style>> OUTPUT_BUFFER2;
static std::vector<std::pair<std::string, Style>> *OUTPUT_BUFFER = &OUTPUT_BUFFER1;
static std::vector<std::pair<std::string, Style>> *OUTPUT_BUFFER_SWAP = &OUTPUT_BUFFER2;
static std::vector<std::pair<LogHook_t, void*>> HOOKS;
static inline std::vector<std::pair<std::string, Style>> *output_buffer_swap() {
OUTPUT_MUTEX.lock();
auto buffer = OUTPUT_BUFFER;
std::swap(OUTPUT_BUFFER, OUTPUT_BUFFER_SWAP);
OUTPUT_MUTEX.unlock();
return buffer;
}
static void save_default_console_attributes(HANDLE hTerminal) {
CONSOLE_SCREEN_BUFFER_INFO info;
if (GetConsoleScreenBufferInfo(hTerminal, &info)) {
DEFAULT_ATTRIBUTES = info.wAttributes;
}
}
static void set_console_color(HANDLE hTerminal, WORD foreground) {
CONSOLE_SCREEN_BUFFER_INFO info;
if (!GetConsoleScreenBufferInfo(hTerminal, &info)) {
return;
}
info.wAttributes &= ~(info.wAttributes & 0x0F);
if (foreground == FOREGROUND_YELLOW)
info.wAttributes |= foreground;
else
info.wAttributes |= foreground | FOREGROUND_INTENSITY;
SetConsoleTextAttribute(hTerminal, info.wAttributes);
}
static void output_buffer_flush() {
// get buffer and swap
auto buffer = output_buffer_swap();
// return early if no messages to process
if (buffer->empty()) {
return;
}
// get terminal handle
HANDLE hTerminal = GetStdHandle(STD_OUTPUT_HANDLE);
if (logger::COLOR) {
// save default terminal attributes
if (!DEFAULT_ATTRIBUTES) {
save_default_console_attributes(hTerminal);
}
// set initial style
set_console_color(hTerminal, FOREGROUND_WHITE);
}
// write to console and file
DWORD result;
Style last_style = DEFAULT;
for (auto &content : *buffer) {
// set style if color mode enabled
if (logger::COLOR && last_style != content.second) {
last_style = content.second;
switch (content.second) {
case Style::DEFAULT:
set_console_color(hTerminal, FOREGROUND_WHITE);
break;
case Style::GREY:
set_console_color(hTerminal, FOREGROUND_GREY);
break;
case Style::YELLOW:
set_console_color(hTerminal, FOREGROUND_YELLOW);
break;
case Style::RED:
set_console_color(hTerminal, FOREGROUND_RED);
break;
}
}
// write to console
WriteFile(hTerminal, content.first.c_str(), content.first.size(), &result, nullptr);
// write to file
if (LOG_FILE && LOG_FILE != INVALID_HANDLE_VALUE) {
WriteFile(LOG_FILE, content.first.c_str(), content.first.size(), &result, nullptr);
}
}
// clear buffer
buffer->clear();
// reset style
if (logger::COLOR) {
SetConsoleTextAttribute(hTerminal, DEFAULT_ATTRIBUTES);
}
}
void start() {
// don't start if blocking
if (BLOCKING) {
return;
}
// start logging thread
RUNNING = true;
THREAD = new std::thread([] {
std::unique_lock<std::mutex> lock(EVENT_MUTEX);
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
// main loop
while (RUNNING) {
// wait for hot buffer
EVENT_CV.wait(lock, [] { return OUTPUT_BUFFER_HOT; });
OUTPUT_BUFFER_HOT = false;
// flush buffer
output_buffer_flush();
}
// make sure all is written
output_buffer_flush();
// flush writes to disk
if (LOG_FILE && LOG_FILE != INVALID_HANDLE_VALUE) {
FlushFileBuffers(LOG_FILE);
}
// reset terminal
if (logger::COLOR) {
HANDLE hTerminal = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hTerminal, DEFAULT_ATTRIBUTES);
}
});
}
void stop() {
log_info("logger", "stop");
// clean up thread if required
RUNNING = false;
if (THREAD) {
// fake notify to exit wait loop
OUTPUT_BUFFER_HOT = true;
EVENT_CV.notify_all();
// join and clean up
THREAD->join();
delete THREAD;
THREAD = nullptr;
}
}
void push(std::string data, Style color, bool terminate) {
// log hooks
for (auto &hook : HOOKS) {
std::string out;
if (hook.first(hook.second, data, color, out)) {
data = std::move(out);
break;
}
}
// check if empty
if (data.empty()) {
return;
}
// add to output
OUTPUT_MUTEX.lock();
OUTPUT_BUFFER->emplace_back(std::move(data), color);
if (terminate) {
OUTPUT_BUFFER->emplace_back("\r\n", color);
}
OUTPUT_MUTEX.unlock();
// check if blocking or the logging thread is not running
if (BLOCKING || !RUNNING) {
// blocking guard
static std::mutex blocking_lock;
std::lock_guard<std::mutex> blocking_guard(blocking_lock);
// immediately process logs
output_buffer_flush();
} else {
// mark buffer as hot
std::unique_lock<std::mutex> lock(EVENT_MUTEX);
OUTPUT_BUFFER_HOT = true;
EVENT_CV.notify_one();
}
}
void hook_add(LogHook_t hook, void *user) {
HOOKS.emplace_back(hook, user);
}
void hook_remove(LogHook_t hook, void *user) {
HOOKS.erase(std::remove(HOOKS.begin(), HOOKS.end(), std::pair(hook, user)), HOOKS.end());
}
PCBIDFilter::PCBIDFilter() {
hook_add(logger::PCBIDFilter::filter, this);
}
PCBIDFilter::~PCBIDFilter() {
hook_remove(logger::PCBIDFilter::filter, this);
}
bool PCBIDFilter::filter(void *user, const std::string &data, Style style, std::string &out) {
// check if PCBID in data
if (data.find(avs::ea3::EA3_BOOT_PCBID) != std::string::npos) {
// replace pcbid
out = data;
strreplace(out, avs::ea3::EA3_BOOT_PCBID, "[hidden]");
return true;
}
// no replacement
return false;
}
}

34
launcher/logger.h Normal file
View File

@@ -0,0 +1,34 @@
#pragma once
#include <string>
namespace logger {
// settings
extern bool BLOCKING;
extern bool COLOR;
enum Style {
DEFAULT = 0,
GREY = 1,
YELLOW = 2,
RED = 3
};
void start();
void stop();
void push(std::string data, Style color, bool terminate = false);
// log hooks
typedef bool (*LogHook_t)(void *user, const std::string &data, Style style, std::string &out);
void hook_add(LogHook_t hook, void *user);
void hook_remove(LogHook_t hook, void *user);
class PCBIDFilter {
public:
PCBIDFilter();
~PCBIDFilter();
private:
static bool filter(void *user, const std::string &data, Style style, std::string &out);
};
}

2290
launcher/options.cpp Normal file

File diff suppressed because it is too large Load Diff

237
launcher/options.h Normal file
View File

@@ -0,0 +1,237 @@
#pragma once
#include <memory>
#include <vector>
#include "cfg/option.h"
namespace launcher {
// options list - order matters
namespace Options {
enum {
GameExecutable,
OpenConfigurator,
OpenKFControl,
EAmusementEmulation,
ServiceURL,
PCBID,
Player1Card,
Player2Card,
WindowedMode,
InjectHook,
ExecuteScript,
CaptureCursor,
ShowCursor,
DisplayAdapter,
GraphicsForceRefresh,
GraphicsForceSingleAdapter,
Graphics9On12,
spice2x_Dx9On12,
NoLegacy,
RichPresence,
SmartEAmusement,
EAmusementMaintenance,
spice2x_EAmusementMaintenance,
AdapterNetwork,
AdapterSubnet,
DisableNetworkFixes,
HTTP11,
DisableSSL,
URLSlash,
SOFTID,
VREnable,
DisableOverlay,
spice2x_FpsAutoShow,
spice2x_SubScreenAutoShow,
spice2x_IOPanelAutoShow,
spice2x_KeypadAutoShow,
LoadIIDXModule,
IIDXCameraOrderFlip,
IIDXDisableCameras,
IIDXTDJCamera,
IIDXTDJCameraRatio,
IIDXTDJCameraOverride,
IIDXSoundOutputDevice,
IIDXAsioDriver,
IIDXBIO2FW,
IIDXTDJMode,
spice2x_IIDXDigitalTTSensitivity,
spice2x_IIDXLDJForce720p,
spice2x_IIDXTDJSubSize,
spice2x_IIDXLEDFontSize,
spice2x_IIDXLEDColor,
spice2x_IIDXLEDPos,
LoadSoundVoltexModule,
SDVXForce720p,
SDVXPrinterEmulation,
SDVXPrinterOutputPath,
SDVXPrinterOutputClear,
SDVXPrinterOutputOverwrite,
SDVXPrinterOutputFormat,
SDVXPrinterJPGQuality,
SDVXDisableCameras,
SDVXNativeTouch,
spice2x_SDVXDigitalKnobSensitivity,
spice2x_SDVXAsioDriver,
spice2x_SDVXSubPos,
spice2x_SDVXSubRedraw,
LoadDDRModule,
DDR43Mode,
LoadPopnMusicModule,
PopnMusicForceHDMode,
PopnMusicForceSDMode,
LoadHelloPopnMusicModule,
LoadGitaDoraModule,
GitaDoraTwoChannelAudio,
GitaDoraCabinetType,
LoadJubeatModule,
LoadReflecBeatModule,
LoadShogikaiModule,
LoadBeatstreamModule,
LoadNostalgiaModule,
LoadDanceEvolutionModule,
LoadFutureTomTomModule,
LoadBBCModule,
LoadMetalGearArcadeModule,
LoadQuizMagicAcademyModule,
LoadRoadFighters3DModule,
LoadSteelChronicleModule,
LoadMahjongFightClubModule,
LoadScottoModule,
LoadDanceRushModule,
LoadWinningElevenModule,
LoadOtocaModule,
LoadLovePlusModule,
LoadChargeMachineModule,
LoadOngakuParadiseModule,
LoadBusouShinkiModule,
LoadCCJModule,
LoadQKSModule,
LoadMusecaModule,
PathToModules,
ScreenshotFolder,
ConfigurationPath,
ScreenResizeConfigPath,
IntelSDEFolder,
PathToEa3Config,
PathToAppConfig,
PathToAvsConfig,
PathToBootstrap,
PathToLog,
APITCPPort,
APIPassword,
APIVerboseLogging,
APISerialPort,
APISerialBaud,
APIPretty,
APIDebugMode,
EnableAllIOModules,
EnableACIOModule,
EnableICCAModule,
EnableDEVICEModule,
EnableEXTDEVModule,
EnableSCIUNITModule,
EnableDevicePassthrough,
ForceWinTouch,
ForceTouchEmulation,
InvertTouchCoordinates,
DisableTouchCardInsert,
spice2x_TouchCardInsert,
ICCAReaderPort,
ICCAReaderPortToggle,
CardIOHIDReaderSupport,
CardIOHIDReaderOrderFlip,
CardIOHIDReaderOrderToggle,
HIDSmartCard,
HIDSmartCardOrderFlip,
HIDSmartCardOrderToggle,
SextetStreamPort,
EnableBemaniTools5API,
RealtimeProcessPriority,
spice2x_ProcessPriority,
spice2x_ProcessAffinity,
spice2x_ProcessorEfficiencyClass,
HeapSize,
DisableGSyncDetection,
spice2x_NvapiProfile,
DisableAudioHooks,
spice2x_DisableVolumeHook,
AudioBackend,
AsioDriverId,
AudioDummy,
DelayBy5Seconds,
spice2x_DelayByNSeconds,
LoadStubs,
AdjustOrientation,
spice2x_AutoOrientation,
LogLevel,
EAAutomap,
EANetdump,
DiscordAppID,
BlockingLogger,
DebugCreateFile,
VerboseGraphicsLogging,
VerboseAVSLogging,
DisableColoredOutput,
DisableACPHook,
DisableSignalHandling,
DisableDebugHooks,
DisableAvsVfsDriveMountRedirection,
OutputPEB,
QKSArgs,
CCJArgs,
CCJMouseTrackball,
CCJMouseTrackballWithToggle,
CCJTrackballSensitivity,
spice2x_LightsOverallBrightness,
spice2x_WindowBorder,
spice2x_WindowSize,
spice2x_WindowPosition,
spice2x_WindowAlwaysOnTop,
spice2x_IIDXWindowedSubscreenSize,
spice2x_IIDXWindowedSubscreenPosition,
spice2x_JubeatLegacyTouch,
spice2x_RBTouchScale,
spice2x_AsioForceUnload,
spice2x_IIDXNoESpec,
spice2x_IIDXWindowedTDJ,
spice2x_DRSDisableTouch,
spice2x_DRSTransposeTouch,
spice2x_IIDXNativeTouch,
spice2x_IIDXNoSub,
spice2x_IIDXEmulateSubscreenKeypadTouch,
spice2x_AutoCard,
spice2x_LowLatencySharedAudio,
spice2x_TapeLedAlgorithm,
spice2x_NoNVAPI,
spice2x_NoD3D9DeviceHook,
spice2x_SDVXNoSub,
spice2x_EnableSMXStage,
};
enum class OptionsCategory {
Everything,
Basic,
Advanced,
Dev,
API
};
}
const std::vector<std::string> &get_categories(Options::OptionsCategory category);
const std::vector<OptionDefinition> &get_option_definitions();
std::unique_ptr<std::vector<Option>> parse_options(int argc, char *argv[]);
std::vector<Option> merge_options(const std::vector<Option> &options, const std::vector<Option> &overrides);
struct GameVersion {
std::string model;
std::string dest;
std::string spec;
std::string rev;
std::string ext;
};
std::string detect_bootstrap_release_code();
GameVersion detect_gameversion(const std::string& ea3_user);
}

162
launcher/richpresence.cpp Normal file
View File

@@ -0,0 +1,162 @@
#include "richpresence.h"
#include <external/robin_hood.h>
#include "external/discord-rpc/include/discord_rpc.h"
#include "util/logging.h"
#include "misc/eamuse.h"
namespace richpresence {
namespace discord {
// application IDs
static robin_hood::unordered_map<std::string, std::string> APP_IDS = {
{"Sound Voltex", "1225989533317992509"},
{"Beatmania IIDX", "1225993043010912258"},
{"Jubeat", "1226662675497484288"},
{"Dance Evolution", "1226662773010727003"},
{"Beatstream", "1226664029666152600"},
{"Metal Gear", "1226664830178693291"},
{"Reflec Beat", "1226666988450087012"},
{"Pop'n Music", "1226667130033016922"},
{"Steel Chronicle", "1226669022859231293"},
{"Road Fighters 3D", "1226669786017042493"},
{"Museca", "1226669886579802252"},
{"Bishi Bashi Channel", "1226671221467512853"},
{"GitaDora", "1226671586661371945"},
{"Dance Dance Revolution", "1226672373143699456"},
{"Nostalgia", "1226680552963309618"},
{"Quiz Magic Academy", "1226681569989754941"},
{"FutureTomTom", "1226693733484068974"},
{"Mahjong Fight Club", "1226693952829128714"},
{"HELLO! Pop'n Music", "1226695294838898761"},
{"LovePlus", "1226702489659641896"},
{"Tenkaichi Shogikai", "1226703627687559218"},
{"DANCERUSH", "1226709135828193282"},
{"Scotto", "1226716024305619016"},
{"Winning Eleven", "1226721709500137574"},
{"Otoca D'or", "1226737298285133836"},
{"Charge Machine", "1226739364126654516"},
{"Ongaku Paradise", "1226739545559531621"},
{"Busou Shinki: Armored Princess Battle Conductor", "1226739666741366916"},
{"Chase Chase Jokers", "1226739863915593770"},
{"QuizKnock STADIUM", "1226739930328334478"},
};
// state
std::string APPID_OVERRIDE = "";
bool INITIALIZED = false;
void ready(const DiscordUser *request) {
log_warning("richpresence:discord", "ready");
}
void disconnected(int errorCode, const char *message) {
log_warning("richpresence:discord", "disconnected");
}
void errored(int errorCode, const char *message) {
log_warning("richpresence:discord", "error {}: {}", errorCode, message);
}
void joinGame(const char *joinSecret) {
log_warning("richpresence:discord", "joinGame");
}
void spectateGame(const char *spectateSecret) {
log_warning("richpresence:discord", "spectateGame");
}
void joinRequest(const DiscordUser *request) {
log_warning("richpresence:discord", "joinRequest");
}
// handler object
static DiscordEventHandlers handlers {
.ready = discord::ready,
.disconnected = discord::disconnected,
.errored = discord::errored,
.joinGame = discord::joinGame,
.spectateGame = discord::spectateGame,
.joinRequest = discord::joinRequest
};
void update() {
// check state
if (!INITIALIZED)
return;
// update presence
DiscordRichPresence presence {};
presence.startTimestamp = std::time(nullptr);
Discord_UpdatePresence(&presence);
}
void init() {
// check state
if (INITIALIZED) {
return;
}
// get id
std::string id = "";
if (!APPID_OVERRIDE.empty()) {
log_info("richpresence:discord", "using custom APPID: {}", APPID_OVERRIDE);
id = APPID_OVERRIDE;
} else {
auto game_model = eamuse_get_game();
if (game_model.empty()) {
log_warning("richpresence:discord", "could not get game model");
return;
}
id = APP_IDS[game_model];
if (id.empty()) {
log_warning("richpresence:discord", "did not find app ID for {}", game_model);
return;
}
}
// initialize discord
Discord_Initialize(id.c_str(), &discord::handlers, 0, nullptr);
// mark as initialized
INITIALIZED = true;
log_info("richpresence:discord", "initialized");
// update once so the presence is displayed
update();
}
void shutdown() {
Discord_ClearPresence();
Discord_Shutdown();
}
}
// state
static bool INITIALIZED = false;
void init() {
if (INITIALIZED)
return;
log_info("richpresence", "initializing");
INITIALIZED = true;
discord::init();
}
void update(const char *state) {
if (!INITIALIZED)
return;
discord::update();
}
void shutdown() {
if (!INITIALIZED)
return;
log_info("richpresence", "shutdown");
discord::shutdown();
INITIALIZED = false;
}
}

15
launcher/richpresence.h Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include <string>
namespace richpresence {
// settings
namespace discord {
extern std::string APPID_OVERRIDE;
}
void init();
void update(const char *state);
void shutdown();
}

118
launcher/shutdown.cpp Normal file
View File

@@ -0,0 +1,118 @@
// included early to avoid warning
#include <winsock2.h>
#include "shutdown.h"
#include "api/controller.h"
#include "easrv/easrv.h"
#include "rawinput/rawinput.h"
#include "misc/vrutil.h"
#include "hooks/audio/audio.h"
#include "util/logging.h"
#include "launcher.h"
#include "logger.h"
#include "nvapi/nvapi.h"
namespace launcher {
void stop_subsystems() {
// note that it is possible for stop_subsystems to be called multiple times
// (e.g., crashing, and then closing the window)
// therefore, subsystems need to be guarded against multiple unload attempts
log_info("launcher", "stopping subsystems");
// flush/stop logger
logger::stop();
// VR
vrutil::shutdown();
// stop ea server
easrv_shutdown();
// free api sockets
if (API_CONTROLLER) {
API_CONTROLLER->free_socket();
}
// notify audio hook
hooks::audio::stop();
// stop raw input
if (RI_MGR) {
RI_MGR->stop();
}
// unload nvapi and free library (if loaded)
nvapi::unload();
}
void kill(UINT exit_code) {
// terminate
TerminateProcess(GetCurrentProcess(), exit_code);
}
void shutdown(UINT exit_code) {
// force exit after 1s
std::thread force_thread([exit_code] {
Sleep(1000);
log_info("launcher", "force shutdown");
kill(exit_code);
return nullptr;
});
force_thread.detach();
// stop all subsystems
stop_subsystems();
// terminate
kill(exit_code);
}
static void restart_spawn() {
// never do this twice
static bool already_done = false;
if (already_done) {
return;
} else {
already_done = true;
}
// start the process using the same args
if (LAUNCHER_ARGC > 0) {
// build cmd line
std::stringstream cmd_line;
cmd_line << "START \"\" ";
for (int i = 0; i < LAUNCHER_ARGC; i++)
cmd_line << " \"" << LAUNCHER_ARGV[i] << "\"";
// run command
system(cmd_line.str().c_str());
}
}
void restart() {
// force restart after 1s
std::thread force_thread([] {
Sleep(1000);
log_info("launcher", "force restart");
restart_spawn();
launcher::kill(0);
return nullptr;
});
force_thread.detach();
// clean up before restart so resources can be reclaimed
stop_subsystems();
// spawn new and terminate this process
restart_spawn();
launcher::kill(0);
}
}

11
launcher/shutdown.h Normal file
View File

@@ -0,0 +1,11 @@
#pragma once
#include <windows.h>
#include <stdlib.h>
namespace launcher {
void stop_subsystems();
void kill(UINT exit_code = EXIT_FAILURE);
void shutdown(UINT exit_code = EXIT_SUCCESS);
void restart();
}

222
launcher/signal.cpp Normal file
View File

@@ -0,0 +1,222 @@
#include "signal.h"
#include <exception>
#include <string>
#include <windows.h>
#include <dbghelp.h>
#include "external/stackwalker/stackwalker.h"
#include "hooks/libraryhook.h"
#include "launcher/shutdown.h"
#include "util/detour.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "cfg/configurator.h"
#include "logger.h"
// MSVC compatibility
#ifdef exception_code
#undef exception_code
#endif
static decltype(MiniDumpWriteDump) *MiniDumpWriteDump_local = nullptr;
namespace launcher::signal {
// settings
bool DISABLE = false;
}
#define V(variant) case variant: return #variant
static std::string control_code(DWORD dwCtrlType) {
switch (dwCtrlType) {
V(CTRL_C_EVENT);
V(CTRL_BREAK_EVENT);
V(CTRL_CLOSE_EVENT);
V(CTRL_LOGOFF_EVENT);
V(CTRL_SHUTDOWN_EVENT);
default:
return "Unknown(0x" + to_hex(dwCtrlType) + ")";
}
}
static std::string exception_code(struct _EXCEPTION_RECORD *ExceptionRecord) {
switch (ExceptionRecord->ExceptionCode) {
V(EXCEPTION_ACCESS_VIOLATION);
V(EXCEPTION_ARRAY_BOUNDS_EXCEEDED);
V(EXCEPTION_BREAKPOINT);
V(EXCEPTION_DATATYPE_MISALIGNMENT);
V(EXCEPTION_FLT_DENORMAL_OPERAND);
V(EXCEPTION_FLT_DIVIDE_BY_ZERO);
V(EXCEPTION_FLT_INEXACT_RESULT);
V(EXCEPTION_FLT_INVALID_OPERATION);
V(EXCEPTION_FLT_OVERFLOW);
V(EXCEPTION_FLT_STACK_CHECK);
V(EXCEPTION_FLT_UNDERFLOW);
V(EXCEPTION_ILLEGAL_INSTRUCTION);
V(EXCEPTION_IN_PAGE_ERROR);
V(EXCEPTION_INT_DIVIDE_BY_ZERO);
V(EXCEPTION_INT_OVERFLOW);
V(EXCEPTION_INVALID_DISPOSITION);
V(EXCEPTION_NONCONTINUABLE_EXCEPTION);
V(EXCEPTION_PRIV_INSTRUCTION);
V(EXCEPTION_SINGLE_STEP);
V(EXCEPTION_STACK_OVERFLOW);
V(DBG_CONTROL_C);
default:
return "Unknown(0x" + to_hex(ExceptionRecord->ExceptionCode) + ")";
}
}
#undef V
static BOOL WINAPI HandlerRoutine(DWORD dwCtrlType) {
log_info("signal", "console ctrl handler called: {}", control_code(dwCtrlType));
if (dwCtrlType == CTRL_C_EVENT) {
launcher::shutdown();
} else if (dwCtrlType == CTRL_CLOSE_EVENT) {
launcher::shutdown();
}
return FALSE;
}
static LONG WINAPI TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo) {
// ignore signal if disabled or no exception info provided
if (!launcher::signal::DISABLE && ExceptionInfo != nullptr) {
// get exception record
struct _EXCEPTION_RECORD *ExceptionRecord = ExceptionInfo->ExceptionRecord;
// print signal
log_warning("signal", "exception raised: {}", exception_code(ExceptionRecord));
// walk the exception chain
struct _EXCEPTION_RECORD *record_cause = ExceptionRecord->ExceptionRecord;
while (record_cause != nullptr) {
log_warning("signal", "caused by: {}", exception_code(record_cause));
record_cause = record_cause->ExceptionRecord;
}
// print stacktrace
StackWalker sw;
log_info("signal", "printing callstack");
if (!sw.ShowCallstack(GetCurrentThread(), ExceptionInfo->ContextRecord)) {
log_warning("signal", "failed to print callstack");
}
if (MiniDumpWriteDump_local != nullptr) {
HANDLE minidump_file = CreateFileA(
"minidump.dmp",
GENERIC_WRITE,
0,
nullptr,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (minidump_file != INVALID_HANDLE_VALUE) {
MINIDUMP_EXCEPTION_INFORMATION ExceptionParam {};
ExceptionParam.ThreadId = GetCurrentThreadId();
ExceptionParam.ExceptionPointers = ExceptionInfo;
ExceptionParam.ClientPointers = FALSE;
MiniDumpWriteDump_local(
GetCurrentProcess(),
GetCurrentProcessId(),
minidump_file,
MiniDumpNormal,
&ExceptionParam,
nullptr,
nullptr);
CloseHandle(minidump_file);
} else {
log_warning("signal", "failed to create 'minidump.dmp' for minidump: 0x{:08x}",
GetLastError());
}
} else {
log_warning("signal", "minidump creation function not available, skipping");
}
log_fatal("signal", "end");
}
return EXCEPTION_CONTINUE_SEARCH;
}
static BOOL WINAPI SetConsoleCtrlHandler_hook(PHANDLER_ROUTINE pHandlerRoutine, BOOL Add) {
log_misc("signal", "SetConsoleCtrlHandler hook hit");
return TRUE;
}
static LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter_hook(
LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter)
{
log_info("signal", "SetUnhandledExceptionFilter hook hit");
return nullptr;
}
static PVOID WINAPI AddVectoredExceptionHandler_hook(ULONG First, PVECTORED_EXCEPTION_HANDLER Handler) {
log_info("signal", "AddVectoredExceptionHandler hook hit");
return nullptr;
}
void launcher::signal::attach() {
if (launcher::signal::DISABLE) {
return;
}
log_info("signal", "attaching...");
// set a `std::terminate` handler so `std::abort()` is not called by default
std::set_terminate([]() {
log_warning("signal", "std::terminate called");
launcher::kill();
});
// NOTE: inline hooks are not used here as they have caused EXCEPTION_ACCESS_VIOLATION in the past
// when hooking these methods
// hook relevant functions
libraryhook_hook_proc("SetConsoleCtrlHandler", SetConsoleCtrlHandler_hook);
libraryhook_hook_proc("SetUnhandledExceptionFilter", SetUnhandledExceptionFilter_hook);
libraryhook_hook_proc("AddVectoredExceptionHandler", AddVectoredExceptionHandler_hook);
libraryhook_enable();
// hook in all loaded modules
detour::iat_try("SetConsoleCtrlHandler", SetConsoleCtrlHandler_hook);
detour::iat_try("SetUnhandledExceptionFilter", SetUnhandledExceptionFilter_hook);
detour::iat_try("AddVectoredExceptionHandler", AddVectoredExceptionHandler_hook);
log_info("signal", "attached");
}
void launcher::signal::init() {
// load debug help library
if (!cfg::CONFIGURATOR_STANDALONE) {
auto dbghelp = libutils::try_library("dbghelp.dll");
if (dbghelp != nullptr) {
MiniDumpWriteDump_local = libutils::try_proc<decltype(MiniDumpWriteDump) *>(
dbghelp, "MiniDumpWriteDump");
}
}
// register our console ctrl handler
SetConsoleCtrlHandler(HandlerRoutine, TRUE);
// register our exception handler
SetUnhandledExceptionFilter(TopLevelExceptionFilter);
}

11
launcher/signal.h Normal file
View File

@@ -0,0 +1,11 @@
#pragma once
namespace launcher::signal {
// settings
extern bool DISABLE;
//void print_stacktrace();
void attach();
void init();
}

118
launcher/superexit.cpp Normal file
View File

@@ -0,0 +1,118 @@
#include "superexit.h"
#include <thread>
#include "windows.h"
#include "launcher/shutdown.h"
#include "rawinput/rawinput.h"
#include "util/logging.h"
#include "touch/touch.h"
#include "misc/eamuse.h"
#include "games/io.h"
#include "overlay/overlay.h"
namespace superexit {
static std::thread *THREAD = nullptr;
static bool THREAD_RUNNING = false;
bool has_focus() {
HWND fg_wnd = GetForegroundWindow();
if (fg_wnd == NULL) {
return false;
}
if (fg_wnd == SPICETOUCH_TOUCH_HWND) {
return true;
}
DWORD fg_pid;
GetWindowThreadProcessId(fg_wnd, &fg_pid);
return fg_pid == GetCurrentProcessId();
}
void enable() {
// check if already running
if (THREAD)
return;
// create new thread
THREAD_RUNNING = true;
THREAD = new std::thread([] {
// log
log_info("superexit", "enabled");
// set variable to false to stop
while (THREAD_RUNNING) {
// check rawinput for ALT+F4
bool rawinput_exit = false;
if (RI_MGR != nullptr) {
auto devices = RI_MGR->devices_get();
for (auto &device : devices) {
switch (device.type) {
case rawinput::KEYBOARD: {
auto &key_states = device.keyboardInfo->key_states;
for (int page_index = 0; page_index < 1024; page_index += 256) {
if (key_states[page_index + VK_MENU]
&& key_states[page_index + VK_F4]) {
rawinput_exit = true;
}
}
break;
}
default:
break;
}
}
}
bool async_key_exit = GetAsyncKeyState(VK_MENU) && GetAsyncKeyState(VK_F4);
bool overlay_exit = false;
auto buttons = games::get_buttons_overlay(eamuse_get_game());
if (buttons &&
(!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) &&
GameAPI::Buttons::getState(RI_MGR, buttons->at(games::OverlayButtons::SuperExit))) {
overlay_exit = true;
}
// check for exit
if (rawinput_exit || async_key_exit) {
if (has_focus()) {
log_info("superexit", "detected ALT+F4, exiting...");
launcher::shutdown();
}
}
if (overlay_exit) {
if (has_focus()) {
log_info("superexit", "detected Force Exit Game overlay shortcut, exiting...");
launcher::shutdown();
}
}
// slow down
Sleep(100);
}
return nullptr;
});
}
void disable() {
if (!THREAD) {
return;
}
// stop old thread
THREAD_RUNNING = false;
THREAD->join();
// delete thread
delete THREAD;
THREAD = nullptr;
// log
log_info("superexit", "disabled");
}
}

7
launcher/superexit.h Normal file
View File

@@ -0,0 +1,7 @@
#pragma once
namespace superexit {
bool has_focus();
void enable();
void disable();
}