|
#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 "&" |
|
char* ptr = result; |
|
|
|
// Replace '&' with "&" |
|
while (*str) { |
|
if (*str == '&') { |
|
strcpy(ptr, "&"); |
|
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; |
|
} |