diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f5e8fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Windows files + +desktop.ini + +# Main directory + +/build/ +/out/ + +# Visual Studio + +/.vs/ + +# VS Code + +/.vscode/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..183642b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "dependencies/argparse"] + path = dependencies/argparse + url = https://github.com/p-ranav/argparse.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ad27a35 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.21) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +project(LightgunM3Remap) + +file(GLOB_RECURSE SRC + "src/*.h" + "src/*.cpp" +) + +add_executable(${PROJECT_NAME} ${SRC}) + +target_compile_definitions(${PROJECT_NAME} + PRIVATE + NOMINMAX +) + +add_subdirectory(dependencies) \ No newline at end of file diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..6ebc300 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,57 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "windows", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/out/build/${presetName}", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "cacheVariables": { + "CMAKE_C_COMPILER": "cl.exe", + "CMAKE_CXX_COMPILER": "cl.exe" + } + }, + { + "name": "x86", + "hidden": true, + "architecture": { + "value": "x86", + "strategy": "external" + }, + "toolset": { + "value": "host=x86", + "strategy": "external" + }, + "cacheVariables": { + "OUT_FILE_SUFFIX": "x86" + } + }, + { + "name": "debug", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "release", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + + { "name": "Windows-x86-Debug", "inherits": ["windows", "x86", "debug"] }, + { "name": "Windows-x86-Release", "inherits": ["windows", "x86", "release"] } + ], + "buildPresets": [ + { "name": "Windows-x86-Debug", "configurePreset": "Windows-x86-Debug" }, + { "name": "Windows-x86-Release", "configurePreset": "Windows-x86-Release" } + ] +} + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..44022ca --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Introduction + +This project is a simple console program that aims to automate the updating of lightgun ids in [Sega Model 3 emulator](https://www.supermodel3.com/). +It does this by updating the `Supermodel.ini` file with new configuration. + +You can pass two parameters to the program: `--gun1` is required, while `--gun2` is optional. +The program requires from the user to pass **vendor id** and **product id** of the lightgun, you can read more in the [Usage Example](#usage-example) section. + +The best recommended use case for this utiltiy is to run each time when you start a game via [LaunchBox](https://www.launchbox-app.com/) and [Bulk Add/Remove Additional Applications addon](https://forums.launchbox-app.com/files/file/4375-bulk-addremove-additional-applications/). + +# Limitations + +Currently, the lightgun configuration is hardcoded inside the source code, if you want to personalize it, you need edit the source code and recompile the program. +I know that this isn't ideal, especially when you want to include your own configuration, but for now it is what it is. + +# Installation + +Simply copy the `SindenM3Remap.exe` into root folder of the SegaM3 emulator, example path: `Your/Path/To/SegaM3/SindenM3Remap.exe`. +You can download the precompiled version of this program from [releases page](https://github.com/Patrix9999/SindenM3Remap/releases). + +# Usage example + +### cmd.exe + +``` +SindenM3Remap.exe --gun1 "VID:16C0 PID:0F38" --gun2 "VID:16C0 PID:0F39" +``` + +### powershell + +``` +./SindenM3Remap --gun1 "VID:16C0 PID:0F38" --gun2 "VID:16C0 PID:0F39" +``` + +# Building + +To build this utility, you only need this software: +- [Visual Studio](https://visualstudio.microsoft.com/pl/) (at least **2019**, make sure to install **C++ Workload** and **CMake Tools for Visual Studio**) + +After having IDE installed, simply open up the folder cloned repository folder and build the project. +The executable file can be located in `out/build/CONFIGURATION/` subdirectory. \ No newline at end of file diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt new file mode 100644 index 0000000..a68c33a --- /dev/null +++ b/dependencies/CMakeLists.txt @@ -0,0 +1,4 @@ +target_include_directories(${PROJECT_NAME} + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/argparse/include/" +) \ No newline at end of file diff --git a/dependencies/argparse b/dependencies/argparse new file mode 160000 index 0000000..68fd027 --- /dev/null +++ b/dependencies/argparse @@ -0,0 +1 @@ +Subproject commit 68fd0277eea5412aff9b91c0b70efc698359dae0 diff --git a/src/DeviceMeta.h b/src/DeviceMeta.h new file mode 100644 index 0000000..61c98ec --- /dev/null +++ b/src/DeviceMeta.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +struct DeviceMeta +{ + std::string vid; + std::string pid; +}; \ No newline at end of file diff --git a/src/RawInput.cpp b/src/RawInput.cpp new file mode 100644 index 0000000..0d5a2e5 --- /dev/null +++ b/src/RawInput.cpp @@ -0,0 +1,52 @@ +#include "RawInput.h" +#include + +std::vector GetRawInputDeviceMetas(size_t deviceType) +{ + UINT nDevices; + if (GetRawInputDeviceList(NULL, &nDevices, sizeof(RAWINPUTDEVICELIST)) != 0 || nDevices == 0) + return {}; // RawInput not initialized or no raw input devices were found + + std::vector devices(nDevices); + if (GetRawInputDeviceList(devices.data(), &nDevices, sizeof(RAWINPUTDEVICELIST)) == -1) + return {}; // Failed to enumerate RawInput devices + + std::vector deviceMetas; + static std::regex regex(R"(\\?\HID#VID_([0-9A-Z]+)&PID_([0-9A-Z]+))"); + + for (size_t i = nDevices - 1; i != -1; --i) + { + if (devices[i].dwType != deviceType) + continue; + + char name[256] = {}; + UINT nLength = sizeof(name); + if (GetRawInputDeviceInfoA(devices[i].hDevice, RIDI_DEVICENAME, name, &nLength) == -1) + continue; // Cannot retrieve RawInput device name + + if (strstr(name, "Root#RDP_") != NULL) + continue; // Ignore any RDP devices + + std::cmatch match; + if (!std::regex_search(name, match, regex)) + continue; // regex doesn't match, skip the record + + // All good, add the entry + deviceMetas.push_back({ match[1].str(), match[2].str() }); + } + + return deviceMetas; +} + +size_t GetRawInputDeviceMetaID(const std::vector& deviceMetas, const DeviceMeta& searchedDeviceMeta) +{ + auto it = std::find_if(deviceMetas.begin(), deviceMetas.end(), [&](const DeviceMeta& deviceMeta) + { + return deviceMeta.vid == searchedDeviceMeta.vid && deviceMeta.pid == searchedDeviceMeta.pid; + }); + + if (it == deviceMetas.end()) + return 0; + + return std::distance(deviceMetas.begin(), it) + 1; +} \ No newline at end of file diff --git a/src/RawInput.h b/src/RawInput.h new file mode 100644 index 0000000..dd0146d --- /dev/null +++ b/src/RawInput.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + +#include "DeviceMeta.h" + +std::vector GetRawInputDeviceMetas(size_t deviceType); +size_t GetRawInputDeviceMetaID(const std::vector& deviceMetas, const DeviceMeta& searchedDeviceMeta); \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..94806e0 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,145 @@ +#include +#include +#include +#include +#include "RawInput.h" + +static std::optional GetLightgunDeviceMeta(const std::string& param) +{ + static std::regex regex(R"(VID:([0-9A-Z]+)\s+PID:([0-9A-Z]+))"); + + std::smatch match; + if (!std::regex_search(param, match, regex)) + return std::nullopt; + + return DeviceMeta{ match[1].str(), match[2].str() }; +} + +static void WriteIniString(const std::string& section, const std::string& key, const std::string& value, const std::string& file) +{ + std::string value_escaped = " \"" + value + "\""; + WritePrivateProfileStringA(section.c_str(), key.c_str(), value_escaped.c_str(), file.c_str()); +} + +static void UpdateGunIni(int gun_id, int lighgun_mouse_id, int lightgun_keyboard_id, const std::string& iniFile) +{ + // global + WriteIniString("GLOBAL", gun_id == 1 ? "InputStart1" : "InputStart2", "KEY" + std::to_string(lightgun_keyboard_id) + "_UP", iniFile); + WriteIniString("GLOBAL", gun_id == 1 ? "InputCoin1" : "InputCoin2", "KEY" + std::to_string(lightgun_keyboard_id) + "_LEFT", iniFile); + + // Star Wars Trilogy + if (gun_id == 1) + { + WriteIniString("GLOBAL", "InputAnalogJoyX", "MOUSE" + std::to_string(lighgun_mouse_id) + "_XAXIS", iniFile); + WriteIniString("GLOBAL", "InputAnalogJoyY", "MOUSE" + std::to_string(lighgun_mouse_id) + "_YAXIS", iniFile); + WriteIniString("GLOBAL", "InputAnalogJoyTrigger", "MOUSE" + std::to_string(lighgun_mouse_id) + "_LEFT_BUTTON", iniFile); + WriteIniString("GLOBAL", "InputAnalogJoyEvent", "MOUSE" + std::to_string(lighgun_mouse_id) + "_RIGHT_BUTTON", iniFile); + } + + // Lost World + WriteIniString("GLOBAL", gun_id == 1 ? "InputGunX" : "InputGunX2", "MOUSE" + std::to_string(lighgun_mouse_id) + "_XAXIS", iniFile); + WriteIniString("GLOBAL", gun_id == 1 ? "InputGunY" : "InputGunY2", "MOUSE" + std::to_string(lighgun_mouse_id) + "_YAXIS", iniFile); + WriteIniString("GLOBAL", gun_id == 1 ? "InputTrigger" : "InputTrigger2", "MOUSE" + std::to_string(lighgun_mouse_id) + "_LEFT_BUTTON", iniFile); + WriteIniString("GLOBAL", gun_id == 1 ? "InputOffscreen" : "InputOffscreen2", "MOUSE" + std::to_string(lighgun_mouse_id) + "_RIGHT_BUTTON", iniFile); + + // Ocean Hunter + WriteIniString("GLOBAL", gun_id == 1 ? "InputAnalogGunX" : "InputAnalogGunX2", "MOUSE" + std::to_string(lighgun_mouse_id) + "_XAXIS", iniFile); + WriteIniString("GLOBAL", gun_id == 1 ? "InputAnalogGunY" : "InputAnalogGunY2", "MOUSE" + std::to_string(lighgun_mouse_id) + "_YAXIS", iniFile); + WriteIniString("GLOBAL", gun_id == 1 ? "InputAnalogTriggerLeft" : "InputAnalogTriggerLeft2", "MOUSE" + std::to_string(lighgun_mouse_id) + "_LEFT_BUTTON", iniFile); + WriteIniString("GLOBAL", gun_id == 1 ? "InputAnalogTriggerRight" : "InputAnalogTriggerRight2", "MOUSE" + std::to_string(lighgun_mouse_id) + "_RIGHT_BUTTON", iniFile); +} + +int main(int argc, char* argv[]) +{ + // Usage: + // LightgunM3Remap.exe --gun1 "VID:16C0 PID:0F38" --gun2 "VID:16C0 PID:0F39" + + std::string iniFile = argv[0]; + iniFile = iniFile.substr(0, iniFile.find_last_of("\\")); + iniFile += "\\Config\\Supermodel.ini"; + + argparse::ArgumentParser program("LightgunM3Remap"); + + program.add_argument("--gun1") + .help("Pass first gun VendorId and ProductId, e.g. \"VID:16C0 PID:0F38\"") + .required(); + + program.add_argument("--gun2") + .help("Optionally pass second gun VendorID and ProductId, e.g. \"VID:16C0 PID:0F39\"") + .default_value(std::string("")); // Default empty if not provided + + std::optional gun1_device_meta, gun2_device_meta; + + try + { + program.parse_args(argc, argv); + + std::string gun1_param = program.get("--gun1"); + gun1_device_meta = GetLightgunDeviceMeta(gun1_param); + + std::string gun2_param = program.get("--gun2"); + if (!gun2_param.empty()) + gun2_device_meta = GetLightgunDeviceMeta(gun2_param); + } + catch (const std::exception& e) + { + std::cerr << e.what() << std::endl; + std::cerr << program; + + return 1; + } + + if (!gun1_device_meta.has_value()) + { + std::cerr << "invalid \"gun1\" argument provided" << std::endl; + return 2; + } + + std::vector keyboardDeviceMetas = GetRawInputDeviceMetas(RIM_TYPEKEYBOARD); + std::vector mouseDeviceMetas = GetRawInputDeviceMetas(RIM_TYPEMOUSE); + + size_t lightun_1_mouse_id = GetRawInputDeviceMetaID(mouseDeviceMetas, *gun1_device_meta); + size_t lightun_1_keyboard_id = GetRawInputDeviceMetaID(keyboardDeviceMetas, *gun1_device_meta); + + if (lightun_1_mouse_id == 0 || lightun_1_keyboard_id == 0) + { + std::cerr << "lightgun 1 not found via RawInput API, make sure you've passed the correct PID and VID" << std::endl; + return 3; + } + + std::cout << "[lightgun 1 found]" << std::endl; + std::cout << "- keyboard_id: " << lightun_1_keyboard_id << std::endl; + std::cout << "- mouse_id: " << lightun_1_mouse_id << std::endl; + std::cout << std::endl; + + std::cout << "Updating lightgun 1 Supermodel.ini entries..." << std::endl; + UpdateGunIni(1, lightun_1_mouse_id, lightun_1_keyboard_id, iniFile); + std::cout << "Done!" << std::endl; + + if (gun2_device_meta.has_value()) + { + size_t lightun_2_mouse_id = GetRawInputDeviceMetaID(mouseDeviceMetas, *gun2_device_meta); + size_t lightun_2_keyboard_id = GetRawInputDeviceMetaID(keyboardDeviceMetas, *gun2_device_meta); + + if (lightun_2_mouse_id == 0 || lightun_2_keyboard_id == 0) + { + std::cerr << "lightgun 2 not found via RawInput API, make sure you've passed the correct PID and VID" << std::endl; + return 3; + } + + std::cout << "[lightgun 2 found]" << std::endl; + std::cout << "- keyboard_id: " << lightun_2_keyboard_id << std::endl; + std::cout << "- mouse_id: " << lightun_2_mouse_id << std::endl; + std::cout << std::endl; + + std::cout << "Updating lightgun 2 Supermodel.ini entries..." << std::endl; + UpdateGunIni(2, lightun_2_mouse_id, lightun_2_keyboard_id, iniFile); + std::cout << "Done!" << std::endl; + } + +#ifdef _DEBUG + std::cin.get(); +#endif + + return 0; +} \ No newline at end of file