Skip to content

Instantly share code, notes, and snippets.

@hyblocker
Last active October 4, 2024 10:28
Show Gist options
  • Save hyblocker/ab50e1b7fbe27581bcc29f9c8589f6f4 to your computer and use it in GitHub Desktop.
Save hyblocker/ab50e1b7fbe27581bcc29f9c8589f6f4 to your computer and use it in GitHub Desktop.
SDVX: EXCEED GEAR Bootstrapper

Small wrapper file for starting SDVX EXCEED GEAR's network emulator, configuring my monitors accordingly for the game, and launching the game itself. Also restores state on exit.

Note: Hard-coded for my setup. Change accordingly to your setup.

Compile using TCC with the following args:

tcc -o bootstrap.exe bootstrap.c -luser32 -lShell32 -lSetupAPI -lHid -lCfgmgr32
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <windows.h>
#include <SetupAPI.h>
#include <cfgmgr32.h>
#include <initguid.h>
// Missing defines / functions
#define EDD_GET_DEVICE_INTERFACE_NAME (0x00000001)
#define CSIDL_APPDATA 0x001a // <user name>\Application Data
DEFINE_GUID(GUID_DEVINTERFACE_USB_DEVICE, 0xA5DCBF10L, 0x6530, 0x11D2, 0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED);
BOOL SHGetSpecialFolderPathA(HWND hwnd, LPSTR pszPath, int csidl, BOOL fCreate);
void HidD_GetHidGuid(LPGUID HidGuid);
const char* NETWORK_NAME = "Asphyxia Core";
const char* NETWORK_DIR = "Asphyxia-Core\\";
const char* NETWORK_EXE = "Asphyxia-Core\\asphyxia-core-x64.exe";
const char* GAME_NAME = "Sound Voltex - EXCEED GEAR";
const char* GAME_DIR = "KFC-J-F-A-2024080500\\";
const char* GAME_EXE = "KFC-J-F-A-2024080500\\spice64.exe";
const char* SPICETOOLS_CFG_RELATIVE_TO_APPDATA = "\\spicetools.xml";
// In ms
const DWORD INIT_HALF_WAIT_TIME = 5 * 1000;
const DWORD EXIT_WAIT_TIME = 10 * 1000;
DEVMODE* settings = NULL;
// https://learn.microsoft.com/en-us/windows/win32/debug/retrieving-the-last-error-code
void ErrorExit(const char* lpszFunction) {
// Retrieve the system error message for the last-error code
LPVOID lpMsgBuf;
LPVOID lpDisplayBuf;
DWORD dw = GetLastError();
FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
dw,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPSTR) &lpMsgBuf,
0, NULL );
// Display the error message and exit the process
lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT, (strlen((LPCSTR)lpMsgBuf) + lstrlen((LPCSTR)lpszFunction) + 40) * sizeof(CHAR));
snprintf((LPSTR)lpDisplayBuf, LocalSize(lpDisplayBuf) / sizeof(CHAR), "%s failed with error %d: %s", lpszFunction, dw, lpMsgBuf);
printf("%s\n", lpDisplayBuf);
LocalFree(lpMsgBuf);
LocalFree(lpDisplayBuf);
}
BOOL LaunchExecutable(const char* exeFriendlyName, const char* dirPath, const char* executablePath, PROCESS_INFORMATION* pInfo) {
printf("Launching %s... (path: %s)\n", exeFriendlyName, executablePath);
STARTUPINFO info = {
sizeof(info)
};
ZeroMemory( &info, sizeof(info) );
ZeroMemory( pInfo, sizeof(PROCESS_INFORMATION) );
char fullPath[MAX_PATH];
ZeroMemory( fullPath, sizeof(fullPath) );
DWORD result = GetFullPathName(executablePath, sizeof(fullPath), fullPath, NULL);
if (!CreateProcess(NULL, fullPath, NULL, NULL, FALSE, 0, NULL, dirPath, &info, pInfo)) {
ErrorExit("CreateProcess");
return FALSE;
}
return TRUE;
}
BOOL ConfigureMonitorsForGame() {
int count = 0;
DISPLAY_DEVICE temp = { 0 };
temp.cb = sizeof(DISPLAY_DEVICE);
while (EnumDisplayDevices(NULL, count, &temp, EDD_GET_DEVICE_INTERFACE_NAME)) {
count++;
}
settings = malloc(count * sizeof(DEVMODE));
DISPLAY_DEVICE *devices = malloc(count * sizeof(DISPLAY_DEVICE));
for (int index = 0; index < count; index++) {
memset(&devices[index], 0, sizeof(DISPLAY_DEVICE));
memset(&settings[index], 0, sizeof(DEVMODE));
devices[index].cb = sizeof(DISPLAY_DEVICE);
settings[index].dmSize = sizeof(DEVMODE);
if (!EnumDisplayDevices(NULL, index, &devices[index], EDD_GET_DEVICE_INTERFACE_NAME)) {
break;
}
if (!EnumDisplaySettings(devices[index].DeviceName, ENUM_CURRENT_SETTINGS, &settings[index])) {
break;
}
}
for(int index = 0; index < count; index++) {
// Check if primary monitor
if (settings[index].dmPelsWidth == 2560 &&
settings[index].dmPelsHeight == 1440 &&
settings[index].dmPosition.x == 0 && // 0,0 => Primary display
settings[index].dmPosition.y == 0) {
DEVMODE newSettings = { 0 };
memcpy(&newSettings, &settings[index], sizeof(newSettings));
// Rotate display, lower resolution to 1080p
newSettings.dmPelsHeight = 1920;
newSettings.dmPelsWidth = 1080;
newSettings.dmDisplayOrientation = DMDO_90;
// Lower to 120Hz
newSettings.dmDisplayFrequency = 120;
newSettings.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYORIENTATION | DM_DISPLAYFREQUENCY;
long lRet = ChangeDisplaySettingsEx(devices[index].DeviceName, &newSettings, NULL, 0, NULL);
if (lRet < 0) {
printf("ChangeDisplaySettingsEx failed with (%d)\n", lRet);
}
}
// Check if subscreen monitor
if (settings[index].dmPelsWidth == 1920 &&
settings[index].dmPelsHeight == 1080 &&
(settings[index].dmPosition.x != 0 || // 0,0 => Primary display
settings[index].dmPosition.y != 0)) {
DEVMODE newSettings = { 0 };
memcpy(&newSettings, &settings[index], sizeof(newSettings));
newSettings.dmDisplayOrientation = DMDO_DEFAULT;
newSettings.dmFields = DM_DISPLAYORIENTATION;
long lRet = ChangeDisplaySettingsEx(devices[index].DeviceName, &newSettings, NULL, 0, NULL);
if (lRet < 0) {
printf("ChangeDisplaySettingsEx failed with (%d)\n", lRet);
}
}
}
return TRUE;
}
BOOL RestoreMonitors() {
int count = 0;
DISPLAY_DEVICE temp = { 0 };
temp.cb = sizeof(DISPLAY_DEVICE);
while(EnumDisplayDevices(NULL, count, &temp, EDD_GET_DEVICE_INTERFACE_NAME)) {
count++;
}
DISPLAY_DEVICE *devices = malloc(count * sizeof(DISPLAY_DEVICE));
for (int index = 0; index < count; index++) {
memset(&devices[index], 0, sizeof(DISPLAY_DEVICE));
devices[index].cb = sizeof(DISPLAY_DEVICE);
if (!EnumDisplayDevices(NULL, index, &devices[index], EDD_GET_DEVICE_INTERFACE_NAME)) {
break;
}
}
for(int index = 0; index < count; index++) {
// Check if primary monitor
if (((settings[index].dmPelsWidth == 2560 &&
settings[index].dmPelsHeight == 1440) ||
(settings[index].dmPelsWidth == 1080 &&
settings[index].dmPelsHeight == 1920)) &&
settings[index].dmPosition.x == 0 && // 0,0 => Primary display
settings[index].dmPosition.y == 0) {
long lRet = ChangeDisplaySettingsEx(devices[index].DeviceName, &settings[index], NULL, 0, NULL);
if (lRet < 0) {
printf("ChangeDisplaySettingsEx failed with (%d)\n", lRet);
}
}
// Check if subscreen monitor
if (settings[index].dmPelsWidth == 1920 &&
settings[index].dmPelsHeight == 1080 &&
(settings[index].dmPosition.x != 0 || // 0,0 => Primary display
settings[index].dmPosition.y != 0)) {
long lRet = ChangeDisplaySettingsEx(devices[index].DeviceName, &settings[index], NULL, 0, NULL);
if (lRet < 0) {
printf("ChangeDisplaySettingsEx failed with (%d)\n", lRet);
}
}
}
if (settings != NULL) {
free(settings);
}
return TRUE;
}
char* EscapeXmlString(const char* str) {
int count = 0;
const char* temp = str;
// Count the number of '&' characters
while (*temp) {
if (*temp == '&') {
count++;
}
temp++;
}
// Allocate memory for the new string
char* result = (char*)malloc(strlen(str) + count * 4 + 1); // 4 extra chars for each '&' replaced by "&amp;"
char* ptr = result;
// Replace '&' with "&amp;"
while (*str) {
if (*str == '&') {
strcpy(ptr, "&amp;");
ptr += 5;
} else {
*ptr++ = *str;
}
str++;
}
*ptr = '\0';
return result;
}
void ReplaceDeviceId(char* xml, const char* newDevid, const char* buttonNames[], size_t buttonCount, const char* analogNames[], size_t analogCount) {
char* start = xml;
char* soundVoltexSectionIdentifier = "<game name=\"Sound Voltex\">";
start = strstr(start, soundVoltexSectionIdentifier);
// Replace buttons
for (size_t i = 0; i < buttonCount; ++i) {
char searchStr[256];
snprintf(searchStr, sizeof(searchStr), "<button name=\"%s\"", buttonNames[i]);
char* pos = strstr(start, searchStr);
size_t offset = pos - start;
if (pos != NULL) {
pos = strstr(pos, "devid=\"");
if (pos != NULL) {
pos += 7; // Move past 'devid="'
char* end = strchr(pos, '"');
if (end != NULL && end != pos) { // Ensure devid is not empty
size_t len = strlen(newDevid);
memmove(pos + len, end, strlen(end) + 1);
memcpy(pos, newDevid, len);
}
}
}
}
// Replace analogs
for (size_t i = 0; i < analogCount; ++i) {
char searchStr[256];
snprintf(searchStr, sizeof(searchStr), "<analog name=\"%s\"", analogNames[i]);
char* pos = strstr(start, searchStr);
if (pos != NULL) {
pos = strstr(pos, "devid=\"");
if (pos != NULL) {
pos += 7; // Move past 'devid="'
char* end = strchr(pos, '"');
if (end != NULL && end != pos) { // Ensure devid is not empty
size_t len = strlen(newDevid);
memmove(pos + len, end, strlen(end) + 1);
memcpy(pos, newDevid, len);
}
}
}
}
}
#define ARRAY_COUNT(x) sizeof(x) / sizeof(x[0])
BOOL GetConfigPath(char* configPath) {
if (SUCCEEDED(SHGetSpecialFolderPathA(NULL, configPath, CSIDL_APPDATA, FALSE))) {
size_t appDataLength = strlen(configPath);
// Append \\spicetools.xml
strcpy(configPath + appDataLength, SPICETOOLS_CFG_RELATIVE_TO_APPDATA);
return TRUE;
}
return FALSE;
}
#define GUID_FORMAT "%08lX-%04hX-%04hX-%02hhX%02hhX-%02hhX%02hhX%02hhX%02hhX%02hhX%02hhX"
#define GUID_ARG(guid) (guid).Data1, (guid).Data2, (guid).Data3, (guid).Data4[0], (guid).Data4[1], (guid).Data4[2], (guid).Data4[3], (guid).Data4[4], (guid).Data4[5], (guid).Data4[6], (guid).Data4[7]
// Looks for the controller in the USB device tree. If found returns TRUE, otherwise returns FALSE.
BOOL FindControllerDeviceInstance(char* pControllerDevicePath) {
GUID hidGuid;
HidD_GetHidGuid(&hidGuid);
HDEVINFO hDevInfo = INVALID_HANDLE_VALUE;
SP_DEVINFO_DATA deviceInfoData;
SP_DEVINFO_DATA deviceInfoData_iface;
SP_DEVICE_INTERFACE_DATA deviceInterfaceData = { 0 };
SP_DEVICE_INTERFACE_DETAIL_DATA_A functionClassDeviceData = { 0 };
BOOL done = FALSE;
DWORD result = 0;
DWORD deviceIndex = 0;
DWORD index = 0;
DWORD deviceInterfaceIndex = 0;
CHAR deviceID[1024] = { 0 };
BOOL retCode = FALSE;
hDevInfo = SetupDiGetClassDevsA(&hidGuid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (hDevInfo == INVALID_HANDLE_VALUE) {
goto Done;
}
deviceInfoData.cbSize = sizeof(deviceInfoData);
while (SetupDiEnumDeviceInfo(hDevInfo, deviceIndex, &deviceInfoData)) {
ZeroMemory(deviceID, sizeof(deviceID));
CM_Get_Device_IDA(deviceInfoData.DevInst, deviceID, MAX_PATH, 0);
// If the device is the SDVX controller
if (strstr(deviceID, "VID_1CCF&PID_1014")) {
deviceInterfaceIndex = 0;
deviceInterfaceData.cbSize = sizeof(deviceInterfaceData);
// memcpy(&deviceInfoData_iface, &deviceInfoData, sizeof(deviceInfoData_iface));
while (SetupDiEnumDeviceInterfaces(hDevInfo, &deviceInfoData, &hidGuid, deviceInterfaceIndex, &deviceInterfaceData)) {
deviceInterfaceData.cbSize = sizeof(deviceInterfaceData);
// Get the required length for the device interface detail
ULONG RequiredLength = 0;
SetupDiGetDeviceInterfaceDetailA(hDevInfo, &deviceInterfaceData, NULL, 0, &RequiredLength, NULL);
functionClassDeviceData.cbSize = sizeof(functionClassDeviceData);
// Retrieve the actual interface details
if (SetupDiGetDeviceInterfaceDetailA(hDevInfo, &deviceInterfaceData, &functionClassDeviceData, RequiredLength, &RequiredLength, NULL)) {
// Ensure the device path contains MI_00 (game controller)
if (strstr(functionClassDeviceData.DevicePath, "MI_00") ||
strstr(functionClassDeviceData.DevicePath, "mi_00")) {
strcpy(pControllerDevicePath, functionClassDeviceData.DevicePath);
size_t controllerDevicePathLen = strlen(functionClassDeviceData.DevicePath);
for (int i = 0; i < controllerDevicePathLen && i < 31; i++) {
pControllerDevicePath[i] = toupper(pControllerDevicePath[i]);
}
done = TRUE;
retCode = TRUE;
break;
}
} else {
ErrorExit("SetupDiGetDeviceInterfaceDetailA");
}
deviceInterfaceIndex++;
}
if (done == TRUE) {
break;
}
}
deviceInfoData.cbSize = sizeof(deviceInfoData);
deviceIndex++;
}
Done:
if (INVALID_HANDLE_VALUE != hDevInfo)
{
SetupDiDestroyDeviceInfoList (hDevInfo);
hDevInfo = INVALID_HANDLE_VALUE;
}
return retCode;
}
void UpdateSpiceConfig() {
char controllerDevicePath[1024] = { 0 };
if (FindControllerDeviceInstance(controllerDevicePath) == FALSE) {
printf("Sound Voltex controller is not connected! Aborting...\n");
exit(-1);
}
printf("Found SDVX controller at \"%s\"!\n", controllerDevicePath);
// This function updates the spice2x config such that it points to the current device instance path of the controller.
// We use setup API to get the current path and then use simple find and replace to update the xml file and then re-write the XML file back to disk.
// First we get the path to the spice2x config
// This may be found at %APPDATA%/spicetools.xml
CHAR configPath[MAX_PATH] = { 0 };
if (GetConfigPath(configPath) == FALSE) {
printf("Failed to get path to spicetools config. Aborting...");
exit(-1);
}
printf("Config: %s\n", configPath);
// Read spicetools file into memory
long lSize;
char* buffer = NULL;
FILE* fPtr = fopen(configPath , "rb+");
if (!fPtr) {
printf("Failed to open %s! Aborting...", configPath);
exit(-1);
}
// Seek file size
fseek(fPtr, 0, SEEK_END);
lSize = ftell(fPtr);
// Move read pointer to file head and read to buffer. Add null terminator
fseek(fPtr, 0, SEEK_SET);
buffer = calloc (lSize + 1, sizeof(char));
if (buffer) {
fread(buffer, sizeof(char), lSize, fPtr);
}
char* xmlDevId = EscapeXmlString(controllerDevicePath);
const char* buttonNames[] = { "BT-A", "BT-B", "BT-C", "BT-D", "FX-L", "FX-R", "Start" };
const char* analogNames[] = { "VOL-L", "VOL-R" };
// printf("Original XML:\n%s\n\n", buffer);
ReplaceDeviceId(buffer, xmlDevId, buttonNames, ARRAY_COUNT(buttonNames), analogNames, ARRAY_COUNT(analogNames));
// printf("Modified XML:\n%s\n", buffer);
fseek(fPtr, 0, SEEK_SET);
fprintf(fPtr, buffer);
free(xmlDevId);
free(buffer);
fclose (fPtr);
}
int main(int argc, char* argv[]) {
SetCurrentDirectory("G:\\Sound Voltex\\Arcade");
// Spice2x's config is really stupidly configured - it stores devices by device instance id
UpdateSpiceConfig();
// Launch Asphyxia Core
PROCESS_INFORMATION networkInfo = { 0 };
if (!LaunchExecutable(NETWORK_NAME, NETWORK_DIR, NETWORK_EXE, &networkInfo)) {
printf("Failed to start network! Aborting...\n");
return -1;
}
printf("Launched %s!\n", NETWORK_NAME);
Sleep(INIT_HALF_WAIT_TIME);
ConfigureMonitorsForGame();
// Launch Sound Voltex
PROCESS_INFORMATION gameInfo = { 0 };
if (!LaunchExecutable(GAME_NAME, GAME_DIR, GAME_EXE, &gameInfo)) {
printf("Failed to launch game! Aborting...\n");
} else {
printf("Launched %s!\n", GAME_NAME);
WaitForSingleObject(gameInfo.hProcess, INFINITE);
CloseHandle(gameInfo.hProcess);
CloseHandle(gameInfo.hThread);
// Wait for the game to shut down fully
Sleep(EXIT_WAIT_TIME);
}
RestoreMonitors();
// Close Asphyxia Core
TerminateProcess(networkInfo.hProcess, -1);
WaitForSingleObject(gameInfo.hProcess, INFINITE);
CloseHandle(networkInfo.hProcess);
CloseHandle(networkInfo.hThread);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment