diff --git a/.gitignore b/.gitignore index e1240f0..be97518 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -cmake-build-release/ -cmake-build-debug/ -.idea/ +.vs/ build/ -/.vs +out/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c76c57c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "dep/imgui"] + path = dep/imgui + url = https://github.com/ocornut/imgui.git +[submodule "dep/libmcc"] + path = dep/libmcc + url = https://github.com/SpringContingency/libmcc.git +[submodule "dep/spdlog"] + path = dep/spdlog + url = https://github.com/gabime/spdlog.git +[submodule "dep/json"] + path = dep/json + url = https://github.com/nlohmann/json.git diff --git a/CMakeLists.txt b/CMakeLists.txt index f81a616..645ce2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,27 +22,28 @@ include_directories(${CMAKE_SOURCE_DIR}/lib/json/include) # imgui add_library(imgui SHARED IMPORTED) include_directories(${CMAKE_SOURCE_DIR}/lib/imgui/inc) -set_target_properties(imgui PROPERTIES IMPORTED_IMPLIB "${CMAKE_SOURCE_DIR}/lib/imgui/lib/${CMAKE_BUILD_TYPE}/imgui.lib") +set_target_properties(imgui PROPERTIES IMPORTED_IMPLIB "${CMAKE_SOURCE_DIR}/lib/imgui/lib/release/imgui.lib") # lua add_library(lua SHARED IMPORTED) include_directories(${CMAKE_SOURCE_DIR}/lib/lua/inc) -set_target_properties(lua PROPERTIES IMPORTED_IMPLIB "${CMAKE_SOURCE_DIR}/lib/lua/lib/${CMAKE_BUILD_TYPE}/lua.lib") +set_target_properties(lua PROPERTIES IMPORTED_IMPLIB "${CMAKE_SOURCE_DIR}/lib/lua/lib/release/lua.lib") # spdlog add_library(spdlog SHARED IMPORTED) include_directories(${CMAKE_SOURCE_DIR}/lib/spdlog/inc) -set_target_properties(spdlog PROPERTIES IMPORTED_IMPLIB "${CMAKE_SOURCE_DIR}/lib/spdlog/lib/${CMAKE_BUILD_TYPE}/spdlog.lib") +set_target_properties(spdlog PROPERTIES IMPORTED_IMPLIB "${CMAKE_SOURCE_DIR}/lib/spdlog/lib/release/spdlog.lib") # tinyxml2 add_library(tinyxml2 SHARED IMPORTED) include_directories(${CMAKE_SOURCE_DIR}/lib/tinyxml2/inc) -set_target_properties(tinyxml2 PROPERTIES IMPORTED_IMPLIB "${CMAKE_SOURCE_DIR}/lib/tinyxml2/lib/${CMAKE_BUILD_TYPE}/tinyxml2.lib") +set_target_properties(tinyxml2 PROPERTIES IMPORTED_IMPLIB "${CMAKE_SOURCE_DIR}/lib/tinyxml2/lib/release/tinyxml2.lib") # minhook add_library(minhook SHARED IMPORTED) include_directories(${CMAKE_SOURCE_DIR}/lib/minhook/inc) -set_target_properties(minhook PROPERTIES IMPORTED_IMPLIB "${CMAKE_SOURCE_DIR}/lib/minhook/lib/${CMAKE_BUILD_TYPE}/libMinhook.x64.lib") +# set_target_properties(minhook PROPERTIES IMPORTED_IMPLIB "${CMAKE_SOURCE_DIR}/lib/minhook/lib/${CMAKE_BUILD_TYPE}/libMinHook.x64.lib") +set_target_properties(minhook PROPERTIES IMPORTED_IMPLIB "${CMAKE_SOURCE_DIR}/lib/minhook/lib/release/libMinHook.x64.lib") # utils file(GLOB_RECURSE UTILS_SRC ${CMAKE_SOURCE_DIR}/lib/utils/src/*.cpp) @@ -75,4 +76,4 @@ else () endif() install(TARGETS ${WRAPPER_NAME} DESTINATION "${MCC_DIR}/mcc/binaries/win64") -install(DIRECTORY "${CMAKE_SOURCE_DIR}/res/" DESTINATION "${MCC_DIR}/alpha_ring") +install(DIRECTORY "${CMAKE_SOURCE_DIR}/res/" DESTINATION "${MCC_DIR}/alpha_ring") \ No newline at end of file diff --git a/README.md b/README.md index 2a2117c..bb08d87 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,47 @@ -## Alpha Ring +# AlphaRing - thejackbitt fork + +> +> **Based on:** JackBitt's AlphaRing v1.2.1 (commit `bdad7eb`) +> +> For the original project, see [WinterSquire/AlphaRing](https://github.com/WinterSquire/AlphaRing) + +--- + +## What's New in This Fork + +### Features Added + +#### 1. Controller-to-Player Binding (Splitscreen) +- Each player now has a **"Bind" button** next to the controller dropdown +- Click "Bind" → Press any button on a controller → Automatically assigns that controller to the player +- No more guessing which controller is "Controller 1" vs "Controller 2" + +#### 2. Button-to-Action Binding (Gamepad Mapping) +- Each action in the Gamepad Mapping section has a **"Bind" button** +- Click "Bind" → Press a button → That button is assigned to the action + +#### 3. Fixed Default Gamepad Mappings +- **Bug fixed:** Previously, all actions defaulted to "Left Trigger" due to uninitialized memory +- **Now:** New profiles initialize with standard Xbox Halo controls: + +| Action | Button | +|--------|--------| +| Jump | A | +| Melee | B | +| Action/Interact | X | +| Change Weapon | Y | +| Reload | RB | +| Switch Grenades | LB | +| Shoot | RT | +| Throw Grenade | LT | +| Flashlight | D-pad Up | +| Crouch | Left Stick Click | +| Zoom | Right Stick Click | + +--- + +## Original Alpha Ring + A Modding Tool for MCC [![Build status](https://ci.appveyor.com/api/projects/status/o3qbtc7jirw81xmb?svg=true)](https://ci.appveyor.com/project/WinterSquire/alpharing) @@ -19,15 +62,37 @@ A Modding Tool for MCC ### Installation Make sure you have the latest [Microsoft Visual C++ Redistributable](https://aka.ms/vs/17/release/vc_redist.x64.exe) installed. -Download the latest stable build from the [Releases](https://github.com/WinterSquire/AlphaRing/releases) page. +Download the latest stable build from the [Releases](https://github.com/kirklandsig/AlphaRing/releases) page. Place the DLL into the "Halo The Master Chief Collection\mcc\binaries\win64" directory and launch the game with EAC off. For Running on Steam Deck/Linux, add the following command in the Steam Game Launch Options: -``` +``` WINEDLLOVERRIDES="WTSAPI32=n,b" %command% ``` +#### Batocera Linux / Steam Deck + +Works with **any Proton version** (Proton 9.0, Proton Experimental, Proton GE, etc.) + +1. **Configure the game**: + - Right-click MCC → Properties → Compatibility + - Enable "Force the use of a specific Steam Play compatibility tool" + - Select any Proton version (Proton 9.0, Experimental, or Proton GE all work) + +2. **Set launch options**: Add the following to Steam Launch Options: + ``` + WINEDLLOVERRIDES="WTSAPI32=n,b" %command% + ``` + +3. **Controller Setup (Important for non-Xbox controllers)**: + - For 8BitDo and other third-party controllers, enable **Steam Input** for the controller + - Go to Steam → Settings → Controller → Enable "Xbox Configuration Support" + - This allows Steam to translate your controller inputs to XInput, which MCC and AlphaRing expect + - Without this, some buttons (like A or stick clicks) may not be detected + +> **Note:** Tested on Batocera Linux with the unofficial Batocera add-ons Steam client. + ### Usage Toggle menu: `F4` or `Controller Back` + `Controller Start` @@ -35,10 +100,31 @@ To navigate using Controller use the `Right Stick` to move the mouse and `RB` to When the menu is open, game input is disabled. -### Bugs Report -Submit it in the [Issues](https://github.com/WinterSquire/AlphaRing/issues) page. +--- + +## Building from Source + +### Prerequisites +- Visual Studio 2022 Build Tools +- CMake 3.27+ + +### Build Commands +```bash +# First time setup +mkdir build && cd build +cmake .. -G "Visual Studio 17 2022" -A x64 + +# Build +"C:/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools/MSBuild/Current/Bin/MSBuild.exe" WTSAPI32.vcxproj -p:Configuration=Release -p:Platform=x64 +``` + +Output: `build/Release/WTSAPI32.dll` + +--- -### Credits +## Credits +- **Original AlphaRing:** [WinterSquire](https://github.com/WinterSquire/AlphaRing) +- **Controller Binding and Proton Fixes Fork:** [kirklandsig](https://github.com/kirklandsig/AlphaRing) - [Assembly](https://github.com/XboxChaos/Assembly) for the tag group research. - [Blender](https://github.com/blender/blender) for the bezier curve calculation. - [Priception](https://github.com/Priception) for adding UI controller support and helping with the interface and crash issue. diff --git a/handoff.md b/handoff.md new file mode 100644 index 0000000..bb41cbb --- /dev/null +++ b/handoff.md @@ -0,0 +1,349 @@ +# AlphaRing Project Handoff + +## Project Overview + +**AlphaRing** is a C++ DLL-based modding tool for Halo: The Master Chief Collection (MCC). It's a fork of `wouter51/AlphaRing` maintained by `thejackbitt` with profile option tweaks. + +**Repository**: https://github.com/thejackbitt/AlphaRing + +**Current Release**: v1.2.1 (commit `bdad7eb`) + +### Tech Stack +- C++17 (not C++20 as previously noted) +- CMake build system +- Visual Studio 2022 Build Tools +- DirectX 11 hooking for UI overlay +- ImGui for the in-game interface +- XInput for controller handling +- nlohmann/json for settings serialization + +### Dependencies (in `lib/` directory) +- `lib/imgui` - Dear ImGui UI framework (precompiled) +- `lib/minhook` - Function hooking library +- `lib/spdlog` - Logging +- `lib/json` - nlohmann/json for serialization +- `lib/lua` - Lua scripting +- `lib/tinyxml2` - XML parsing +- `lib/game` - Game offsets and structures +- `lib/utils` - Utility functions + +--- + +## Actual Project Structure + +``` +AlphaRing/ +├── CMakeLists.txt +├── build/ # Build output directory +│ └── Release/ +│ └── WTSAPI32.dll # The compiled mod DLL +├── src/ +│ ├── main.cpp # DLL entry point +│ ├── common.h # Common includes and macros +│ ├── filesystem/ # File system utilities +│ ├── global/ # Global state management +│ ├── hook/ # Low-level function hooking +│ ├── input/ # XInput controller handling +│ │ ├── Input.h +│ │ └── Input.cpp # XInput wrapper functions +│ ├── log/ # Logging utilities +│ ├── mcc/ # MCC game integration +│ │ ├── CDeviceManager.* # Input device management +│ │ ├── CGameEngine.* # Game engine interface +│ │ ├── CGameGlobal.* # Global game state +│ │ ├── CGameManager.* # Player/profile management +│ │ ├── CGamepadMapping.* # Gamepad button-to-action mapping +│ │ ├── CInputDevice.* # Input device abstraction +│ │ ├── CUserProfile.* # User profile settings +│ │ ├── mcc.* # MCC state detection +│ │ ├── module/ # Game-specific modules (Halo 1-4, Reach, ODST) +│ │ ├── network/ # Network functionality +│ │ ├── settings/ # Settings save/load system +│ │ │ ├── Settings.h +│ │ │ └── Settings.cpp # JSON serialization for profiles +│ │ └── splitscreen/ # Splitscreen functionality +│ │ ├── Splitscreen.h +│ │ └── Splitscreen.cpp # Splitscreen UI and logic +│ ├── render/ # Rendering subsystem +│ │ ├── d3d11/ # DirectX 11 hooking +│ │ ├── imgui/ # ImGui integration +│ │ │ ├── game/ +│ │ │ │ ├── halo3/ # Halo 3 specific UI +│ │ │ │ └── mcc/ # MCC main menu UI +│ │ │ │ └── CMCCContext.cpp # Main ImGui menu +│ │ │ └── curve_editor/ # Animation curve editor +│ │ └── window/ # Window management +│ └── wrapper/ # DLL wrapper (WTSAPI32) +└── lib/ # Precompiled libraries +``` + +**Build Output**: DLL named `WTSAPI32.dll` in `build/Release/` + +**Installation**: Copy to `%MCC_DIR%/mcc/binaries/win64/` + +--- + +## Working Features (Confirmed) + +- **Splitscreen support** (1-4 players) - works in all Halo games +- **Wireframe rendering** - works +- **Controller-to-player binding** - IMPLEMENTED (see below) +- **Button-to-action gamepad mapping** - IMPLEMENTED with bind feature +- **Profile save/load** - IMPLEMENTED, saves to `settings.json` + +--- + +## IMPLEMENTED: Input-Based Controller Binding + +### Feature 1: Controller-to-Player Slot Binding (Splitscreen) + +**File**: `src/mcc/splitscreen/Splitscreen.cpp` + +**How it works**: +1. In the Splitscreen window, each player tab shows an "Input" dropdown +2. Next to the dropdown is a "Bind" button +3. Click "Bind" → shows yellow text "Press any button on controller..." +4. Press any button/trigger on a controller +5. System auto-detects which controller (0-3) and assigns it to that player +6. Click "Cancel" to abort binding + +**Implementation details**: + +```cpp +// Detect which controller (0-3) has any button/trigger pressed, returns -1 if none +static int DetectActiveController() { + for (int i = 0; i < 4; i++) { + XINPUT_STATE state; + if (AlphaRing::Input::GetXInputGetState(i, &state)) { + if (state.Gamepad.wButtons != 0 || + state.Gamepad.bLeftTrigger > 30 || + state.Gamepad.bRightTrigger > 30) { + return i; + } + } + } + return -1; +} + +// Binding state: which player slot is waiting for controller input (-1 = none) +static int s_binding_player = -1; +``` + +The UI in `ProfileContext()` checks `s_binding_player` and either shows the binding prompt or the normal dropdown + bind button. + +### Feature 2: Button-to-Action Gamepad Mapping + +**File**: `src/mcc/CGamepadMapping.cpp` + +**How it works**: +1. In the Splitscreen window → expand "Gamepad Mapping" section +2. Select which controller to listen to (Controller 1-4 dropdown at top) +3. For each action (Jump, Melee, Reload, etc.), there's a dropdown + "Bind" button +4. Click "Bind" → shows "Press button..." prompt +5. Press a button on the selected controller +6. That button is assigned to that action + +**Implementation details**: + +```cpp +// Detect which button is currently pressed on a controller +static int DetectPressedButton(int controllerIndex) { + XINPUT_STATE state; + if (!AlphaRing::Input::GetXInputGetState(controllerIndex, &state)) + return -1; + + // Check triggers first + if (state.Gamepad.bLeftTrigger > 30) return CGamepadMapping::LeftTrigger; + if (state.Gamepad.bRightTrigger > 30) return CGamepadMapping::RightTrigger; + + // Check buttons + if (state.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_UP) return CGamepadMapping::DpadUp; + // ... etc for all buttons + + return -1; +} +``` + +--- + +## Profile Save/Load System + +**File**: `src/mcc/settings/Settings.cpp` + +**Config file**: `settings.json` (saved in same directory as MCC executable) + +### What Gets Saved + +**Splitscreen Settings** (auto-saves when changed): +- `b_override` - Whether splitscreen is enabled +- `player_count` - Number of players (1-4) +- `b_use_player0_profile` - Use player 1's profile for all +- `b_player0_use_km` - Enable keyboard/mouse for player 1 +- `b_override_profile` - Override profile settings + +**Player Profiles** (saved via "Save Profile" button): +- `controller_index` - Which controller (0-3) or NONE (4) +- `name` - Player display name +- `profile` - All game settings (FOV, sensitivity, colors, armor, etc.) +- `mapping` - Gamepad button-to-action mappings (66 actions) + +### Save/Load Functions + +```cpp +namespace MCC::Settings { + namespace Splitscreen { + bool Load(); // Load splitscreen config from JSON + bool Save(); // Save splitscreen config to JSON + void ApplyToRuntime(); // Apply loaded config to game + void CaptureFromRuntime(); // Capture current config from game + } + + namespace Profile { + bool Load(); // Load all 4 player profiles from JSON + bool Save(); // Save all 4 player profiles to JSON + void ApplyToRuntime(); // Apply loaded profiles to game + void CaptureFromRuntime(); // Capture current profiles from game + void Initialize(CGameManager* mng); // Initialize profile system + } +} +``` + +### When Saves Happen + +- **Splitscreen settings**: Auto-save when any option is changed in the menu +- **Player profiles**: Manual save via "Save Profile" button in each player's tab + +--- + +## XInput Integration + +**File**: `src/input/Input.cpp` + +```cpp +namespace AlphaRing::Input { + bool Init(); // Loads xinput1_3.dll, xinput1_4.dll, or xinput9_1_0.dll + bool GetXInputGetState(DWORD dwUserIndex, XINPUT_STATE* pState); + void SetState(DWORD dwUserIndex, XINPUT_VIBRATION* pVibration); + bool Update(); // Handles Start+Back toggle for menu, mouse simulation with right stick +} +``` + +The `Update()` function also handles: +- Start + Back combo to toggle the ImGui menu +- Right stick to move mouse cursor when menu is open +- Right shoulder button for mouse click when menu is open + +--- + +## Build Instructions + +### Prerequisites +- Visual Studio 2022 Build Tools (installed at `C:/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools`) +- CMake 3.27+ + +### Build Commands + +**First time setup** (if `build/` doesn't exist): +```bash +mkdir build +cd build +cmake .. -G "Visual Studio 17 2022" -A x64 +``` + +**Build the DLL**: +```bash +cd build +"C:/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools/MSBuild/Current/Bin/MSBuild.exe" WTSAPI32.vcxproj -p:Configuration=Release -p:Platform=x64 +``` + +**Output**: `build/Release/WTSAPI32.dll` + +### Install +Copy `WTSAPI32.dll` to your MCC installation: +``` +/steamapps/common/Halo The Master Chief Collection/mcc/binaries/win64/ +``` + +--- + +## Experiment Log: 6-Player Splitscreen + +**Status**: FAILED - Game engine limitation + +**What was tried**: +- Changed profile arrays from `[4]` to `[6]` +- Updated all loops from `< 4` to `< 6` +- Changed UI limit from `<= 4` to `<= 6` + +**Why it failed**: +The Halo engine's `c_splitscreen_config` structure (in `lib/game/src/halo3/render/views/split_screen_config.h`) has hardcoded arrays: +```cpp +struct c_splitscreen_config { + // ... + } m_view_bounds[4]; // Only 4 view bounds +} m_config_table[4]; // Only 4 config tables +``` + +The game engine itself only has screen layouts for 1-4 players. There's no way to add more without modifying the game executable itself. + +**Reverted**: All changes reverted back to 4-player maximum. + +--- + +## Key Files Reference + +| File | Purpose | +|------|---------| +| `src/mcc/splitscreen/Splitscreen.cpp` | Splitscreen UI, controller-to-player binding | +| `src/mcc/CGamepadMapping.cpp` | Button-to-action mapping with bind feature | +| `src/mcc/CGameManager.cpp` | Player profile container, XUID management | +| `src/mcc/settings/Settings.cpp` | JSON save/load for all settings | +| `src/input/Input.cpp` | XInput wrapper, menu toggle | +| `src/render/imgui/game/mcc/CMCCContext.cpp` | Main ImGui menu structure | +| `CMakeLists.txt` | Build configuration | + +--- + +## Current Git State + +**Branch**: Detached HEAD at `bdad7eb` + +**Modified files** (not committed): +- `src/mcc/splitscreen/Splitscreen.cpp` - Controller bind feature added +- `src/mcc/CGamepadMapping.cpp` - Already had button bind feature + +**To commit these changes**: +```bash +git add src/mcc/splitscreen/Splitscreen.cpp +git commit -m "feat: add controller-to-player bind feature in splitscreen" +``` + +--- + +## Notes for Future Development + +1. **Thread safety**: The codebase uses critical sections for thread-safe access. Follow existing patterns. + +2. **ImGui patterns**: UI code uses `ImGui::BeginDisabled()`/`EndDisabled()` for conditional enabling. + +3. **XInput limitation**: Only 4 controllers (0-3) are supported by XInput. Players 5+ would need to share controllers or use keyboard. + +4. **Settings auto-save**: Splitscreen options auto-save. Profile changes require manual "Save Profile" click. + +5. **Game detection**: Use `MCC::IsInGame()` to check if player is in a game session. + +6. **Build warning**: `LNK4098: defaultlib 'LIBCMT' conflicts` - This is harmless and can be ignored. + +--- + +## Summary + +**What's working**: +- 4-player splitscreen with input-based controller binding +- Gamepad button-to-action mapping with bind feature +- Full profile save/load to JSON +- All original AlphaRing features + +**What's NOT possible**: +- More than 4 players (game engine limitation) +- More than 4 controllers (XInput limitation) diff --git a/src/hook/Hook.cpp b/src/hook/Hook.cpp index 58cc194..bccb89b 100644 --- a/src/hook/Hook.cpp +++ b/src/hook/Hook.cpp @@ -51,8 +51,8 @@ namespace AlphaRing::Hook { return true; } - sprintf(buffer, "Version mismatch [%s]:%s", GAME_VERSION, version.toString().c_str()); - MessageBoxA(nullptr, buffer, "Error", MB_OK); + // Use logging instead of MessageBox for Wine/Proton compatibility + LOG_ERROR("Version mismatch - Expected [{}], Got [{}]", GAME_VERSION, version.toString()); return false; } diff --git a/src/mcc/CGameManager.cpp b/src/mcc/CGameManager.cpp index 1de3c20..d40a90c 100644 --- a/src/mcc/CGameManager.cpp +++ b/src/mcc/CGameManager.cpp @@ -8,11 +8,38 @@ static struct ProfileContainer_t {CGameManager::Profile_t profiles[4]; ProfileContainer_t();} container; -CGameManager::Profile_t* CGameManager::get_profile(int index) {return container.profiles + index;} +CGameManager::Profile_t* CGameManager::get_profile(int index) { + // Bounds check to prevent out-of-bounds access + if (index < 0 || index >= 4) + return nullptr; + return container.profiles + index; +} + +// Initialize default Xbox controller mapping for standard Halo controls +static void InitializeDefaultMapping(CGamepadMapping& mapping) { + // Set all to None (unbound) first + for (int i = 0; i < 66; i++) { + mapping.actions[i] = CGamepadMapping::None; + } + + // Standard Xbox Halo controls - only bind the essential actions + mapping.actions[0] = CGamepadMapping::A; // Jump + mapping.actions[1] = CGamepadMapping::LeftShoulder; // Switch Grenades + mapping.actions[2] = CGamepadMapping::X; // Action/Interact + mapping.actions[3] = CGamepadMapping::RightShoulder; // Reload Right Weapon + mapping.actions[4] = CGamepadMapping::Y; // Change Weapon + mapping.actions[5] = CGamepadMapping::B; // Melee + mapping.actions[6] = CGamepadMapping::DpadUp; // Toggle Flashlight + mapping.actions[7] = CGamepadMapping::LeftTrigger; // Throw Grenade + mapping.actions[8] = CGamepadMapping::RightTrigger; // Use Right Weapon (Shoot) + mapping.actions[9] = CGamepadMapping::LeftThumb; // Crouch + mapping.actions[10] = CGamepadMapping::RightThumb; // Player Zoom + mapping.actions[20] = CGamepadMapping::Back; // Multiplayer Scoreboard +} ProfileContainer_t::ProfileContainer_t() { __int64 guid[2]; - const int controller_map[4] {3, 0, 1, 2}; + const int controller_map[4] {0, 1, 2, 3}; memset(this, 0, sizeof(ProfileContainer_t)); CoCreateGuid((GUID*)guid); @@ -22,6 +49,9 @@ ProfileContainer_t::ProfileContainer_t() { profiles[i].controller_index = controller_map[i]; profiles[i].id = id + i; swprintf(profiles[i].name, L"Player %d", i + 1); + + // Initialize with standard Xbox Halo controls + InitializeDefaultMapping(profiles[i].mapping); } } @@ -45,6 +75,10 @@ bool CGameManager::Initialize(CGameManager* mng) { __int64 CGameManager::get_xuid(int index) { __int64 result; + // Bounds check + if (index < 0 || index >= 4) + return 0; + if (index) return container.profiles[index].id; else @@ -54,12 +88,18 @@ __int64 CGameManager::get_xuid(int index) { CInputDevice *CGameManager::get_controller(int index) { auto mng = DeviceManager(); auto setting = AlphaRing::Global::MCC::Splitscreen(); - auto controller_index = get_profile(index)->controller_index; + auto profile = get_profile(index); + + // Null checks to prevent crashes + if (mng == nullptr || setting == nullptr || profile == nullptr) + return nullptr; + + auto controller_index = profile->controller_index; if ((!index && setting->b_player0_use_km) || controller_index >= 4 || controller_index < 0) return nullptr; - else - return mng->p_input_device[controller_index]; + + return mng->p_input_device[controller_index]; } int CGameManager::get_index(__int64 xuid) { diff --git a/src/mcc/CGamepadMapping.cpp b/src/mcc/CGamepadMapping.cpp index b428aed..23ed08f 100644 --- a/src/mcc/CGamepadMapping.cpp +++ b/src/mcc/CGamepadMapping.cpp @@ -1,13 +1,14 @@ #include "CGamepadMapping.h" #include "CGameEngine.h" -static std::array button_names { +static std::array button_names { "Left Trigger","Right Trigger", "Dpad Up","Dpad Down","Dpad Left","Dpad Right", "Start","Back", "Left Thumb","Right Thumb", "Left Shoulder","Right Shoulder", - "A","B","X","Y" + "A","B","X","Y", + "None" // Unbound }; static std::array action_names { @@ -79,31 +80,212 @@ static std::array action_names { "Select Next Grenades", }; -const std::array* CGamepadMapping::ButtonNames() {return &button_names;} +const std::array* CGamepadMapping::ButtonNames() {return &button_names;} const std::array* CGamepadMapping::ActionNames() {return &action_names;} +void CGamepadMapping::ResetToDefaults() { + // Set all to None (unbound) first + for (int i = 0; i < 66; i++) { + actions[i] = None; + } + + // Standard Xbox Halo controls + actions[0] = A; // Jump + actions[1] = LeftShoulder; // Switch Grenades + actions[2] = X; // Action/Interact + actions[3] = RightShoulder; // Reload Right Weapon + actions[4] = Y; // Change Weapon + actions[5] = B; // Melee + actions[6] = DpadUp; // Toggle Flashlight + actions[7] = LeftTrigger; // Throw Grenade + actions[8] = RightTrigger; // Use Right Weapon (Shoot) + actions[9] = LeftThumb; // Crouch + actions[10] = RightThumb; // Player Zoom + actions[20] = Back; // Multiplayer Scoreboard +} + #include #include +#include +#include +#include "input/Input.h" +#include "mcc/settings/Settings.h" + +// Detect which button is currently pressed on a controller +static int DetectPressedButton(int controllerIndex) { + XINPUT_STATE state; + if (!AlphaRing::Input::GetXInputGetState(controllerIndex, &state)) + return -1; + + // Check triggers first + if (state.Gamepad.bLeftTrigger > 30) return CGamepadMapping::LeftTrigger; + if (state.Gamepad.bRightTrigger > 30) return CGamepadMapping::RightTrigger; + + // Check buttons + if (state.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_UP) return CGamepadMapping::DpadUp; + if (state.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_DOWN) return CGamepadMapping::DpadDown; + if (state.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_LEFT) return CGamepadMapping::DpadLeft; + if (state.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_RIGHT) return CGamepadMapping::DpadRight; + if (state.Gamepad.wButtons & XINPUT_GAMEPAD_START) return CGamepadMapping::Start; + if (state.Gamepad.wButtons & XINPUT_GAMEPAD_BACK) return CGamepadMapping::Back; + if (state.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_THUMB) return CGamepadMapping::LeftThumb; + if (state.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_THUMB) return CGamepadMapping::RightThumb; + if (state.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) return CGamepadMapping::LeftShoulder; + if (state.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) return CGamepadMapping::RightShoulder; + if (state.Gamepad.wButtons & XINPUT_GAMEPAD_A) return CGamepadMapping::A; + if (state.Gamepad.wButtons & XINPUT_GAMEPAD_B) return CGamepadMapping::B; + if (state.Gamepad.wButtons & XINPUT_GAMEPAD_X) return CGamepadMapping::X; + if (state.Gamepad.wButtons & XINPUT_GAMEPAD_Y) return CGamepadMapping::Y; + + return -1; +} void CGamepadMapping::ImGuiContext() { - char buffer[10]; + char buffer[64]; bool result = false; + static int binding_action = -1; // Which action is being bound (-1 = none) + static int binding_controller = 0; // Which controller to listen to + + // Controller selector for binding + ImGui::PushItemWidth(150); + ImGui::Combo("Bind Controller", &binding_controller, "Controller 1\0Controller 2\0Controller 3\0Controller 4\0"); + ImGui::PopItemWidth(); + ImGui::Separator(); + + // Custom Profile Management + static std::vector profile_names; + static int selected_profile = -1; + static char new_profile_name[64] = ""; + static bool show_save_input = false; + static bool needs_refresh = true; + + // Refresh profile list when needed + if (needs_refresh) { + profile_names = MCC::Settings::CustomMapping::GetProfileNames(); + needs_refresh = false; + // Reset selection if out of bounds + if (selected_profile >= (int)profile_names.size()) { + selected_profile = profile_names.empty() ? -1 : 0; + } + } + + ImGui::Text("Custom Profiles:"); + + // Profile dropdown + ImGui::PushItemWidth(200); + const char* preview = (selected_profile >= 0 && selected_profile < (int)profile_names.size()) + ? profile_names[selected_profile].c_str() + : "-- Select Profile --"; + + if (ImGui::BeginCombo("##CustomProfile", preview)) { + for (int i = 0; i < (int)profile_names.size(); ++i) { + bool is_selected = (selected_profile == i); + if (ImGui::Selectable(profile_names[i].c_str(), is_selected)) { + selected_profile = i; + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + + ImGui::SameLine(); + if (ImGui::Button("Load") && selected_profile >= 0 && selected_profile < (int)profile_names.size()) { + if (MCC::Settings::CustomMapping::LoadProfile(profile_names[selected_profile], *this)) { + result = true; + } + } + + ImGui::SameLine(); + if (ImGui::Button("Delete") && selected_profile >= 0 && selected_profile < (int)profile_names.size()) { + MCC::Settings::CustomMapping::DeleteProfile(profile_names[selected_profile]); + needs_refresh = true; + selected_profile = -1; + } + + // Save new profile section + if (!show_save_input) { + if (ImGui::Button("Save as New Profile...")) { + show_save_input = true; + new_profile_name[0] = '\0'; + } + + ImGui::SameLine(); + if (ImGui::Button("Reset to Defaults")) { + ResetToDefaults(); + result = true; + } + } else { + ImGui::PushItemWidth(200); + ImGui::InputText("##NewProfileName", new_profile_name, sizeof(new_profile_name)); + ImGui::PopItemWidth(); + + ImGui::SameLine(); + if (ImGui::Button("Save") && new_profile_name[0] != '\0') { + if (MCC::Settings::CustomMapping::SaveProfile(new_profile_name, *this)) { + needs_refresh = true; + show_save_input = false; + } + } + + ImGui::SameLine(); + if (ImGui::Button("Cancel##SaveCancel")) { + show_save_input = false; + } + } + + ImGui::Separator(); + + // Check for button press if binding is active + if (binding_action >= 0) { + int pressed = DetectPressedButton(binding_controller); + if (pressed >= 0) { + actions[binding_action] = static_cast(pressed); + binding_action = -1; + result = true; + } + } for (int i = 0; i < action_names.size(); ++i) { auto name = action_names.at(i); if (name == nullptr) { - sprintf(buffer, "Button %d", i); + snprintf(buffer, sizeof(buffer), "Action %d", i); name = buffer; } - ImGui::PushItemWidth(200); - int value = actions[i]; - if (ImGui::Combo(name, &value, button_names.data(), button_names.size())) { - actions[i] = static_cast(value); - result = true; + bool is_binding = (binding_action == i); + + if (is_binding) { + // Show binding prompt + ImGui::Text("%s:", name); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Press button..."); + ImGui::SameLine(); + snprintf(buffer, sizeof(buffer), "Cancel##%d", i); + if (ImGui::Button(buffer)) { + binding_action = -1; + } + } else { + // Show current binding with dropdown and bind button + ImGui::PushItemWidth(150); + // Convert None (-1) to index 16 for dropdown display + int value = (actions[i] == CGamepadMapping::None) ? 16 : static_cast(actions[i]); + if (ImGui::Combo(name, &value, button_names.data(), button_names.size())) { + // Convert index 16 back to None (-1) for storage + actions[i] = (value == 16) ? CGamepadMapping::None : static_cast(value); + result = true; + } + ImGui::PopItemWidth(); + + ImGui::SameLine(); + snprintf(buffer, sizeof(buffer), "Bind##%d", i); + if (ImGui::Button(buffer) && binding_action < 0) { + binding_action = i; + } } - ImGui::PopItemWidth(); } if (result) { diff --git a/src/mcc/CGamepadMapping.h b/src/mcc/CGamepadMapping.h index a53b0da..93664af 100644 --- a/src/mcc/CGamepadMapping.h +++ b/src/mcc/CGamepadMapping.h @@ -9,13 +9,15 @@ struct CGamepadMapping { Start, Back, LeftThumb, RightThumb, LeftShoulder, RightShoulder, - A, B, X, Y + A, B, X, Y, + None = -1 // Unbound - no button assigned }; eButton actions[66]; void ImGuiContext(); + void ResetToDefaults(); - static const std::array* ButtonNames(); + static const std::array* ButtonNames(); static const std::array* ActionNames(); }; \ No newline at end of file diff --git a/src/mcc/mcc.cpp b/src/mcc/mcc.cpp index 079ae65..b8dfc91 100644 --- a/src/mcc/mcc.cpp +++ b/src/mcc/mcc.cpp @@ -8,6 +8,7 @@ #include "mcc/module/Module.h" #include "mcc/network/Network.h" #include "mcc/splitscreen/Splitscreen.h" +#include "mcc/settings/Settings.h" namespace MCC { static bool* bIsInGame; @@ -66,6 +67,14 @@ namespace MCC { return false; } + MCC::Settings::Splitscreen::Load(); + bool profileLoad = MCC::Settings::Profile::Load(); + if(profileLoad) { + MCC::Settings::Profile::ApplyToRuntime(); + // MCC::Settings::Profile::Initialize(game_manager); + } + MCC::Settings::Splitscreen::ApplyToRuntime(); + ////Ask user if they want to enable network // if (MessageBox(nullptr, "Would you like to enable network?", "Network", MB_YESNO) == IDYES) // { diff --git a/src/mcc/settings/Settings.cpp b/src/mcc/settings/Settings.cpp new file mode 100644 index 0000000..e4d58e6 --- /dev/null +++ b/src/mcc/settings/Settings.cpp @@ -0,0 +1,827 @@ +#include "mcc/settings/Settings.h" +#include "global/Global.h" +#include "nlohmann/json.hpp" + +#include +#include +#include +#include + +using json = nlohmann::json; + +namespace fs = std::filesystem; + +namespace MCC::Settings { + static SplitscreenConfig g_Config; + static CGameManager::Profile_t g_Profiles[4]; + + static fs::path GetConfigPath() { + char exePath[MAX_PATH] = {0}; + if (!GetModuleFileNameA(nullptr, exePath, MAX_PATH)) + return "settings.json"; + + fs::path dir = fs::path(exePath).parent_path(); + return dir / "settings.json"; + } + + const SplitscreenConfig& Splitscreen::Get() { + return g_Config; + } + + bool Splitscreen::Load() { + fs::path path = GetConfigPath(); + + if (!fs::exists(path)) + return false; + + std::ifstream file(path); + if (!file.is_open()) + return false; + + json j; + file >> j; + + if (!j.contains("splitscreen")) + return false; + + auto& s = j["splitscreen"]; + + g_Config.b_override = s.value("b_override", false); + g_Config.player_count = s.value("player_count", 1); + g_Config.b_use_player0_profile = s.value("b_use_player0_profile", true); + g_Config.b_player0_use_km = s.value("b_player0_use_km", false); + g_Config.b_override_profile = s.value("b_override_profile", false); + + return true; + } + + bool Profile::Load() { + fs::path path = GetConfigPath(); + + if (!fs::exists(path)) + return false; + + std::ifstream file(path); + if (!file.is_open()) + return false; + + json j; + file >> j; + + if (!j.contains("profile_t")) + return false; + + auto& p = j["profile_t"]; + for (int i = 0; i < 4; ++i) { + if (!p.contains(std::to_string(i))) + continue; + + auto& entry = p[std::to_string(i)]; + g_Profiles[i].id = entry.value("id", g_Profiles[i].id); + g_Profiles[i].controller_index = entry.value("controller_index", g_Profiles[i].controller_index); + auto name = entry.value("name", ""); + if (!name.empty()) + mbstowcs(g_Profiles[i].name, name.c_str(), 1024); + if (!entry.contains("profile")) + continue; + + auto& prof = entry["profile"]; + auto& dst = g_Profiles[i].profile; + auto& dst2 = g_Profiles[i].mapping; + + dst.SubtitleSetting = prof.value("SubtitleSetting", dst.SubtitleSetting); + dst.SubtitleSizeSetting = prof.value("SubtitleSizeSetting", dst.SubtitleSizeSetting); + dst.SubtitleBackgroundSetting = prof.value("SubtitleBackgroundSetting", dst.SubtitleBackgroundSetting); + dst.SubtitleShadowColorSetting = prof.value("SubtitleShadowColorSetting", dst.SubtitleShadowColorSetting); + dst.DialogueColorSetting = prof.value("DialogueColorSetting", dst.DialogueColorSetting); + dst.DialoguePaletteSetting = prof.value("DialoguePaletteSetting", dst.DialoguePaletteSetting); + dst.SpeakerSetting = prof.value("SpeakerSetting", dst.SpeakerSetting); + dst.SpeakerColorStyleSetting = prof.value("SpeakerColorStyleSetting", dst.SpeakerColorStyleSetting); + dst.SpeakerColorSetting = prof.value("SpeakerColorSetting", dst.SpeakerColorSetting); + dst.SpeakerPaletteSetting = prof.value("SpeakerPaletteSetting", dst.SpeakerPaletteSetting); + dst.SubtitleFontSetting = prof.value("SubtitleFontSetting", dst.SubtitleFontSetting); + dst.SubtitleBackgroundOpacitySetting = prof.value("SubtitleBackgroundOpacitySetting", dst.SubtitleBackgroundOpacitySetting); + dst.SubtitleShadowOpacitySetting = prof.value("SubtitleShadowOpacitySetting", dst.SubtitleShadowOpacitySetting); + dst.FOVSetting = prof.value("FOVSetting", dst.FOVSetting); + dst.VehicleFOVSetting = prof.value("VehicleFOVSetting", dst.VehicleFOVSetting); + dst.CrosshairLocation = prof.value("CrosshairLocation", dst.CrosshairLocation); + dst.LookControlsInverted = prof.value("LookControlsInverted", dst.LookControlsInverted); + dst.MouseLookControlsInverted = prof.value("MouseLookControlsInverted", dst.MouseLookControlsInverted); + dst.VibrationDisabled = prof.value("VibrationDisabled", dst.VibrationDisabled); + dst.ImpulseTriggersDisabled = prof.value("ImpulseTriggersDisabled", dst.ImpulseTriggersDisabled); + dst.AircraftControlsInverted = prof.value("AircraftControlsInverted", dst.AircraftControlsInverted); + dst.MouseAircraftControlsInverted = prof.value("MouseAircraftControlsInverted", dst.MouseAircraftControlsInverted); + dst.AutoCenterEnabled = prof.value("AutoCenterEnabled", dst.AutoCenterEnabled); + dst.CrouchLockEnabled = prof.value("CrouchLockEnabled", dst.CrouchLockEnabled); + dst.MKCrouchLockEnabled = prof.value("MKCrouchLockEnabled", dst.MKCrouchLockEnabled); + dst.ClenchProtectionEnabled = prof.value("ClenchProtectionEnabled", dst.ClenchProtectionEnabled); + dst.UseFemaleVoice = prof.value("UseFemaleVoice", dst.UseFemaleVoice); + dst.HoldToZoom = prof.value("HoldToZoom", dst.HoldToZoom); + dst.PlayerModelPrimaryColorIndex = prof.value("PlayerModelPrimaryColorIndex", dst.PlayerModelPrimaryColorIndex); + dst.PlayerModelSecondaryColorIndex = prof.value("PlayerModelSecondaryColorIndex", dst.PlayerModelSecondaryColorIndex); + dst.PlayerModelTertiaryColorIndex = prof.value("PlayerModelTertiaryColorIndex", dst.PlayerModelTertiaryColorIndex); + dst.UseEliteModel = prof.value("UseEliteModel", dst.UseEliteModel); + dst.LockMaxAspectRatio = prof.value("LockMaxAspectRatio", dst.LockMaxAspectRatio); + dst.un = prof.value("un", dst.un); + dst.UsersSkinsEnabled = prof.value("UsersSkinsEnabled", dst.UsersSkinsEnabled); + dst.PlayerModelPermutation = prof.value("PlayerModelPermutation", dst.PlayerModelPermutation); + dst.HelmetIndex = prof.value("HelmetIndex", dst.HelmetIndex); + dst.LeftShoulderIndex = prof.value("LeftShoulderIndex", dst.LeftShoulderIndex); + dst.RightShoulderIndex = prof.value("RightShoulderIndex", dst.RightShoulderIndex); + dst.ChestIndex = prof.value("ChestIndex", dst.ChestIndex); + dst.WristIndex = prof.value("WristIndex", dst.WristIndex); + dst.UtilityIndex = prof.value("UtilityIndex", dst.UtilityIndex); + dst.ArmsIndex = prof.value("ArmsIndex", dst.ArmsIndex); + dst.LegsIndex = prof.value("LegsIndex", dst.LegsIndex); + dst.BackpackIndex = prof.value("BackpackIndex", dst.BackpackIndex); + dst.SpartanBodyIndex = prof.value("SpartanBodyIndex", dst.SpartanBodyIndex); + dst.SpartanArmorEffectIndex = prof.value("SpartanArmorEffectIndex", dst.SpartanArmorEffectIndex); + dst.KneesIndex = prof.value("KneesIndex", dst.KneesIndex); + dst.VisorColorIndex = prof.value("VisorColorIndex", dst.VisorColorIndex); + dst.EliteHelmetIndex = prof.value("EliteHelmetIndex", dst.EliteHelmetIndex); + dst.EliteLeftShoulderIndex = prof.value("EliteLeftShoulderIndex", dst.EliteLeftShoulderIndex); + dst.EliteRightShoulderIndex = prof.value("EliteRightShoulderIndex", dst.EliteRightShoulderIndex); + dst.EliteChestIndex = prof.value("EliteChestIndex", dst.EliteChestIndex); + dst.EliteArmsIndex = prof.value("EliteArmsIndex", dst.EliteArmsIndex); + dst.EliteLegsIndex = prof.value("EliteLegsIndex", dst.EliteLegsIndex); + dst.EliteArmorIndex = prof.value("EliteArmorIndex", dst.EliteArmorIndex); + dst.EliteArmorEffectIndex = prof.value("EliteArmorEffectIndex", dst.EliteArmorEffectIndex); + dst.VoiceIndex = prof.value("VoiceIndex", dst.VoiceIndex); + dst.PlayerModelPrimaryColor = prof.value("PlayerModelPrimaryColor", dst.PlayerModelPrimaryColor); + dst.PlayerModelSecondaryColor = prof.value("PlayerModelSecondaryColor", dst.PlayerModelSecondaryColor); + dst.PlayerModelTertiaryColor = prof.value("PlayerModelTertiaryColor", dst.PlayerModelTertiaryColor); + dst.SpartanPose = prof.value("SpartanPose", dst.SpartanPose); + dst.ElitePose = prof.value("ElitePose", dst.ElitePose); + auto& skinArray = prof["Skins"]; + for (size_t o = 0; o < skinArray.size() && o < 32; ++o) { + auto& s = skinArray[o]; + dst.Skins[o].object = s.value("object", dst.Skins[o].object); + dst.Skins[o].skin = s.value("skin", dst.Skins[o].skin); + } + const std::string s = prof.value("ServiceTag", ""); + wmemset(dst.ServiceTag, 0, 4); + if (!s.empty()) { + mbstowcs_s(nullptr, dst.ServiceTag, 4, s.c_str(), _TRUNCATE); + } + dst.OnlineMedalFlasher = prof.value("OnlineMedalFlasher", dst.OnlineMedalFlasher); + dst.VerticalLookSensitivity = prof.value("VerticalLookSensitivity", dst.VerticalLookSensitivity); + dst.HorizontalLookSensitivity = prof.value("HorizontalLookSensitivity", dst.HorizontalLookSensitivity); + dst.LookAcceleration = prof.value("LookAcceleration", dst.LookAcceleration); + dst.LookAxialDeadZone = prof.value("LookAxialDeadZone", dst.LookAxialDeadZone); + dst.LookRadialDeadZone = prof.value("LookRadialDeadZone", dst.LookRadialDeadZone); + dst.ZoomLookSensitivityMultiplier = prof.value("ZoomLookSensitivityMultiplier", dst.ZoomLookSensitivityMultiplier); + dst.VehicleLookSensitivityMultiplier = prof.value("VehicleLookSensitivityMultiplier", dst.VehicleLookSensitivityMultiplier); + dst.ButtonPreset = prof.value("ButtonPreset", dst.ButtonPreset); + dst.StickPreset = prof.value("StickPreset", dst.StickPreset); + dst.LeftyToggle = prof.value("LeftyToggle", dst.LeftyToggle); + dst.FlyingCameraTurnSensitivity = prof.value("FlyingCameraTurnSensitivity", dst.FlyingCameraTurnSensitivity); + dst.FlyingCameraPanning = prof.value("FlyingCameraPanning", dst.FlyingCameraPanning); + dst.FlyingCameraSpeed = prof.value("FlyingCameraSpeed", dst.FlyingCameraSpeed); + dst.FlyingCameraThrust = prof.value("FlyingCameraThrust", dst.FlyingCameraThrust); + dst.TheaterTurnSensitivity = prof.value("TheaterTurnSensitivity", dst.TheaterTurnSensitivity); + dst.TheaterPanning = prof.value("TheaterPanning", dst.TheaterPanning); + dst.TheaterSpeed = prof.value("TheaterSpeed", dst.TheaterSpeed); + dst.TheaterThrust = prof.value("TheaterThrust", dst.TheaterThrust); + dst.MKTheaterTurnSensitivity = prof.value("MKTheaterTurnSensitivity", dst.MKTheaterTurnSensitivity); + dst.MKTheaterPanning = prof.value("MKTheaterPanning", dst.MKTheaterPanning); + dst.MKTheaterSpeed = prof.value("MKTheaterSpeed", dst.MKTheaterSpeed); + dst.MKTheaterThrust = prof.value("MKTheaterThrust", dst.MKTheaterThrust); + dst.SwapTriggersAndBumpers = prof.value("SwapTriggersAndBumpers", dst.SwapTriggersAndBumpers); + dst.UseModernAimControl = prof.value("UseModernAimControl", dst.UseModernAimControl); + dst.UseDoublePressJumpToJetpack = prof.value("UseDoublePressJumpToJetpack", dst.UseDoublePressJumpToJetpack); + dst.DualWieldInverted = prof.value("DualWieldInverted", dst.DualWieldInverted); + dst.ControllerDualWieldInverted = prof.value("ControllerDualWieldInverted", dst.ControllerDualWieldInverted); + dst.ControllerHornetControlJoystick = prof.value("ControllerHornetControlJoystick", dst.ControllerHornetControlJoystick); + dst.ControllerBansheeTrickButtonsSwapped = prof.value("ControllerBansheeTrickButtonsSwapped", dst.ControllerBansheeTrickButtonsSwapped); + dst.ColorCorrection = prof.value("ColorCorrection", dst.ColorCorrection); + dst.EnemyPlayerNameColor = prof.value("EnemyPlayerNameColor", dst.EnemyPlayerNameColor); + dst.GameEngineTimer = prof.value("GameEngineTimer", dst.GameEngineTimer); + auto& loadoutArray = prof["LoadoutSlots"]; + for (size_t o = 0; o < loadoutArray.size() && o < 5; ++o) { + auto& slot = loadoutArray[o]; + dst.LoadoutSlots[o].TacticalPackageIndex = slot.value("TacticalPackageIndex", dst.LoadoutSlots[o].TacticalPackageIndex); + dst.LoadoutSlots[o].SupportUpgradeIndex = slot.value("SupportUpgradeIndex", dst.LoadoutSlots[o].SupportUpgradeIndex); + dst.LoadoutSlots[o].PrimaryWeaponIndex = slot.value("PrimaryWeaponIndex", dst.LoadoutSlots[o].PrimaryWeaponIndex); + dst.LoadoutSlots[o].SecondaryWeaponIndex = slot.value("SecondaryWeaponIndex", dst.LoadoutSlots[o].SecondaryWeaponIndex); + dst.LoadoutSlots[o].PrimaryWeaponVariantIndex = slot.value("PrimaryWeaponVariantIndex", dst.LoadoutSlots[o].PrimaryWeaponVariantIndex); + dst.LoadoutSlots[o].SecondaryWeaponVariantIndex = slot.value("SecondaryWeaponVariantIndex", dst.LoadoutSlots[o].SecondaryWeaponVariantIndex); + dst.LoadoutSlots[o].EquipmentIndex = slot.value("EquipmentIndex", dst.LoadoutSlots[o].EquipmentIndex); + dst.LoadoutSlots[o].GrenadeIndex = slot.value("GrenadeIndex", dst.LoadoutSlots[o].GrenadeIndex); + std::string name = slot.value("Name", ""); + wmemset(dst.LoadoutSlots[o].Name, 0, 14); + mbstowcs(dst.LoadoutSlots[o].Name, name.c_str(), 14); + } + std::string gs = prof.value("GameSpecific", std::string{}); + std::memset(dst.GameSpecific, 0, sizeof(dst.GameSpecific)); + std::strncpy(dst.GameSpecific, gs.c_str(), sizeof(dst.GameSpecific) - 1); + dst.MouseSensitivity = prof.value("MouseSensitivity", dst.MouseSensitivity); + dst.MouseSmoothing = prof.value("MouseSmoothing", dst.MouseSmoothing); + dst.MouseAcceleration = prof.value("MouseAcceleration", dst.MouseAcceleration); + dst.PixelPerfectHudScale = prof.value("PixelPerfectHudScale", dst.PixelPerfectHudScale); + dst.MouseAccelerationMinRate = prof.value("MouseAccelerationMinRate", dst.MouseAccelerationMinRate); + dst.MouseAccelerationMaxAccel = prof.value("MouseAccelerationMaxAccel", dst.MouseAccelerationMaxAccel); + dst.MouseAccelerationScale = prof.value("MouseAccelerationScale", dst.MouseAccelerationScale); + dst.MouseAccelerationExp = prof.value("MouseAccelerationExp", dst.MouseAccelerationExp); + dst.KeyboardMouseButtonPreset = prof.value("KeyboardMouseButtonPreset", dst.KeyboardMouseButtonPreset); + auto& mappings = prof["CustomKeyboardMouseMappingV2"]; + for (size_t o = 0; o < mappings.size() && o < 66; ++o) { + auto& srcMap = mappings[o]; + auto& dstMap = dst.CustomKeyboardMouseMappingV2[o]; + dstMap.AbstractButton = srcMap.value("AbstractButton", dstMap.AbstractButton); + auto& keys = srcMap["VirtualKeyCodes"]; + for (size_t k = 0; k < keys.size() && k < 5; ++k) { + dstMap.VirtualKeyCodes[k] = keys[k].get(); + } + } + dst.MasterVolume = prof.value("MasterVolume", dst.MasterVolume); + dst.MusicVolume = prof.value("MusicVolume", dst.MusicVolume); + dst.SfxVolume = prof.value("SfxVolume", dst.SfxVolume); + std::string buf = prof.value("buffer4", std::string{}); + std::memset(dst.buffer4, 0, sizeof(dst.buffer4)); + std::strncpy(dst.buffer4, buf.c_str(), sizeof(dst.buffer4) - 1); + dst.Brightness = prof.value("Brightness", dst.Brightness); + auto& offsets = prof["WeaponDisplayOffset"]; + for (size_t o = 0; o < offsets.size() && o < 5; ++o) { + auto& off = offsets[o]; + dst.WeaponDisplayOffset[o].x = off.value("x", dst.WeaponDisplayOffset[o].x); + dst.WeaponDisplayOffset[o].y = off.value("y", dst.WeaponDisplayOffset[o].y); + dst.WeaponDisplayOffset[o].z = off.value("z", dst.WeaponDisplayOffset[o].z); + } + dst.ColorBlindMode = prof.value("ColorBlindMode", dst.ColorBlindMode); + dst.ColorBlindStrength = prof.value("ColorBlindStrength", dst.ColorBlindStrength); + dst.ColorBlindBrightness = prof.value("ColorBlindBrightness", dst.ColorBlindBrightness); + dst.ColorBlindContrast = prof.value("ColorBlindContrast", dst.ColorBlindContrast); + dst.RemasteredHUDSetting = prof.value("RemasteredHUDSetting", dst.RemasteredHUDSetting); + dst.HUDScale = prof.value("HUDScale", dst.HUDScale); + auto& actionsJson = entry["mapping"]["actions"]; + for (size_t a = 0; a < actionsJson.size() && a < 66; ++a) { + dst2.actions[a] = static_cast(actionsJson[a].get()); + } + } + + return true; + + } + + bool Splitscreen::Save() { + fs::path path = GetConfigPath(); + + json j; + + j["splitscreen"] = { + {"b_override", g_Config.b_override}, + {"player_count", g_Config.player_count}, + {"b_use_player0_profile", g_Config.b_use_player0_profile}, + {"b_player0_use_km", g_Config.b_player0_use_km}, + {"b_override_profile", g_Config.b_override_profile} + }; + + std::ofstream file(path, std::ios::trunc); + if (!file.is_open()) + return false; + + file << j.dump(4); + return true; + } + + bool Profile::Save() { + fs::path path = GetConfigPath(); + + json j; + if (fs::exists(path)) { + std::ifstream file(path); + if (file.is_open()) { + file >> j; + } + } + + for (int i = 0; i < 4; ++i) { + std::string name; + size_t len = wcslen(g_Profiles[i].name); + name.resize(len); + wcstombs(name.data(), g_Profiles[i].name, len); + + json prof; + json mapp; + auto& src = g_Profiles[i].profile; + auto& src2 = g_Profiles[i].mapping; + + prof["SubtitleSetting"] = src.SubtitleSetting; + prof["SubtitleSizeSetting"] = src.SubtitleSizeSetting; + prof["SubtitleBackgroundSetting"] = src.SubtitleBackgroundSetting; + prof["SubtitleShadowColorSetting"] = src.SubtitleShadowColorSetting; + prof["DialogueColorStyleSetting"] = src.DialogueColorStyleSetting; + prof["DialogueColorSetting"] = src.DialogueColorSetting; + prof["DialoguePaletteSetting"] = src.DialoguePaletteSetting; + prof["SpeakerSetting"] = src.SpeakerSetting; + prof["SpeakerColorStyleSetting"] = src.SpeakerColorStyleSetting; + prof["SpeakerColorSetting"] = src.SpeakerColorSetting; + prof["SpeakerPaletteSetting"] = src.SpeakerPaletteSetting; + prof["SubtitleFontSetting"] = src.SubtitleFontSetting; + prof["SubtitleBackgroundOpacitySetting"] = src.SubtitleBackgroundOpacitySetting; + prof["SubtitleShadowOpacitySetting"] = src.SubtitleShadowOpacitySetting; + prof["FOVSetting"] = src.FOVSetting; + prof["VehicleFOVSetting"] = src.VehicleFOVSetting; + prof["CrosshairLocation"] = src.CrosshairLocation; + prof["LookControlsInverted"] = src.LookControlsInverted; + prof["MouseLookControlsInverted"] = src.MouseLookControlsInverted; + prof["VibrationDisabled"] = src.VibrationDisabled; + prof["ImpulseTriggersDisabled"] = src.ImpulseTriggersDisabled; + prof["AircraftControlsInverted"] = src.AircraftControlsInverted; + prof["MouseAircraftControlsInverted"] = src.MouseAircraftControlsInverted; + prof["AutoCenterEnabled"] = src.AutoCenterEnabled; + prof["CrouchLockEnabled"] = src.CrouchLockEnabled; + prof["MKCrouchLockEnabled"] = src.MKCrouchLockEnabled; + prof["ClenchProtectionEnabled"] = src.ClenchProtectionEnabled; + prof["UseFemaleVoice"] = src.UseFemaleVoice; + prof["HoldToZoom"] = src.HoldToZoom; + prof["PlayerModelPrimaryColorIndex"] = src.PlayerModelPrimaryColorIndex; + prof["PlayerModelSecondaryColorIndex"] = src.PlayerModelSecondaryColorIndex; + prof["PlayerModelTertiaryColorIndex"] = src.PlayerModelTertiaryColorIndex; + prof["UseEliteModel"] = src.UseEliteModel; + prof["LockMaxAspectRatio"] = src.LockMaxAspectRatio; + prof["un"] = src.un; + prof["UsersSkinsEnabled"] = src.UsersSkinsEnabled; + prof["PlayerModelPermutation"] = src.PlayerModelPermutation; + prof["HelmetIndex"] = src.HelmetIndex; + prof["LeftShoulderIndex"] = src.LeftShoulderIndex; + prof["RightShoulderIndex"] = src.RightShoulderIndex; + prof["ChestIndex"] = src.ChestIndex; + prof["WristIndex"] = src.WristIndex; + prof["UtilityIndex"] = src.UtilityIndex; + prof["ArmsIndex"] = src.ArmsIndex; + prof["LegsIndex"] = src.LegsIndex; + prof["BackpackIndex"] = src.BackpackIndex; + prof["SpartanBodyIndex"] = src.SpartanBodyIndex; + prof["SpartanArmorEffectIndex"] = src.SpartanArmorEffectIndex; + prof["KneesIndex"] = src.KneesIndex; + prof["VisorColorIndex"] = src.VisorColorIndex; + prof["EliteHelmetIndex"] = src.EliteHelmetIndex; + prof["EliteLeftShoulderIndex"] = src.EliteLeftShoulderIndex; + prof["EliteRightShoulderIndex"] = src.EliteRightShoulderIndex; + prof["EliteChestIndex"] = src.EliteChestIndex; + prof["EliteArmsIndex"] = src.EliteArmsIndex; + prof["EliteLegsIndex"] = src.EliteLegsIndex; + prof["EliteArmorIndex"] = src.EliteArmorIndex; + prof["EliteArmorEffectIndex"] = src.EliteArmorEffectIndex; + prof["VoiceIndex"] = src.VoiceIndex; + prof["PlayerModelPrimaryColor"] = src.PlayerModelPrimaryColor; + prof["PlayerModelSecondaryColor"] = src.PlayerModelSecondaryColor; + prof["PlayerModelTertiaryColor"] = src.PlayerModelTertiaryColor; + prof["SpartanPose"] = src.SpartanPose; + prof["ElitePose"] = src.ElitePose; + json skinArray = json::array(); + for (int o = 0; o < 32; ++o) { + json skin; + skin["object"] = src.Skins[o].object; + skin["skin"] = src.Skins[o].skin; + skinArray.push_back(skin); + } + prof["Skins"] = skinArray; + char buf[5] = {}; + wcstombs(buf, src.ServiceTag, 4); + prof["ServiceTag"] = std::string(buf, strnlen(buf, 4)); + prof["OnlineMedalFlasher"] = src.OnlineMedalFlasher; + prof["VerticalLookSensitivity"] = src.VerticalLookSensitivity; + prof["HorizontalLookSensitivity"] = src.HorizontalLookSensitivity; + prof["LookAcceleration"] = src.LookAcceleration; + prof["LookAxialDeadZone"] = src.LookAxialDeadZone; + prof["LookRadialDeadZone"] = src.LookRadialDeadZone; + prof["ZoomLookSensitivityMultiplier"] = src.ZoomLookSensitivityMultiplier; + prof["VehicleLookSensitivityMultiplier"] = src.VehicleLookSensitivityMultiplier; + prof["ButtonPreset"] = src.ButtonPreset; + prof["StickPreset"] = src.StickPreset; + prof["LeftyToggle"] = src.LeftyToggle; + prof["FlyingCameraTurnSensitivity"] = src.FlyingCameraTurnSensitivity; + prof["FlyingCameraPanning"] = src.FlyingCameraPanning; + prof["FlyingCameraSpeed"] = src.FlyingCameraSpeed; + prof["FlyingCameraThrust"] = src.FlyingCameraThrust; + prof["TheaterTurnSensitivity"] = src.TheaterTurnSensitivity; + prof["TheaterPanning"] = src.TheaterPanning; + prof["TheaterSpeed"] = src.TheaterSpeed; + prof["TheaterThrust"] = src.TheaterThrust; + prof["MKTheaterTurnSensitivity"] = src.MKTheaterTurnSensitivity; + prof["MKTheaterPanning"] = src.MKTheaterPanning; + prof["MKTheaterSpeed"] = src.MKTheaterSpeed; + prof["MKTheaterThrust"] = src.MKTheaterThrust; + prof["SwapTriggersAndBumpers"] = src.SwapTriggersAndBumpers; + prof["UseModernAimControl"] = src.UseModernAimControl; + prof["UseDoublePressJumpToJetpack"] = src.UseDoublePressJumpToJetpack; + prof["DualWieldInverted"] = src.DualWieldInverted; + prof["ControllerDualWieldInverted"] = src.ControllerDualWieldInverted; + prof["ControllerHornetControlJoystick"] = src.ControllerHornetControlJoystick; + prof["ControllerBansheeTrickButtonsSwapped"] = src.ControllerBansheeTrickButtonsSwapped; + prof["ColorCorrection"] = src.ColorCorrection; + prof["EnemyPlayerNameColor"] = src.EnemyPlayerNameColor; + prof["GameEngineTimer"] = src.GameEngineTimer; + json loadoutArray = json::array(); + for (int o = 0; o < 5; ++o) { + json slot; + slot["TacticalPackageIndex"] = src.LoadoutSlots[o].TacticalPackageIndex; + slot["SupportUpgradeIndex"] = src.LoadoutSlots[o].SupportUpgradeIndex; + slot["PrimaryWeaponIndex"] = src.LoadoutSlots[o].PrimaryWeaponIndex; + slot["SecondaryWeaponIndex"] = src.LoadoutSlots[o].SecondaryWeaponIndex; + slot["PrimaryWeaponVariantIndex"] = src.LoadoutSlots[o].PrimaryWeaponVariantIndex; + slot["SecondaryWeaponVariantIndex"] = src.LoadoutSlots[o].SecondaryWeaponVariantIndex; + slot["EquipmentIndex"] = src.LoadoutSlots[o].EquipmentIndex; + slot["GrenadeIndex"] = src.LoadoutSlots[o].GrenadeIndex; + + char buf[15] = {}; + wcstombs(buf, src.LoadoutSlots[o].Name, 14); + slot["Name"] = std::string(buf); + + loadoutArray.push_back(slot); + } + prof["LoadoutSlots"] = loadoutArray; + prof["GameSpecific"] = std::string(src.GameSpecific, strnlen(src.GameSpecific, sizeof(src.GameSpecific))); + prof["MouseSensitivity"] = src.MouseSensitivity; + prof["MouseSmoothing"] = src.MouseSmoothing; + prof["MouseAcceleration"] = src.MouseAcceleration; + prof["PixelPerfectHudScale"] = src.PixelPerfectHudScale; + prof["MouseAccelerationMinRate"] = src.MouseAccelerationMinRate; + prof["MouseAccelerationMaxAccel"] = src.MouseAccelerationMaxAccel; + prof["MouseAccelerationScale"] = src.MouseAccelerationScale; + prof["MouseAccelerationExp"] = src.MouseAccelerationExp; + prof["KeyboardMouseButtonPreset"] = src.KeyboardMouseButtonPreset; + json customKeyboardMouseMappingArray = json::array(); + for (int o = 0; o < 66; ++o) { + json cKMMObj; + cKMMObj["AbstractButton"] = src.CustomKeyboardMouseMappingV2[o].AbstractButton; + json virtualKeyCodesArray = json::array(); + for (int k = 0; k < 5; ++k) { + virtualKeyCodesArray.push_back(src.CustomKeyboardMouseMappingV2[o].VirtualKeyCodes[k]); + } + cKMMObj["VirtualKeyCodes"] = virtualKeyCodesArray; + customKeyboardMouseMappingArray.push_back(cKMMObj); + } + prof["CustomKeyboardMouseMappingV2"] = customKeyboardMouseMappingArray; + prof["MasterVolume"] = src.MasterVolume; + prof["MusicVolume"] = src.MusicVolume; + prof["SfxVolume"] = src.SfxVolume; + prof["buffer4"] = std::string(src.buffer4, strnlen(src.buffer4, sizeof(src.buffer4))); + prof["Brightness"] = src.Brightness; + json weaponDisplayOffsetArray = json::array(); + for (int o = 0; o < 5; ++o) { + json offset; + offset["x"] = src.WeaponDisplayOffset[o].x; + offset["y"] = src.WeaponDisplayOffset[o].y; + offset["z"] = src.WeaponDisplayOffset[o].z; + weaponDisplayOffsetArray.push_back(offset); + } + prof["WeaponDisplayOffset"] = weaponDisplayOffsetArray; + prof["ColorBlindMode"] = src.ColorBlindMode; + prof["ColorBlindStrength"] = src.ColorBlindStrength; + prof["ColorBlindBrightness"] = src.ColorBlindBrightness; + prof["ColorBlindContrast"] = src.ColorBlindContrast; + prof["RemasteredHUDSetting"] = src.RemasteredHUDSetting; + prof["HUDScale"] = src.HUDScale; + json mappingJson = json::array(); + for (int a = 0; a < 66; ++a) { + mappingJson.push_back(static_cast(src2.actions[a])); + } + mapp["actions"] = mappingJson; + + j["profile_t"][std::to_string(i)] = { + {"id", g_Profiles[i].id}, + {"controller_index", g_Profiles[i].controller_index}, + {"name", name}, + {"profile", prof}, + {"mapping", mapp} + }; + } + + std::ofstream file(path, std::ios::trunc); + if (!file.is_open()) + return false; + + file << j.dump(4); + return true; + } + + void Splitscreen::ApplyToRuntime() { + auto p = AlphaRing::Global::MCC::Splitscreen(); + if (!p) + return; + + p->b_override = g_Config.b_override; + p->player_count = g_Config.player_count; + p->b_use_player0_profile = g_Config.b_use_player0_profile; + p->b_player0_use_km = g_Config.b_player0_use_km; + p->b_override_profile = g_Config.b_override_profile; + } + + void Profile::ApplyToRuntime() { + for (int i = 0; i < 4; ++i) { + auto dst = CGameManager::get_profile(i); + auto& src = g_Profiles[i]; + + dst->controller_index = src.controller_index; + dst->id = src.id; + wcscpy_s(dst->name, _countof(dst->name), src.name); + dst->profile = src.profile; + dst->mapping = src.mapping; + } + } + + void Splitscreen::CaptureFromRuntime() { + auto p = AlphaRing::Global::MCC::Splitscreen(); + if (!p) + return; + + g_Config.b_override = p->b_override; + g_Config.player_count = p->player_count; + g_Config.b_use_player0_profile = p->b_use_player0_profile; + g_Config.b_player0_use_km = p->b_player0_use_km; + g_Config.b_override_profile = p->b_override_profile; + } + + void Profile::CaptureFromRuntime() { + for (int i = 0; i < 4; ++i) { + auto src = CGameManager::get_profile(i); + auto& dst = g_Profiles[i]; + + dst.controller_index = src->controller_index; + dst.id = src->id; + wcscpy_s(dst.name, src->name); + dst.profile.SubtitleSizeSetting = src->profile.SubtitleSizeSetting; + dst.profile.SubtitleBackgroundSetting = src->profile.SubtitleBackgroundSetting; + dst.profile.SubtitleShadowColorSetting = src->profile.SubtitleShadowColorSetting; + dst.profile.DialogueColorStyleSetting = src->profile.DialogueColorStyleSetting; + dst.profile.DialogueColorSetting = src->profile.DialogueColorSetting; + dst.profile.DialoguePaletteSetting = src->profile.DialoguePaletteSetting; + dst.profile.SpeakerSetting = src->profile.SpeakerSetting; + dst.profile.SpeakerColorStyleSetting = src->profile.SpeakerColorStyleSetting; + dst.profile.SpeakerColorSetting = src->profile.SpeakerColorSetting; + dst.profile.SpeakerPaletteSetting = src->profile.SpeakerPaletteSetting; + dst.profile.SubtitleFontSetting = src->profile.SubtitleFontSetting; + dst.profile.SubtitleBackgroundOpacitySetting = src->profile.SubtitleBackgroundOpacitySetting; + dst.profile.SubtitleShadowOpacitySetting = src->profile.SubtitleShadowOpacitySetting; + dst.profile.FOVSetting = src->profile.FOVSetting; + dst.profile.VehicleFOVSetting = src->profile.VehicleFOVSetting; + dst.profile.CrosshairLocation = src->profile.CrosshairLocation; + dst.profile.LookControlsInverted = src->profile.LookControlsInverted; + dst.profile.MouseLookControlsInverted = src->profile.MouseLookControlsInverted; + dst.profile.VibrationDisabled = src->profile.VibrationDisabled; + dst.profile.ImpulseTriggersDisabled = src->profile.ImpulseTriggersDisabled; + dst.profile.AircraftControlsInverted = src->profile.AircraftControlsInverted; + dst.profile.AutoCenterEnabled = src->profile.AutoCenterEnabled; + dst.profile.CrouchLockEnabled = src->profile.CrouchLockEnabled; + dst.profile.MKCrouchLockEnabled = src->profile.MKCrouchLockEnabled; + dst.profile.ClenchProtectionEnabled = src->profile.ClenchProtectionEnabled; + dst.profile.UseFemaleVoice = src->profile.UseFemaleVoice; + dst.profile.HoldToZoom = src->profile.HoldToZoom; + dst.profile.PlayerModelPrimaryColorIndex = src->profile.PlayerModelPrimaryColorIndex; + dst.profile.PlayerModelSecondaryColorIndex = src->profile.PlayerModelSecondaryColorIndex; + dst.profile.PlayerModelTertiaryColorIndex = src->profile.PlayerModelTertiaryColorIndex; + dst.profile.UseEliteModel = src->profile.UseEliteModel; + dst.profile.LockMaxAspectRatio = src->profile.LockMaxAspectRatio; + dst.profile.un = src->profile.un; + dst.profile.UsersSkinsEnabled = src->profile.UsersSkinsEnabled; + dst.profile.PlayerModelPermutation = src->profile.PlayerModelPermutation; + dst.profile.HelmetIndex = src->profile.HelmetIndex; + dst.profile.LeftShoulderIndex = src->profile.LeftShoulderIndex; + dst.profile.RightShoulderIndex = src->profile.RightShoulderIndex; + dst.profile.ChestIndex = src->profile.ChestIndex; + dst.profile.WristIndex = src->profile.WristIndex; + dst.profile.UtilityIndex = src->profile.UtilityIndex; + dst.profile.ArmsIndex = src->profile.ArmsIndex; + dst.profile.LegsIndex = src->profile.LegsIndex; + dst.profile.BackpackIndex = src->profile.BackpackIndex; + dst.profile.SpartanBodyIndex = src->profile.SpartanBodyIndex; + dst.profile.SpartanArmorEffectIndex = src->profile.SpartanArmorEffectIndex; + dst.profile.KneesIndex = src->profile.KneesIndex; + dst.profile.VisorColorIndex = src->profile.VisorColorIndex; + dst.profile.EliteHelmetIndex = src->profile.EliteHelmetIndex; + dst.profile.EliteLeftShoulderIndex = src->profile.EliteLeftShoulderIndex; + dst.profile.EliteRightShoulderIndex = src->profile.EliteRightShoulderIndex; + dst.profile.EliteChestIndex = src->profile.EliteChestIndex; + dst.profile.EliteArmsIndex = src->profile.EliteArmsIndex; + dst.profile.EliteLegsIndex = src->profile.EliteLegsIndex; + dst.profile.EliteArmorIndex = src->profile.EliteArmorIndex; + dst.profile.EliteArmorEffectIndex = src->profile.EliteArmorEffectIndex; + dst.profile.VoiceIndex = src->profile.VoiceIndex; + dst.profile.PlayerModelPrimaryColor = src->profile.PlayerModelPrimaryColor; + dst.profile.PlayerModelSecondaryColor = src->profile.PlayerModelSecondaryColor; + dst.profile.PlayerModelTertiaryColor = src->profile.PlayerModelTertiaryColor; + dst.profile.SpartanPose = src->profile.SpartanPose; + dst.profile.ElitePose = src->profile.ElitePose; + // dst.profile.Skins = src->profile.Skins; + // dst.profile.ServiceTag = src->profile.ServiceTag; + dst.profile.OnlineMedalFlasher = src->profile.OnlineMedalFlasher; + dst.profile.VerticalLookSensitivity = src->profile.VerticalLookSensitivity; + dst.profile.HorizontalLookSensitivity = src->profile.HorizontalLookSensitivity; + dst.profile.LookAcceleration = src->profile.LookAcceleration; + dst.profile.LookAxialDeadZone = src->profile.LookAxialDeadZone; + dst.profile.LookRadialDeadZone = src->profile.LookRadialDeadZone; + dst.profile.ZoomLookSensitivityMultiplier = src->profile.ZoomLookSensitivityMultiplier; + dst.profile.VehicleLookSensitivityMultiplier = src->profile.VehicleLookSensitivityMultiplier; + dst.profile.ButtonPreset = src->profile.ButtonPreset; + dst.profile.StickPreset = src->profile.StickPreset; + dst.profile.LeftyToggle = src->profile.LeftyToggle; + dst.profile.FlyingCameraTurnSensitivity = src->profile.FlyingCameraTurnSensitivity; + dst.profile.FlyingCameraPanning = src->profile.FlyingCameraPanning; + dst.profile.FlyingCameraSpeed = src->profile.FlyingCameraSpeed; + dst.profile.FlyingCameraThrust = src->profile.FlyingCameraThrust; + dst.profile.TheaterTurnSensitivity = src->profile.TheaterTurnSensitivity; + dst.profile.TheaterPanning = src->profile.TheaterPanning; + dst.profile.TheaterSpeed = src->profile.TheaterSpeed; + dst.profile.TheaterThrust = src->profile.TheaterThrust; + dst.profile.MKTheaterTurnSensitivity = src->profile.MKTheaterTurnSensitivity; + dst.profile.MKTheaterPanning = src->profile.MKTheaterPanning; + dst.profile.MKTheaterSpeed = src->profile.MKTheaterSpeed; + dst.profile.MKTheaterThrust = src->profile.MKTheaterThrust; + dst.profile.SwapTriggersAndBumpers = src->profile.SwapTriggersAndBumpers; + dst.profile.UseModernAimControl = src->profile.UseModernAimControl; + dst.profile.UseDoublePressJumpToJetpack = src->profile.UseDoublePressJumpToJetpack; + dst.profile.DualWieldInverted = src->profile.DualWieldInverted; + dst.profile.ControllerDualWieldInverted = src->profile.ControllerDualWieldInverted; + dst.profile.ControllerHornetControlJoystick = src->profile.ControllerHornetControlJoystick; + dst.profile.ControllerBansheeTrickButtonsSwapped = src->profile.ControllerBansheeTrickButtonsSwapped; + dst.profile.ColorCorrection = src->profile.ColorCorrection; + dst.profile.EnemyPlayerNameColor = src->profile.EnemyPlayerNameColor; + dst.profile.GameEngineTimer = src->profile.GameEngineTimer; + // dst.profile.LoadoutSlots = src->profile.LoadoutSlots; + // dst.profile.GameSpecific = src->profile.GameSpecific; + dst.profile.MouseSensitivity = src->profile.MouseSensitivity; + dst.profile.MouseSmoothing = src->profile.MouseSmoothing; + dst.profile.MouseAcceleration = src->profile.MouseAcceleration; + dst.profile.PixelPerfectHudScale = src->profile.PixelPerfectHudScale; + dst.profile.MouseAccelerationMinRate = src->profile.MouseAccelerationMinRate; + dst.profile.MouseAccelerationMaxAccel = src->profile.MouseAccelerationMaxAccel; + dst.profile.MouseAccelerationScale = src->profile.MouseAccelerationScale; + dst.profile.MouseAccelerationExp = src->profile.MouseAccelerationExp; + dst.profile.KeyboardMouseButtonPreset = src->profile.KeyboardMouseButtonPreset; + // dst.profile.CustomKeyboardMouseMappingV2 = src->profile.CustomKeyboardMouseMappingV2; + dst.profile.MasterVolume = src->profile.MasterVolume; + dst.profile.MusicVolume = src->profile.MusicVolume; + dst.profile.SfxVolume = src->profile.SfxVolume; + // dst.profile.buffer4 = src->profile.buffer4; + dst.profile.Brightness = src->profile.Brightness; + // dst.profile.WeaponDisplayOffset = src->profile.WeaponDisplayOffset; + dst.profile.ColorBlindMode = src->profile.ColorBlindMode; + dst.profile.ColorBlindStrength = src->profile.ColorBlindStrength; + dst.profile.ColorBlindBrightness = src->profile.ColorBlindBrightness; + dst.profile.ColorBlindContrast = src->profile.ColorBlindContrast; + dst.profile.RemasteredHUDSetting = src->profile.RemasteredHUDSetting; + dst.profile.HUDScale = src->profile.HUDScale; + memcpy(&dst.mapping, &src->mapping, sizeof(dst.mapping)); + } + } + + void Profile::Initialize(CGameManager* mng) + { + pGameManager = mng; + + for (int i = 0; i < 4; ++i) + { + //sets up id, controller_index, name, profile and mapping + auto& dst = g_Profiles[i]; + auto runtime = CGameManager::get_profile(i); + + dst.controller_index = runtime->controller_index; + dst.id = runtime->id; + wcscpy_s(dst.name, _countof(dst.name), runtime-> name); + __int64 xuid = CGameManager::get_xuid(i); + } + } + + // Custom Mapping Profiles + static fs::path GetCustomMappingsPath() { + char exePath[MAX_PATH] = {0}; + if (!GetModuleFileNameA(nullptr, exePath, MAX_PATH)) + return "custom_mappings.json"; + + fs::path dir = fs::path(exePath).parent_path(); + return dir / "custom_mappings.json"; + } + + std::vector CustomMapping::GetProfileNames() { + std::vector names; + fs::path path = GetCustomMappingsPath(); + + if (!fs::exists(path)) + return names; + + std::ifstream file(path); + if (!file.is_open()) + return names; + + try { + json j; + file >> j; + + if (j.contains("profiles") && j["profiles"].is_object()) { + for (auto& [key, value] : j["profiles"].items()) { + names.push_back(key); + } + } + } catch (...) { + // Invalid JSON, return empty + } + + return names; + } + + bool CustomMapping::SaveProfile(const std::string& name, const CGamepadMapping& mapping) { + fs::path path = GetCustomMappingsPath(); + + json j; + // Load existing if present + if (fs::exists(path)) { + std::ifstream file(path); + if (file.is_open()) { + try { + file >> j; + } catch (...) { + j = json::object(); + } + } + } + + // Ensure profiles object exists + if (!j.contains("profiles") || !j["profiles"].is_object()) { + j["profiles"] = json::object(); + } + + // Save the mapping actions array + json actionsArray = json::array(); + for (int i = 0; i < 66; ++i) { + actionsArray.push_back(static_cast(mapping.actions[i])); + } + + j["profiles"][name] = { + {"actions", actionsArray} + }; + + std::ofstream file(path, std::ios::trunc); + if (!file.is_open()) + return false; + + file << j.dump(4); + return true; + } + + bool CustomMapping::LoadProfile(const std::string& name, CGamepadMapping& mapping) { + fs::path path = GetCustomMappingsPath(); + + if (!fs::exists(path)) + return false; + + std::ifstream file(path); + if (!file.is_open()) + return false; + + try { + json j; + file >> j; + + if (!j.contains("profiles") || !j["profiles"].contains(name)) + return false; + + auto& profile = j["profiles"][name]; + if (!profile.contains("actions")) + return false; + + auto& actionsArray = profile["actions"]; + for (size_t i = 0; i < actionsArray.size() && i < 66; ++i) { + mapping.actions[i] = static_cast(actionsArray[i].get()); + } + + return true; + } catch (...) { + return false; + } + } + + bool CustomMapping::DeleteProfile(const std::string& name) { + fs::path path = GetCustomMappingsPath(); + + if (!fs::exists(path)) + return false; + + std::ifstream infile(path); + if (!infile.is_open()) + return false; + + json j; + try { + infile >> j; + } catch (...) { + return false; + } + infile.close(); + + if (!j.contains("profiles") || !j["profiles"].contains(name)) + return false; + + j["profiles"].erase(name); + + std::ofstream outfile(path, std::ios::trunc); + if (!outfile.is_open()) + return false; + + outfile << j.dump(4); + return true; + } + +} \ No newline at end of file diff --git a/src/mcc/settings/Settings.h b/src/mcc/settings/Settings.h new file mode 100644 index 0000000..9d73721 --- /dev/null +++ b/src/mcc/settings/Settings.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include "../CGameManager.h" + +namespace MCC::Settings { + + struct SplitscreenConfig { + bool b_override = false; + int player_count = 1; + bool b_use_player0_profile = true; + bool b_player0_use_km = false; + bool b_override_profile = false; + }; + + namespace Splitscreen { + bool Load(); + bool Save(); + + void ApplyToRuntime(); + void CaptureFromRuntime(); + + const SplitscreenConfig& Get(); + } + + namespace Profile { + bool Load(); + bool Save(); + + void ApplyToRuntime(); + void CaptureFromRuntime(); + void Initialize(CGameManager* mng); + } + + namespace CustomMapping { + // Get list of saved custom mapping profile names + std::vector GetProfileNames(); + + // Save current mapping as a named profile + bool SaveProfile(const std::string& name, const CGamepadMapping& mapping); + + // Load a named profile into the provided mapping + bool LoadProfile(const std::string& name, CGamepadMapping& mapping); + + // Delete a named profile + bool DeleteProfile(const std::string& name); + } +} diff --git a/src/mcc/splitscreen/Splitscreen.cpp b/src/mcc/splitscreen/Splitscreen.cpp index 54a0c6a..d5a3f63 100644 --- a/src/mcc/splitscreen/Splitscreen.cpp +++ b/src/mcc/splitscreen/Splitscreen.cpp @@ -1,5 +1,7 @@ #include "Splitscreen.h" +#include "mcc/settings/Settings.h" + #include "common.h" #include "global/Global.h" @@ -36,12 +38,31 @@ namespace MCC::Splitscreen { #include "imgui.h" #include "mcc/mcc.h" +#include "input/Input.h" #include namespace MCC::Splitscreen { void RealContext(); + // Detect which controller (0-3) has any button/trigger pressed, returns -1 if none + static int DetectActiveController() { + for (int i = 0; i < 4; i++) { + XINPUT_STATE state; + if (AlphaRing::Input::GetXInputGetState(i, &state)) { + if (state.Gamepad.wButtons != 0 || + state.Gamepad.bLeftTrigger > 30 || + state.Gamepad.bRightTrigger > 30) { + return i; + } + } + } + return -1; + } + + // Binding state: which player slot is waiting for controller input (-1 = none) + static int s_binding_player = -1; + void ImGuiContext() { static bool show_splitscreen; @@ -70,10 +91,38 @@ namespace MCC::Splitscreen { ImGui::PopItemWidth(); ImGui::BeginDisabled(!index && p_setting->b_player0_use_km); - ImGui::PushItemWidth(200);ImGui::Combo("Input", &p_profile->controller_index, items, IM_ARRAYSIZE(items));ImGui::PopItemWidth(); + + // Check for controller binding completion + if (s_binding_player == index) { + int detected = DetectActiveController(); + if (detected >= 0) { + p_profile->controller_index = detected; + s_binding_player = -1; + } + } + + if (s_binding_player == index) { + // Show binding prompt + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Press any button on controller..."); + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + s_binding_player = -1; + } + } else { + // Show dropdown and bind button + ImGui::PushItemWidth(200); + ImGui::Combo("Input", &p_profile->controller_index, items, IM_ARRAYSIZE(items)); + ImGui::PopItemWidth(); + ImGui::SameLine(); + sprintf(buffer, "Bind##ctrl%d", index); + if (ImGui::Button(buffer) && s_binding_player < 0) { + s_binding_player = index; + } + } + ImGui::EndDisabled(); - if (ImGui::Button("Load Profile")) { + if (ImGui::Button("Apply Profile")) { __int64 xuid; auto p_mng = GameManager(); auto p_engine = GameEngine(); @@ -86,6 +135,10 @@ namespace MCC::Splitscreen { } if (ImGui::IsItemHovered()) ImGui::SetTooltip("Use this in game!!!"); + if (ImGui::Button("Save Profile")) { + MCC::Settings::Profile::CaptureFromRuntime(); + MCC::Settings::Profile::Save(); + } bool is_disabled = (!index && !p_setting->b_override_profile) || (index && p_setting->b_use_player0_profile); @@ -107,22 +160,48 @@ namespace MCC::Splitscreen { } void RealContext() { + bool dirty = false; char buffer[10]; auto p_setting = AlphaRing::Global::MCC::Splitscreen(); if (ImGui::BeginMenuBar()) { - ImGui::MenuItem(p_setting->b_override ? "Disable" : "Enable", nullptr, &p_setting->b_override); + // ImGui::MenuItem(p_setting->b_override ? "Disable" : "Enable", nullptr, &p_setting->b_override); + dirty |= ImGui::MenuItem( + p_setting->b_override ? "Disable" : "Enable", + nullptr, + &p_setting->b_override + ); if (ImGui::BeginMenu("Options")) { - ImGui::MenuItem("Use player1's profile", nullptr, &p_setting->b_use_player0_profile); - ImGui::MenuItem("Enable K/M for player1", nullptr, &p_setting->b_player0_use_km); - ImGui::MenuItem("Override profile", nullptr, &p_setting->b_override_profile); + // ImGui::MenuItem("Use player1's profile", nullptr, &p_setting->b_use_player0_profile); + dirty |= ImGui::MenuItem( + "Use player1's profile", + nullptr, + &p_setting->b_use_player0_profile + ); + // ImGui::MenuItem("Enable K/M for player1", nullptr, &p_setting->b_player0_use_km); + dirty |= ImGui::MenuItem( + "Enable K/M for player1", + nullptr, + &p_setting->b_player0_use_km + ); + // ImGui::MenuItem("Override profile", nullptr, &p_setting->b_override_profile); + dirty |= ImGui::MenuItem( + "Override profile", + nullptr, + &p_setting->b_override_profile + ); ImGui::EndMenu(); } #pragma region player count ImGui::PushItemWidth(200); + // int count = p_setting->player_count; + // if (ImGui::InputInt("Players", &count) && count >= 1 && count <=4) { + // p_setting->player_count = count; + // } int count = p_setting->player_count; - if (ImGui::InputInt("Players", &count) && count >= 1 && count <=4) { + if (ImGui::InputInt("Players", &count) && count >= 1 && count <= 4) { p_setting->player_count = count; + dirty = true; } ImGui::PopItemWidth(); ImGui::EndMenuBar(); @@ -139,5 +218,12 @@ namespace MCC::Splitscreen { } ImGui::EndTabBar(); } + + if (dirty) { + MCC::Settings::Splitscreen::CaptureFromRuntime(); + // MCC::Settings::Profile::CaptureFromRuntime(); + MCC::Settings::Splitscreen::Save(); + // MCC::Settings::Profile::Save(); + } } } \ No newline at end of file diff --git a/src/render/d3d11/D3d11.cpp b/src/render/d3d11/D3d11.cpp index 3c4d4c0..81ff90e 100644 --- a/src/render/d3d11/D3d11.cpp +++ b/src/render/d3d11/D3d11.cpp @@ -77,6 +77,10 @@ namespace AlphaRing::Render::D3d11 { pDXGIAdapter->GetParent(__uuidof(IDXGIFactory), (void **)&Graphics()->pIDXGIFactory); + // Release temporary COM objects to prevent memory leaks + pDXGIAdapter->Release(); + pDXGIDevice->Release(); + // Create Render Target View Graphics()->RecreateRenderTargetView(); diff --git a/src/render/d3d11/Graphics.cpp b/src/render/d3d11/Graphics.cpp index 8c5c989..406c13f 100644 --- a/src/render/d3d11/Graphics.cpp +++ b/src/render/d3d11/Graphics.cpp @@ -18,13 +18,19 @@ void Graphics_t::RecreateRenderTargetView() { void Graphics_t::SetWireframe() { D3D11_RASTERIZER_DESC r_desc; - ID3D11RasterizerState* r_state; - + ID3D11RasterizerState* r_state = nullptr; + ID3D11RasterizerState* r_state_new = nullptr; + pContext->RSGetState(&r_state); if (r_state != nullptr) { r_state->GetDesc(&r_desc); + r_state->Release(); // Release the original state + r_desc.FillMode = D3D11_FILL_WIREFRAME; - pDevice->CreateRasterizerState(&r_desc, &r_state); - pContext->RSSetState(r_state); + pDevice->CreateRasterizerState(&r_desc, &r_state_new); + pContext->RSSetState(r_state_new); + + if (r_state_new != nullptr) + r_state_new->Release(); // Release after setting (context holds its own ref) } } \ No newline at end of file diff --git a/src/render/imgui/ImGui.cpp b/src/render/imgui/ImGui.cpp index 78f8439..1b24051 100644 --- a/src/render/imgui/ImGui.cpp +++ b/src/render/imgui/ImGui.cpp @@ -61,6 +61,13 @@ namespace AlphaRing::Render::ImGui { } void Render() { + // Skip ImGui processing entirely when menu is hidden + // This prevents ImGui from capturing input and interfering with game menus + if (!AlphaRing::Global::Global()->show_imgui) { + AlphaRing::Input::Update(); + return; + } + ImGui_ImplDX11_NewFrame(); ImGui_ImplWin32_NewFrame(); ::ImGui::NewFrame(); @@ -70,17 +77,15 @@ namespace AlphaRing::Render::ImGui { AlphaRing::Input::Update(); - if (!AlphaRing::Global::Global()->show_imgui || !AlphaRing::Global::Global()->show_imgui_mouse) + if (!AlphaRing::Global::Global()->show_imgui_mouse) ::ImGui::SetMouseCursor(ImGuiMouseCursor_None); - if (!AlphaRing::Global::Global()->show_imgui) - return; - g_pMCCContext->render(); if (inGame && pGameGlobal != nullptr) { - if (pGameGlobal->current_game != 0) + // Bounds check: pages array has 7 elements (indices 0-6) + if (pGameGlobal->current_game > 0 && pGameGlobal->current_game < 7) { auto context = pages[pGameGlobal->current_game]; if (context != nullptr) diff --git a/src/wrapper/module_definition.cpp b/src/wrapper/module_definition.cpp index 4b2fe9b..2191fcd 100644 --- a/src/wrapper/module_definition.cpp +++ b/src/wrapper/module_definition.cpp @@ -2,31 +2,36 @@ #include +// Use OutputDebugStringA instead of MessageBoxA for Wine/Proton compatibility +// MessageBoxA blocks and can freeze the game on Linux +static void LogError(const char* message) { + OutputDebugStringA("[AlphaRing Error] "); + OutputDebugStringA(message); + OutputDebugStringA("\n"); +} + ModuleDefinition::ModuleDefinition(const char *moduleName, std::initializer_list funcs) { // Get System Directory wchar_t systemPath[MAX_PATH]; if (!GetSystemDirectoryW(systemPath, MAX_PATH)) { - MessageBoxA(0, "Unable to load system directory", "Error", 0); - - ExitProcess(0); + LogError("Unable to load system directory"); + ExitProcess(1); } // Load DLL std::filesystem::path path(systemPath); path.append(moduleName); if ((m_hModule = LoadLibraryW(path.c_str())) == nullptr) { - MessageBoxA(0, (std::string("Unable to load dll: ") + moduleName).c_str(), "Error", 0); - - ExitProcess(0); + LogError((std::string("Unable to load dll: ") + moduleName).c_str()); + ExitProcess(1); } // Find Function void *func_ptr; for (auto func: funcs) { if ((func_ptr = GetProcAddress(m_hModule, func)) == nullptr) { - MessageBoxA(0, (std::string("Unable to load function: ") + func).c_str(), "Error", 0); - - ExitProcess(0); + LogError((std::string("Unable to load function: ") + func).c_str()); + ExitProcess(1); } m_funcs.push_back(func_ptr); @@ -34,6 +39,6 @@ ModuleDefinition::ModuleDefinition(const char *moduleName, std::initializer_list } void *ModuleDefinition::GetFunc(int index) { - if (index < 0 || index > m_funcs.size()) return nullptr; + if (index < 0 || index >= (int)m_funcs.size()) return nullptr; return m_funcs[index]; }