Initial re-upload of spice2x-24-08-24
This commit is contained in:
1971
launcher/launcher.cpp
Normal file
1971
launcher/launcher.cpp
Normal file
File diff suppressed because it is too large
Load Diff
27
launcher/launcher.h
Normal file
27
launcher/launcher.h
Normal 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
272
launcher/logger.cpp
Normal 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
34
launcher/logger.h
Normal 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
2290
launcher/options.cpp
Normal file
File diff suppressed because it is too large
Load Diff
237
launcher/options.h
Normal file
237
launcher/options.h
Normal 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
162
launcher/richpresence.cpp
Normal 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
15
launcher/richpresence.h
Normal 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
118
launcher/shutdown.cpp
Normal 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
11
launcher/shutdown.h
Normal 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
222
launcher/signal.cpp
Normal 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
11
launcher/signal.h
Normal 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
118
launcher/superexit.cpp
Normal 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
7
launcher/superexit.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
namespace superexit {
|
||||
bool has_focus();
|
||||
void enable();
|
||||
void disable();
|
||||
}
|
||||
Reference in New Issue
Block a user