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/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..386ecb3 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,24 @@ +{ + "configurations": [ + { + "name": "Win32", + "includePath": [ + "${workspaceFolder}/**", + "${vcpkgRoot}/x64-windows/include", + "${vcpkgRoot}/x86-windows/include" + ], + "defines": [ + "_DEBUG", + "UNICODE", + "_UNICODE" + ], + "windowsSdkVersion": "10.0.22621.0", + "compilerPath": "cl.exe", + "cStandard": "c17", + "cppStandard": "c++17", + "intelliSenseMode": "windows-msvc-x64", + "configurationProvider": "ms-vscode.cmake-tools" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..61d8d22 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "C_Cpp.errorSquiggles": "enabled", + "cmake.configureSettings": { + "CMAKE_TOOLCHAIN_FILE": "C:/Users/jackb/Repositories/vcpkg/scripts/buildsystems/vcpkg.cmake", + "VCPKG_TARGET_TRIPLET": "x64-windows-static-md" + } +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index f81a616..63207b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,9 @@ project(alpha_ring) set(CMAKE_CXX_STANDARD 17) +find_package(SDL2 CONFIG REQUIRED) +find_package(SDL2_mixer CONFIG REQUIRED) + set(VERSION "1.3528.0.0") set(WRAPPER_NAME "WTSAPI32") @@ -22,38 +25,39 @@ 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) include_directories(${CMAKE_SOURCE_DIR}/lib/utils/inc) -add_library(utils ${UTILS_SRC} "lib/game/inc/1.3495.0.0/offset_groundhog.h" "lib/game/inc/1.3495.0.0/offset_halo1.h" "lib/game/inc/1.3495.0.0/offset_halo2.h" "lib/game/inc/1.3495.0.0/offset_halo3.h" "lib/game/inc/1.3495.0.0/offset_halo3odst.h" "lib/game/inc/1.3495.0.0/offset_halo4.h" "lib/game/inc/1.3495.0.0/offset_haloreach.h" "lib/game/inc/1.3495.0.0/offset_mcc.h") +add_library(utils ${UTILS_SRC} "lib/game/inc/${VERSION}/offset_groundhog.h" "lib/game/inc/${VERSION}/offset_halo1.h" "lib/game/inc/${VERSION}/offset_halo2.h" "lib/game/inc/${VERSION}/offset_halo3.h" "lib/game/inc/${VERSION}/offset_halo3odst.h" "lib/game/inc/${VERSION}/offset_halo4.h" "lib/game/inc/${VERSION}/offset_haloreach.h" "lib/game/inc/${VERSION}/offset_mcc.h") # game file(GLOB_RECURSE GAME_SRC ${CMAKE_SOURCE_DIR}/lib/game/src/*.cpp) include_directories(${CMAKE_SOURCE_DIR}/lib/game/inc) include_directories(${CMAKE_SOURCE_DIR}/lib/game/inc/${VERSION}) -add_library(game ${GAME_SRC} "lib/game/inc/1.3495.0.0/offset_groundhog.h" "lib/game/inc/1.3495.0.0/offset_halo1.h" "lib/game/inc/1.3495.0.0/offset_halo2.h" "lib/game/inc/1.3495.0.0/offset_halo3.h" "lib/game/inc/1.3495.0.0/offset_halo3odst.h" "lib/game/inc/1.3495.0.0/offset_halo4.h" "lib/game/inc/1.3495.0.0/offset_haloreach.h" "lib/game/inc/1.3495.0.0/offset_mcc.h") +add_library(game ${GAME_SRC} "lib/game/inc/${VERSION}/offset_groundhog.h" "lib/game/inc/${VERSION}/offset_halo1.h" "lib/game/inc/${VERSION}/offset_halo2.h" "lib/game/inc/${VERSION}/offset_halo3.h" "lib/game/inc/${VERSION}/offset_halo3odst.h" "lib/game/inc/${VERSION}/offset_halo4.h" "lib/game/inc/${VERSION}/offset_haloreach.h" "lib/game/inc/${VERSION}/offset_mcc.h") # disable warning c4996 add_definitions(-D_SILENCE_ALL_MS_EXT_DEPRECATION_WARNINGS) @@ -62,11 +66,17 @@ add_definitions(-DIMGUI_DEFINE_MATH_OPERATORS) add_compile_definitions(WRAPPER_DLL_NAME="${WRAPPER_DLL_NAME}") add_compile_definitions(GAME_VERSION="${VERSION}") -file(GLOB_RECURSE CORE_SRC ${CMAKE_SOURCE_DIR}/src/*.cpp) +file(GLOB_RECURSE CORE_SRC + ${CMAKE_SOURCE_DIR}/src/*.cpp + ${CMAKE_SOURCE_DIR}/src/*.c +) -add_library(${WRAPPER_NAME} SHARED ${CORE_SRC} "lib/game/inc/1.3495.0.0/offset_groundhog.h" "lib/game/inc/1.3495.0.0/offset_halo1.h" "lib/game/inc/1.3495.0.0/offset_halo2.h" "lib/game/inc/1.3495.0.0/offset_halo3.h" "lib/game/inc/1.3495.0.0/offset_halo3odst.h" "lib/game/inc/1.3495.0.0/offset_halo4.h" "lib/game/inc/1.3495.0.0/offset_haloreach.h" "lib/game/inc/1.3495.0.0/offset_mcc.h") +add_library(${WRAPPER_NAME} SHARED ${CORE_SRC} "lib/game/inc/${VERSION}/offset_groundhog.h" "lib/game/inc/${VERSION}/offset_halo1.h" "lib/game/inc/${VERSION}/offset_halo2.h" "lib/game/inc/${VERSION}/offset_halo3.h" "lib/game/inc/${VERSION}/offset_halo3odst.h" "lib/game/inc/${VERSION}/offset_halo4.h" "lib/game/inc/${VERSION}/offset_haloreach.h" "lib/game/inc/${VERSION}/offset_mcc.h") -target_link_libraries(${WRAPPER_NAME} PUBLIC ${LIBRARY_LIST}) +target_link_libraries(${WRAPPER_NAME} PUBLIC ${LIBRARY_LIST} + SDL2::SDL2-static + SDL2_mixer::SDL2_mixer-static +) if (MSVC) target_link_options(${WRAPPER_NAME} PUBLIC /DEBUG) @@ -75,4 +85,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/colorMapping.csv b/colorMapping.csv new file mode 100644 index 0000000..7a4c301 --- /dev/null +++ b/colorMapping.csv @@ -0,0 +1,31 @@ +AlphaRing,Halo 4,Halo Reach,Halo 3,Halo 3 ODST,Halo 2,Halo 2 Anniversary,Halo 1 +Steel,H4_Color0_Steel,HR_Color_Steel,H3_COLOR0_STEEL,ODST_COLOR_BLACK,H2_Color_2,H2A_Color_2,H1_Color_2 +Silver,H4_Color1_Silver,HR_Color_Silver,H3_COLOR1_SILVER,ODST_COLOR_GRAY,H2_Color_2,H2A_Color_1,H1_Color_5 +White,H4_Color2_White,HR_Color_White,H3_COLOR2_WHITE,ODST_COLOR_SNOW,H2_Color_1,H2A_Color_3,H1_Color_1 +Brown,H4_Color3_Brown,HR_Color_Brown,H3_COLOR27_BROWN,ODST_COLOR_BROWN,H2_Color_17,H2A_Color_4,H1_Color_15 +Tan,H4_Color4_Tan,HR_Color_Tan,H3_COLOR28_TAN,ODST_COLOR_TAN,H2_Color_18,H2A_Color_5,H1_Color_16 +Khaki,H4_Color5_Khaki,HR_Color_Khaki,H3_COLOR29_KHAKI,ODST_COLOR_KHAKI,H2_Color_18,H2A_Color_6,H1_Color_16 +Sage,H4_Color6_Sage,HR_Color_Sage,H3_COLOR12_SAGE,ODST_COLOR_SAGE,H2_Color_8,H2A_Color_7,H1_Color_14 +Olive,H4_Color7_Olive,HR_Color_Olive,H3_COLOR14_OLIVE,ODST_COLOR_OLIVE,H2_Color_6,H2A_Color_8,H1_Color_14 +Drab,H4_Color8_Drab,HR_Color_Drab,H3_COLOR13_GREEN,ODST_COLOR_DRAB,H2_Color_7,H2A_Color_9,H1_Color_7 +Forest,H4_Color9_Forest,HR_Color_Forest,H3_COLOR12_SAGE,ODST_COLOR_FOREST,H2_Color_8,H2A_Color_10,H1_Color_14 +Green,H4_Color10_Green,HR_Color_Green,H3_COLOR13_GREEN,ODST_COLOR_GREEN,H2_Color_7,H2A_Color_11,H1_Color_14 +Sea Foam,H4_Color11_SeaFoam,HR_Color_SeaFoam,H3_COLOR15_TEAL,ODST_COLOR_SEA_FOAM,H2_Color_10,H2A_Color_12,H1_Color_13 +Teal,H4_Color12_Teal,HR_Color_Teal,H3_COLOR15_TEAL,ODST_COLOR_TEAL,H2_Color_10,H2A_Color_13,H1_Color_13 +Aqua,H4_Color13_Aqua,HR_Color_Aqua,H3_COLOR16_AQUA,ODST_COLOR_AQUA,H2_Color_9,H2A_Color_14,H1_Color_10 +Cyan,H4_Color14_Cyan,HR_Color_Cyan,H3_COLOR17_CYAN,ODST_COLOR_CYAN,H2_Color_9,H2A_Color_15,H1_Color_10 +Blue,H4_Color15_Blue,HR_Color_Blue,H3_COLOR18_BLUE,ODST_COLOR_BLUE,H2_Color_12,H2A_Color_16,H1_Color_4 +Cobalt,H4_Color16_Cobalt,HR_Color_Cobalt,H3_COLOR19_COBALT,ODST_COLOR_COBALT,H2_Color_11,H2A_Color_17,H1_Color_11 +Ice,H4_Color17_Ice,HR_Color_Ice,H3_COLOR20_SAPHIRE,ODST_COLOR_ICE,H2_Color_11,H2A_Color_18,H1_Color_11 +Violet,H4_Color18_Violet,HR_Color_Violet,H3_COLOR21_VIOLET,ODST_COLOR_VIOLET,H2_Color_13,H2A_Color_19,H1_Color_9 +Orchid,H4_Color19_Orchid,HR_Color_Orchid,H3_COLOR22_ORCHID,ODST_COLOR_PINK,H2_Color_15,H2A_Color_20,H1_Color_8 +Lavender,H4_Color20_Lavender,HR_Color_Lavender,H3_COLOR23_LAVENDER,ODST_COLOR_LAVENDER,H2_Color_14,H2A_Color_21,H1_Color_9 +Maroon,H4_Color21_Maroon,HR_Color_Maroon,H3_COLOR3_RED,ODST_COLOR_MAROON,H2_Color_16,H2A_Color_22,H1_Color_17 +Brick,H4_Color22_Brick,HR_Color_Brick,H3_COLOR3_RED,ODST_COLOR_BRICK,H2_Color_3,H2A_Color_23,H1_Color_3 +Rose,H4_Color23_Rose,HR_Color_Rose,H3_COLOR5_SALMON,ODST_COLOR_ROSE,H2_Color_15,H2A_Color_24,H1_Color_8 +Rust,H4_Color24_Rust,HR_Color_Rust,H3_COLOR6_ORANGE,ODST_COLOR_COCOA,H2_Color_4,H2A_Color_25,H1_Color_12 +Coral,H4_Color25_Coral,HR_Color_Coral,H3_COLOR7_CORAL,ODST_COLOR_CORAL,H2_Color_4,H2A_Color_26,H1_Color_12 +Peach,H4_Color26_Peach,HR_Color_Peach,H3_COLOR8_PEACH,ODST_COLOR_ROSE,H2_Color_15,H2A_Color_27,H1_Color_18 +Gold,H4_Color27_Gold,HR_Color_Gold,H3_COLOR9_GOLD,ODST_COLOR_GOLD,H2_Color_5,H2A_Color_28,H1_Color_16 +Yellow,H4_Color28_Yellow,HR_Color_Yellow,H3_COLOR10_YELLOW,ODST_COLOR_SAND,H2_Color_5,H2A_Color_29,H1_Color_6 +Pale,H4_Color28_Yellow,HR_Color_Pale,H3_COLOR11_PALE,ODST_COLOR_DESERT,H2_Color_5,H2A_Color_29,H1_Color_6 \ No newline at end of file diff --git a/handoff.md b/handoff.md new file mode 100644 index 0000000..9b9e6ed --- /dev/null +++ b/handoff.md @@ -0,0 +1,298 @@ +# 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 a Xbox 360-style menu. + +**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 +│ │ └── 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 +- **Controller-friendly menu** - works, but can be improved +- **Wireframe rendering** - works +- **Controller-to-player binding** - IMPLEMENTED (see below) +- **Button-to-action gamepad mapping** - IMPLEMENTED with bind feature +- **Configurable menu binding** - works + +--- + +## 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; +} +``` + +--- + +## 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/global/Global.h b/src/global/Global.h index 99b654c..b78e6fc 100644 --- a/src/global/Global.h +++ b/src/global/Global.h @@ -14,9 +14,9 @@ namespace AlphaRing::Global { bool wireframe; bool wireframe_model; bool wireframe_structure; - bool show_imgui = true; + bool show_imgui = false; // on menu - bool show_imgui_mouse = true; + bool show_imgui_mouse = false; bool pause_game_on_menu_shown = false; bool disable_input_on_menu_shown = true; }; @@ -39,7 +39,7 @@ namespace AlphaRing::Global { // player 0 bool b_player0_use_km = true; - bool b_override_profile = false; + bool b_override_profile = true; bool b_use_player0_profile = true; }; } diff --git a/src/hook/Hook.cpp b/src/hook/Hook.cpp index 58cc194..1ab4489 100644 --- a/src/hook/Hook.cpp +++ b/src/hook/Hook.cpp @@ -29,7 +29,8 @@ namespace AlphaRing::Hook { assertm(result, "failed to initialize minhook"); LOG_INFO("Initializing AlphaRing..."); - LOG_INFO("Created by WinterSquire, updated by xTrxplex\n"); + LOG_INFO("Created by WinterSquire, updated by thejackbitt\n"); + LOG_WARNING("** CURRENTLY IN BETA - STRAP IN FOR A BUMPY RIDE **"); LOG_WARNING(" == This version only supports the steam version of the game =="); if ((hModule = (__int64)GetModuleHandleA("MCC-Win64-Shipping.exe")) != 0) { @@ -51,8 +52,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/input/Input.cpp b/src/input/Input.cpp index d0e89a8..f4c9966 100644 --- a/src/input/Input.cpp +++ b/src/input/Input.cpp @@ -1,14 +1,24 @@ #include "Input.h" +#include "MenuConfig.h" #include "common.h" +#include "MinHook.h" #include "imgui.h" #include "global/Global.h" +#include "render/imgui/game/xbox/CXboxContext.h" static HMODULE hModule; -static DWORD (WINAPI* g_pXInputGetState)(_In_ DWORD dwUserIndex, _Out_ XINPUT_STATE* pState) WIN_NOEXCEPT; +static DWORD (WINAPI* g_pXInputGetState)(_In_ DWORD dwUserIndex, _Out_ XINPUT_STATE* pState) WIN_NOEXCEPT; static DWORD (WINAPI* g_pXInputSetState)(_In_ DWORD dwUserIndex, _In_ XINPUT_VIBRATION* pVibration) WIN_NOEXCEPT; +static DWORD WINAPI XInputGetStateDetour(DWORD dwUserIndex, XINPUT_STATE* pState) { + DWORD result = g_pXInputGetState(dwUserIndex, pState); + if (result == ERROR_SUCCESS && pState && g_pXboxContext && g_pXboxContext->isOpen()) + ZeroMemory(&pState->Gamepad, sizeof(pState->Gamepad)); + return result; +} + namespace AlphaRing::Input { bool Init() { if ((hModule = GetModuleHandleA("XINPUT1_3.dll")) || @@ -20,6 +30,17 @@ namespace AlphaRing::Input { assertm(hModule != nullptr, "failed to find xinput module"); + auto xInputAddr = (LPVOID)GetProcAddress(hModule, "XInputGetState"); + MH_STATUS mhStatus = MH_Initialize(); + if (mhStatus == MH_OK || mhStatus == MH_ERROR_ALREADY_INITIALIZED) { + if (MH_CreateHook(xInputAddr, &XInputGetStateDetour, + reinterpret_cast(&g_pXInputGetState)) == MH_OK) { + MH_EnableHook(xInputAddr); + } + } + + g_menuConfig = MenuConfig::load(); + return true; } @@ -39,39 +60,116 @@ namespace AlphaRing::Input { } bool Update() { - static bool b_toggled = false; - static bool b_pressed = false; - XINPUT_STATE state; - - if (!GetXInputGetState(0, &state)) + static bool b_pressed = false; + static WORD prevButtons = 0; + static bool stickActive[4] = {}; // up, down, left, right + + // keyboard nav state — tracks rising edge for each key + struct NavKey { int vk; InputCommand cmd; bool wasDown; }; + static NavKey navKeys[] = { + {'W', InputCommand::Up, false}, + {VK_UP, InputCommand::Up, false}, + {'S', InputCommand::Down, false}, + {VK_DOWN, InputCommand::Down, false}, + {'A', InputCommand::Left, false}, + {VK_LEFT, InputCommand::Left, false}, + {'D', InputCommand::Right, false}, + {VK_RIGHT, InputCommand::Right, false}, + {VK_RETURN, InputCommand::Select, false}, + {VK_ESCAPE, InputCommand::Back, false}, + }; + + // Zero-initialized so a disconnected/missing controller (GetXInputGetState + // failure) still leaves us with a neutral state instead of bailing out — + // keyboard nav below must keep working even with no gamepad plugged in. + XINPUT_STATE state{}; + GetXInputGetState(0, &state); + + WORD buttons = state.Gamepad.wButtons; + WORD justPressed = buttons & ~prevButtons; + prevButtons = buttons; + + // configurable debug UI keyboard key + int debugKey = g_menuConfig.debugKeyboardVKey; + if (GetAsyncKeyState(debugKey) & 0x8000) { + AlphaRing::Global::Global()->show_imgui = !AlphaRing::Global::Global()->show_imgui; + return false; + } + + // configurable debug UI combo + WORD debugCombo = g_menuConfig.debugComboMask; + if ((buttons & debugCombo) == debugCombo && (justPressed & debugCombo) != 0) { + AlphaRing::Global::Global()->show_imgui = !AlphaRing::Global::Global()->show_imgui; return false; + } - if (state.Gamepad.wButtons & XINPUT_GAMEPAD_START && state.Gamepad.wButtons & XINPUT_GAMEPAD_BACK) { - if (!b_toggled) { - AlphaRing::Global::Global()->show_imgui = !AlphaRing::Global::Global()->show_imgui; - b_toggled = true; - return false; + // configurable xbox menu combo + WORD menuCombo = g_menuConfig.controllerComboMask; + if ((buttons & menuCombo) == menuCombo && (justPressed & menuCombo) != 0) { + if (g_pXboxContext) { + if (g_pXboxContext->isOpen()) g_pXboxContext->close(); + else g_pXboxContext->open(); } - } else { - b_toggled = false; } + // navigation and input consumption while Xbox menu is open + if (g_pXboxContext && g_pXboxContext->isOpen()) { + // d-pad / face button navigation + const auto send = [&](WORD btn, InputCommand cmd) { + if (justPressed & btn) g_pXboxContext->handleInput(cmd); + }; + send(XINPUT_GAMEPAD_DPAD_UP, InputCommand::Up); + send(XINPUT_GAMEPAD_DPAD_DOWN, InputCommand::Down); + send(XINPUT_GAMEPAD_DPAD_LEFT, InputCommand::Left); + send(XINPUT_GAMEPAD_DPAD_RIGHT, InputCommand::Right); + send(XINPUT_GAMEPAD_A, InputCommand::Select); + send(XINPUT_GAMEPAD_B, InputCommand::Back); + + // left stick navigation — edge-triggered on deadzone crossing + const SHORT dz = XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE; + bool upNow = state.Gamepad.sThumbLY > dz; + bool downNow = state.Gamepad.sThumbLY < -dz; + bool leftNow = state.Gamepad.sThumbLX < -dz; + bool rightNow = state.Gamepad.sThumbLX > dz; + + if (upNow && !stickActive[0]) g_pXboxContext->handleInput(InputCommand::Up); + if (downNow && !stickActive[1]) g_pXboxContext->handleInput(InputCommand::Down); + if (leftNow && !stickActive[2]) g_pXboxContext->handleInput(InputCommand::Left); + if (rightNow && !stickActive[3]) g_pXboxContext->handleInput(InputCommand::Right); + + stickActive[0] = upNow; + stickActive[1] = downNow; + stickActive[2] = leftNow; + stickActive[3] = rightNow; + + // keyboard navigation via polling + for (auto& k : navKeys) { + bool isDown = (GetAsyncKeyState(k.vk) & 0x8000) != 0; + if (isDown && !k.wasDown) g_pXboxContext->handleInput(k.cmd); + k.wasDown = isDown; + } + + return true; // consume all remaining controller input + } + + // reset edge-tracking state when menu is closed + stickActive[0] = stickActive[1] = stickActive[2] = stickActive[3] = false; + for (auto& k : navKeys) k.wasDown = false; + + // thumbstick mouse control for debug UI if (AlphaRing::Global::Global()->show_imgui) { const auto f_speed = [](SHORT x, SHORT y) -> ImVec2 { - // Mouse Move Speed for Gamepad const auto speed = 5.0f; - // Normalize Move Speed const auto f_normalize = [](SHORT sThumb) -> float { const auto deadZone = XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE; return (abs(sThumb) > deadZone) ? (sThumb / 32767.0f) : 0.0f; }; - // Get Final Move Speed return {f_normalize(x) * speed, -f_normalize(y) * speed}; }; ImGuiIO& io = ImGui::GetIO(); - if (state.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) { + if (buttons & XINPUT_GAMEPAD_RIGHT_SHOULDER) { if (!b_pressed) { io.MouseDown[0] = true; b_pressed = true; @@ -86,4 +184,4 @@ namespace AlphaRing::Input { return true; } -} \ No newline at end of file +} diff --git a/src/input/MenuConfig.cpp b/src/input/MenuConfig.cpp new file mode 100644 index 0000000..2091056 --- /dev/null +++ b/src/input/MenuConfig.cpp @@ -0,0 +1,330 @@ +#include "MenuConfig.h" + +#include +#include +#include +#include +#include +#include + +MenuConfig g_menuConfig; + +static const std::map k_buttonMap = { + {"DPAD_UP", XINPUT_GAMEPAD_DPAD_UP}, + {"DPAD_DOWN", XINPUT_GAMEPAD_DPAD_DOWN}, + {"DPAD_LEFT", XINPUT_GAMEPAD_DPAD_LEFT}, + {"DPAD_RIGHT", XINPUT_GAMEPAD_DPAD_RIGHT}, + {"START", XINPUT_GAMEPAD_START}, + {"BACK", XINPUT_GAMEPAD_BACK}, + {"LEFT_THUMB", XINPUT_GAMEPAD_LEFT_THUMB}, + {"RIGHT_THUMB", XINPUT_GAMEPAD_RIGHT_THUMB}, + {"LEFT_SHOULDER", XINPUT_GAMEPAD_LEFT_SHOULDER}, + {"RIGHT_SHOULDER", XINPUT_GAMEPAD_RIGHT_SHOULDER}, + {"A", XINPUT_GAMEPAD_A}, + {"B", XINPUT_GAMEPAD_B}, + {"X", XINPUT_GAMEPAD_X}, + {"Y", XINPUT_GAMEPAD_Y}, +}; + +static const std::map k_gamepadButtonMap = { + {"DPAD_UP", CGamepadMapping::DpadUp}, + {"DPAD_DOWN", CGamepadMapping::DpadDown}, + {"DPAD_LEFT", CGamepadMapping::DpadLeft}, + {"DPAD_RIGHT", CGamepadMapping::DpadRight}, + {"START", CGamepadMapping::Start}, + {"BACK", CGamepadMapping::Back}, + {"LEFT_THUMB", CGamepadMapping::LeftThumb}, + {"RIGHT_THUMB", CGamepadMapping::RightThumb}, + {"LEFT_SHOULDER", CGamepadMapping::LeftShoulder}, + {"RIGHT_SHOULDER", CGamepadMapping::RightShoulder}, + {"LEFT_TRIGGER", CGamepadMapping::LeftTrigger}, + {"RIGHT_TRIGGER", CGamepadMapping::RightTrigger}, + {"A", CGamepadMapping::A}, + {"B", CGamepadMapping::B}, + {"X", CGamepadMapping::X}, + {"Y", CGamepadMapping::Y}, +}; + +// Maps a controller-profile cfg key prefix ("d_", "s_", ...) to the profile +// index used by the "Controller Profile" subpage in CXboxMenu.hpp. +static const std::map k_profilePrefixMap = { + {"d_", 0}, // default + {"s_", 1}, // southpaw + {"b_", 2}, // boxer + {"g_", 3}, // green thumb + {"j_", 4}, // bumper jumper + {"r_", 5}, // recon +}; + +// Maps a controller-profile cfg key suffix to the field it binds. +static const std::map k_profileActionMap = { + {"jump", &ControllerProfileMapping::jump}, + {"switch_grenades", &ControllerProfileMapping::switchGrenades}, + {"use_equipment", &ControllerProfileMapping::useEquipment}, + {"action_interact", &ControllerProfileMapping::actionInteract}, + {"reload_right_weapon", &ControllerProfileMapping::reloadRightWeapon}, + {"change_weapon", &ControllerProfileMapping::changeWeapon}, + {"melee", &ControllerProfileMapping::melee}, + {"toggle_flashlight", &ControllerProfileMapping::toggleFlashlight}, + {"throw_grenade", &ControllerProfileMapping::throwGrenade}, + {"shoot", &ControllerProfileMapping::shoot}, + {"crouch", &ControllerProfileMapping::crouch}, + {"player_zoom", &ControllerProfileMapping::playerZoom}, + {"multiplayer_scoreboard", &ControllerProfileMapping::multiplayerScoreboard}, +}; + +static const std::map k_keyMap = { + {"F1", VK_F1}, {"F2", VK_F2}, {"F3", VK_F3}, {"F4", VK_F4}, + {"F5", VK_F5}, {"F6", VK_F6}, {"F7", VK_F7}, {"F8", VK_F8}, + {"F9", VK_F9}, {"F10", VK_F10}, {"F11", VK_F11}, {"F12", VK_F12}, + {"ENTER", VK_RETURN}, + {"ESCAPE", VK_ESCAPE}, + {"SPACE", VK_SPACE}, + {"TAB", VK_TAB}, +}; + +std::string MenuConfig::trim(const std::string& s) { + size_t start = s.find_first_not_of(" \t\r\n"); + size_t end = s.find_last_not_of(" \t\r\n"); + return (start == std::string::npos) ? "" : s.substr(start, end - start + 1); +} + +WORD MenuConfig::parseButton(const std::string& raw) { + std::string name = trim(raw); + std::transform(name.begin(), name.end(), name.begin(), ::toupper); + auto it = k_buttonMap.find(name); + return (it != k_buttonMap.end()) ? it->second : 0; +} + +CGamepadMapping::eButton MenuConfig::parseGamepadButton(const std::string& raw) { + std::string name = trim(raw); + std::transform(name.begin(), name.end(), name.begin(), ::toupper); + auto it = k_gamepadButtonMap.find(name); + return (it != k_gamepadButtonMap.end()) ? it->second : CGamepadMapping::None; +} + +int MenuConfig::parseKey(const std::string& raw) { + std::string name = trim(raw); + std::transform(name.begin(), name.end(), name.begin(), ::toupper); + + auto it = k_keyMap.find(name); + if (it != k_keyMap.end()) return it->second; + + // Single letter A-Z + if (name.size() == 1 && name[0] >= 'A' && name[0] <= 'Z') + return static_cast(name[0]); + + return VK_F4; +} + +void MenuConfig::writeDefault(const std::string& path) { + std::ofstream ofs(path); + if (!ofs) return; + + ofs << "# AlphaRing Xbox Menu Configuration\n" + "#\n" + "# Available Xbox controller button IDs:\n" + "# DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT\n" + "# START, BACK\n" + "# LEFT_THUMB, RIGHT_THUMB\n" + "# LEFT_SHOULDER, RIGHT_SHOULDER\n" + "# A, B, X, Y\n" + "#\n" + "# Use + to combine buttons for a combo (e.g., START+BACK)\n" + "# Keyboard key IDs: F1-F12, ENTER, ESCAPE, SPACE, TAB, A-Z\n" + "# Note: open_menu_controller and open_debug_controller must not use the same combo.\n" + "\n" + "open_menu_controller=START+LEFT_THUMB\n" + "open_debug_controller=START+RIGHT_THUMB\n" + "open_menu_keyboard=F4\n" + "open_debug_keyboard=F1\n" + "\n" + "# Controller Profile Mappings\n" + "#\n" + "# Available Xbox controller button IDs:\n" + "# DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT\n" + "# START, BACK\n" + "# LEFT_THUMB, RIGHT_THUMB\n" + "# LEFT_SHOULDER, RIGHT_SHOULDER\n" + "# LEFT_TRIGGER, RIGHT_TRIGGER\n" + "# A, B, X, Y\n" + "#\n" + "# Select a profile for a player from the Controller Profile menu\n" + "# (Default, Southpaw, Boxer, Green Thumb, Bumper Jumper, Recon).\n" + "\n" + "# default\n" + "d_jump = A\n" + "d_switch_grenades = B\n" + "d_use_equipment = LEFT_SHOULDER\n" + "d_action_interact = X\n" + "d_reload_right_weapon = X\n" + "d_change_weapon = Y\n" + "d_melee = RIGHT_SHOULDER\n" + "d_toggle_flashlight = DPAD_LEFT\n" + "d_throw_grenade = LEFT_TRIGGER\n" + "d_shoot = RIGHT_TRIGGER\n" + "d_crouch = LEFT_THUMB\n" + "d_player_zoom = RIGHT_THUMB\n" + "d_multiplayer_scoreboard = BACK\n" + "\n" + "# southpaw\n" + "s_jump = A\n" + "s_switch_grenades = B\n" + "s_use_equipment = RIGHT_SHOULDER\n" + "s_action_interact = X\n" + "s_reload_right_weapon = X\n" + "s_change_weapon = Y\n" + "s_melee = LEFT_SHOULDER\n" + "s_toggle_flashlight = DPAD_LEFT\n" + "s_throw_grenade = RIGHT_TRIGGER\n" + "s_shoot = LEFT_TRIGGER\n" + "s_crouch = RIGHT_THUMB\n" + "s_player_zoom = LEFT_THUMB\n" + "s_multiplayer_scoreboard = BACK\n" + "\n" + "# boxer\n" + "b_jump = A\n" + "b_switch_grenades = B\n" + "b_use_equipment = LEFT_SHOULDER\n" + "b_action_interact = X\n" + "b_reload_right_weapon = X\n" + "b_change_weapon = Y\n" + "b_melee = LEFT_TRIGGER\n" + "b_toggle_flashlight = DPAD_LEFT\n" + "b_throw_grenade = LEFT_SHOULDER\n" + "b_shoot = RIGHT_TRIGGER\n" + "b_crouch = LEFT_THUMB\n" + "b_player_zoom = RIGHT_THUMB\n" + "b_multiplayer_scoreboard = BACK\n" + "\n" + "# green thumb\n" + "g_jump = A\n" + "g_switch_grenades = B\n" + "g_use_equipment = LEFT_SHOULDER\n" + "g_action_interact = X\n" + "g_reload_right_weapon = X\n" + "g_change_weapon = Y\n" + "g_melee = RIGHT_THUMB\n" + "g_toggle_flashlight = DPAD_LEFT\n" + "g_throw_grenade = LEFT_TRIGGER\n" + "g_shoot = RIGHT_TRIGGER\n" + "g_crouch = LEFT_THUMB\n" + "g_player_zoom = RIGHT_SHOULDER\n" + "g_multiplayer_scoreboard = BACK\n" + "\n" + "# bumper jumper\n" + "j_jump = LEFT_SHOULDER\n" + "j_switch_grenades = A\n" + "j_use_equipment = X\n" + "j_action_interact = B\n" + "j_reload_right_weapon = B\n" + "j_change_weapon = Y\n" + "j_melee = RIGHT_SHOULDER\n" + "j_toggle_flashlight = DPAD_LEFT\n" + "j_throw_grenade = LEFT_TRIGGER\n" + "j_shoot = RIGHT_TRIGGER\n" + "j_crouch = LEFT_THUMB\n" + "j_player_zoom = RIGHT_THUMB\n" + "j_multiplayer_scoreboard = BACK\n" + "\n" + "# recon\n" + "r_jump = A\n" + "r_switch_grenades = X\n" + "r_use_equipment = LEFT_SHOULDER\n" + "r_action_interact = RIGHT_SHOULDER\n" + "r_reload_right_weapon = RIGHT_SHOULDER\n" + "r_change_weapon = Y\n" + "r_melee = B\n" + "r_toggle_flashlight = DPAD_LEFT\n" + "r_throw_grenade = LEFT_TRIGGER\n" + "r_shoot = RIGHT_TRIGGER\n" + "r_crouch = LEFT_THUMB\n" + "r_player_zoom = RIGHT_THUMB\n" + "r_multiplayer_scoreboard = BACK\n"; +} + +MenuConfig MenuConfig::load() { + MenuConfig cfg; + std::ifstream ifs(k_configPath); + + if (!ifs.is_open()) { + writeDefault(k_configPath); + return cfg; + } + + std::string line; + while (std::getline(ifs, line)) { + auto hash = line.find('#'); + if (hash != std::string::npos) line = line.substr(0, hash); + line = trim(line); + if (line.empty()) continue; + + auto eq = line.find('='); + if (eq == std::string::npos) continue; + + std::string key = trim(line.substr(0, eq)); + std::string val = trim(line.substr(eq + 1)); + std::transform(key.begin(), key.end(), key.begin(), ::tolower); + + auto parseCombo = [&](const std::string& v) -> WORD { + WORD mask = 0; + std::istringstream ss(v); + std::string token; + while (std::getline(ss, token, '+')) + mask |= parseButton(token); + return mask; + }; + + if (key == "open_menu_controller") { + WORD mask = parseCombo(val); + if (mask != 0) cfg.controllerComboMask = mask; + } + else if (key == "open_debug_controller") { + WORD mask = parseCombo(val); + if (mask != 0) cfg.debugComboMask = mask; + } + else if (key == "open_menu_keyboard") { + int vk = parseKey(val); + if (vk != 0) cfg.keyboardVKey = vk; + } + else if (key == "open_debug_keyboard") { + int vk = parseKey(val); + if (vk != 0) cfg.debugKeyboardVKey = vk; + } + else if (key.size() > 2 && key[1] == '_') { + // Controller profile binding: "_ = BUTTON" (e.g. d_jump = A) + auto profileIt = k_profilePrefixMap.find(key.substr(0, 2)); + auto actionIt = k_profileActionMap.find(key.substr(2)); + if (profileIt != k_profilePrefixMap.end() && actionIt != k_profileActionMap.end()) { + CGamepadMapping::eButton btn = parseGamepadButton(val); + if (btn != CGamepadMapping::None) + cfg.controllerProfiles[profileIt->second].*(actionIt->second) = btn; + } + } + } + + return cfg; +} + +void MenuConfig::ApplyControllerProfile(int profileIndex, CGamepadMapping& mapping) const { + if (profileIndex < 0 || profileIndex >= k_profileCount) + profileIndex = 0; + + for (auto& action : mapping.actions) + action = CGamepadMapping::None; + + const ControllerProfileMapping& p = controllerProfiles[profileIndex]; + mapping.actions[0] = p.jump; + mapping.actions[1] = p.switchGrenades; + mapping.actions[2] = p.actionInteract; + mapping.actions[3] = p.reloadRightWeapon; + mapping.actions[4] = p.changeWeapon; + mapping.actions[5] = p.melee; + mapping.actions[6] = p.toggleFlashlight; + mapping.actions[7] = p.throwGrenade; + mapping.actions[8] = p.shoot; + mapping.actions[9] = p.crouch; + mapping.actions[10] = p.playerZoom; + mapping.actions[20] = p.multiplayerScoreboard; + mapping.actions[23] = p.useEquipment; +} diff --git a/src/input/MenuConfig.h b/src/input/MenuConfig.h new file mode 100644 index 0000000..80517c4 --- /dev/null +++ b/src/input/MenuConfig.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include + +#include "mcc/CGamepadMapping.h" + +// One button binding per Halo action, used to build a CGamepadMapping for a +// controller profile (default/southpaw/boxer/green thumb/bumper jumper/recon). +struct ControllerProfileMapping { + CGamepadMapping::eButton jump; + CGamepadMapping::eButton switchGrenades; + CGamepadMapping::eButton useEquipment; + CGamepadMapping::eButton actionInteract; + CGamepadMapping::eButton reloadRightWeapon; + CGamepadMapping::eButton changeWeapon; + CGamepadMapping::eButton melee; + CGamepadMapping::eButton toggleFlashlight; + CGamepadMapping::eButton throwGrenade; + CGamepadMapping::eButton shoot; + CGamepadMapping::eButton crouch; + CGamepadMapping::eButton playerZoom; + CGamepadMapping::eButton multiplayerScoreboard; +}; + +struct MenuConfig { + WORD controllerComboMask = XINPUT_GAMEPAD_START | XINPUT_GAMEPAD_LEFT_THUMB; + WORD debugComboMask = XINPUT_GAMEPAD_START | XINPUT_GAMEPAD_RIGHT_THUMB; + int keyboardVKey = VK_F4; + int debugKeyboardVKey = VK_F1; + + // Indexed to match the "Controller Profile" subpage order in CXboxMenu.hpp: + // 0 = Default, 1 = Southpaw, 2 = Boxer, 3 = Green Thumb, 4 = Bumper Jumper, 5 = Recon + static constexpr int k_profileCount = 6; + ControllerProfileMapping controllerProfiles[k_profileCount] = { + // Default + {CGamepadMapping::A, CGamepadMapping::B, CGamepadMapping::LeftShoulder, CGamepadMapping::X, + CGamepadMapping::X, CGamepadMapping::Y, CGamepadMapping::RightShoulder, CGamepadMapping::DpadLeft, + CGamepadMapping::LeftTrigger, CGamepadMapping::RightTrigger, CGamepadMapping::LeftThumb, + CGamepadMapping::RightThumb, CGamepadMapping::Back}, + // Southpaw + {CGamepadMapping::A, CGamepadMapping::B, CGamepadMapping::RightShoulder, CGamepadMapping::X, + CGamepadMapping::X, CGamepadMapping::Y, CGamepadMapping::LeftShoulder, CGamepadMapping::DpadLeft, + CGamepadMapping::RightTrigger, CGamepadMapping::LeftTrigger, CGamepadMapping::RightThumb, + CGamepadMapping::LeftThumb, CGamepadMapping::Back}, + // Boxer + {CGamepadMapping::A, CGamepadMapping::B, CGamepadMapping::LeftShoulder, CGamepadMapping::X, + CGamepadMapping::X, CGamepadMapping::Y, CGamepadMapping::LeftTrigger, CGamepadMapping::DpadLeft, + CGamepadMapping::LeftShoulder, CGamepadMapping::RightTrigger, CGamepadMapping::LeftThumb, + CGamepadMapping::RightThumb, CGamepadMapping::Back}, + // Green Thumb + {CGamepadMapping::A, CGamepadMapping::B, CGamepadMapping::LeftShoulder, CGamepadMapping::X, + CGamepadMapping::X, CGamepadMapping::Y, CGamepadMapping::RightThumb, CGamepadMapping::DpadLeft, + CGamepadMapping::LeftTrigger, CGamepadMapping::RightTrigger, CGamepadMapping::LeftThumb, + CGamepadMapping::RightShoulder, CGamepadMapping::Back}, + // Bumper Jumper + {CGamepadMapping::LeftShoulder, CGamepadMapping::A, CGamepadMapping::X, CGamepadMapping::B, + CGamepadMapping::B, CGamepadMapping::Y, CGamepadMapping::RightShoulder, CGamepadMapping::DpadLeft, + CGamepadMapping::LeftTrigger, CGamepadMapping::RightTrigger, CGamepadMapping::LeftThumb, + CGamepadMapping::RightThumb, CGamepadMapping::Back}, + // Recon + {CGamepadMapping::A, CGamepadMapping::X, CGamepadMapping::LeftShoulder, CGamepadMapping::RightShoulder, + CGamepadMapping::RightShoulder, CGamepadMapping::Y, CGamepadMapping::B, CGamepadMapping::DpadLeft, + CGamepadMapping::LeftTrigger, CGamepadMapping::RightTrigger, CGamepadMapping::LeftThumb, + CGamepadMapping::RightThumb, CGamepadMapping::Back}, + }; + + static constexpr const char* k_configPath = "./alpha_ring_menu.cfg"; + + static MenuConfig load(); + + // Binds the given profile's buttons onto `mapping`'s Halo actions, clearing + // everything else. `profileIndex` is clamped to [0, k_profileCount). + void ApplyControllerProfile(int profileIndex, CGamepadMapping& mapping) const; + +private: + static void writeDefault(const std::string& path); + static WORD parseButton(const std::string& name); + static int parseKey(const std::string& name); + static CGamepadMapping::eButton parseGamepadButton(const std::string& name); + static std::string trim(const std::string& s); +}; + +extern MenuConfig g_menuConfig; diff --git a/src/mcc/CGameManager.cpp b/src/mcc/CGameManager.cpp index 1de3c20..3f41cdc 100644 --- a/src/mcc/CGameManager.cpp +++ b/src/mcc/CGameManager.cpp @@ -1,18 +1,53 @@ #include "CGameManager.h" #include "common.h" +#include "mcc/mcc.h" +#include "render/imgui/game/xbox/CXboxMenuState.h" +#include "render/imgui/game/xbox/CXboxColorMapping.h" +#include "input/MenuConfig.h" #include #include #include +#include +#include + +static constexpr const char* k_menuStateBinPath = "./MCC/Binaries/Win64/alpha_ring_menu.bin"; 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 +57,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 +83,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 +96,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) { @@ -73,17 +121,120 @@ void CGameManager::set_state(CGameManager *self, eState state) { auto state_name = "Unknown"; if (state == Exiting) state_name = "Exiting"; - LOG_INFO("Set Game State[{}]: {}", state, state_name); return ppOriginal.set_state(self, state); } +void CGameManager::apply_profiles() { + auto p_mng = GameManager(); + if (!MCC::IsInGame() || !p_mng) + return; + // Always use player 0's real XUID — fake XUIDs for players 1-3 are unknown to the original game functions + auto xuid = get_xuid(0); + if (!xuid) + return; + auto* base_profile = p_mng->ppOriginal.get_player_profile(p_mng, xuid); + auto* base_mapping = p_mng->ppOriginal.retrive_gamepad_mapping(p_mng, xuid); + if (!base_profile || !base_mapping) + return; + for (int i = 0; i < 4; ++i) { + auto profile = get_profile(i); + if (profile) { + memcpy(&profile->profile, base_profile, sizeof(CUserProfile)); + memcpy(&profile->mapping, base_mapping, sizeof(CGamepadMapping)); + } + } +} + +static void apply_menu_state_from_bin() { + MenuState ms{}; + ms.playerCount = 1; + ms.useKM = false; + for (int i = 0; i < 4; ++i) { ms.controllerIndex[i] = i; ms.teamIndex[i] = i % 2; } + loadMenuStateBin(ms, k_menuStateBinPath); + + for (int i = 0; i < 4; ++i) { + auto profile = CGameManager::get_profile(i); + if (profile) { + profile->controller_index = ms.controllerIndex[i]; + profile->profile.LookControlsInverted = ms.invert[i]; + profile->profile.MouseLookControlsInverted = ms.invert[i]; + profile->profile.VerticalLookSensitivity = static_cast(ms.sensitivity[i]); + profile->profile.HorizontalLookSensitivity = static_cast(ms.sensitivity[i]); + g_menuConfig.ApplyControllerProfile(ms.controllerProfile[i], profile->mapping); + } + } + + auto p_setting = AlphaRing::Global::MCC::Splitscreen(); + if (p_setting) { + p_setting->b_override = ms.playerCount > 1; + p_setting->player_count = ms.playerCount; + p_setting->b_player0_use_km = ms.useKM; + p_setting->b_use_player0_profile = (ms.playerCount <= 1); + } + + auto* engine = GameEngine(); + + for (int i = 0; i < 4; ++i) { + auto xuid = CGameManager::get_xuid(i); + if (xuid && engine) + engine->change_team(xuid, ms.teamIndex[i]); + } + + auto* gg = GameGlobal(); + int game = gg ? static_cast(gg->current_game) : static_cast(CGameGlobal::Halo3); + for (int i = 0; i < 4; ++i) { + auto profile = CGameManager::get_profile(i); + if (profile) { + int primary = CXboxColorMapping::GetColorIndex(ms.playerColors[i].colors[0], game); + int secondary = CXboxColorMapping::GetColorIndex(ms.playerColors[i].colors[1], game); + int tertiary = CXboxColorMapping::GetColorIndex(ms.playerColors[i].colors[2], game); + profile->profile.PlayerModelPrimaryColorIndex = primary; + profile->profile.PlayerModelSecondaryColorIndex = secondary; + profile->profile.PlayerModelTertiaryColorIndex = tertiary; + profile->profile.PlayerModelPrimaryColor = CXboxColorMapping::GetColorItemIndex(ms.playerColors[i].colors[0], game); + profile->profile.PlayerModelSecondaryColor = CXboxColorMapping::GetColorItemIndex(ms.playerColors[i].colors[1], game); + profile->profile.PlayerModelTertiaryColor = CXboxColorMapping::GetColorItemIndex(ms.playerColors[i].colors[2], game); + } + } + if (game == static_cast(CGameGlobal::Halo2)) { + std::thread([]() { + Sleep(10000); + auto* e = GameEngine(); + if (e) e->load_setting(); + }).detach(); + } else if (engine) { + engine->load_setting(); + } +} + +static bool ShouldThrottleApply() { + using clock = std::chrono::steady_clock; + static clock::time_point s_last_apply{}; + constexpr auto k_min_interval = std::chrono::milliseconds(250); + + auto now = clock::now(); + bool first_call = s_last_apply.time_since_epoch().count() == 0; + if (!first_call && (now - s_last_apply) < k_min_interval) + return true; + + s_last_apply = now; + return false; +} + void *CGameManager::game_restart(CGameManager *self, int type, const char *reason) { - auto final_reason = reason ? reason : "NoReason"; - LOG_INFO("Game Restart[{}]: {}", type, final_reason); - return ppOriginal.game_restart(self, type, reason); + auto result = ppOriginal.game_restart(self, type, reason); + if (!ShouldThrottleApply()) { + apply_profiles(); + apply_menu_state_from_bin(); + } + return result; } char __fastcall CGameManager::game_setup(CGameManager* self, void* a2) { -// LOG_INFO("game setup"); player init/add - return ppOriginal.game_setup(self, a2); + char result = ppOriginal.game_setup(self, a2); + if (!ShouldThrottleApply()) { + apply_profiles(); + apply_menu_state_from_bin(); + } + return result; } diff --git a/src/mcc/CGameManager.h b/src/mcc/CGameManager.h index 0b7cc24..6e01f79 100644 --- a/src/mcc/CGameManager.h +++ b/src/mcc/CGameManager.h @@ -34,6 +34,7 @@ static int get_index(__int64 xuid); static Profile_t* get_profile(int index); static CInputDevice* get_controller(int index); + static void apply_profiles(); private: static void __fastcall set_vibration(CGameManager* self, DWORD dwUserIndex, XINPUT_VIBRATION *pVibration); diff --git a/src/mcc/CGameManagerSplitscreen.cpp b/src/mcc/CGameManagerSplitscreen.cpp index 1912629..176df16 100644 --- a/src/mcc/CGameManagerSplitscreen.cpp +++ b/src/mcc/CGameManagerSplitscreen.cpp @@ -107,7 +107,41 @@ CUserProfile* CGameManager::get_player_profile(CGameManager *self, __int64 xid) if (p_setting->b_use_player0_profile) return &get_profile(0)->profile; - return &get_profile(get_index(xid))->profile; + auto* dest = get_profile(get_index(xid)); + + // Reach initializes armor into the player-0 profile after game_setup fires, so the + // snapshot taken by apply_profiles() is stale. Sync armor from the live player-0 + // profile on every call while preserving the per-player custom colors and look + // settings we applied. + auto* gg = GameGlobal(); + if (gg && gg->current_game == CGameGlobal::HaloReach) { + auto* base = ppOriginal.get_player_profile(self, get_xuid(0)); + if (base) { + int pc = dest->profile.PlayerModelPrimaryColorIndex; + int sc = dest->profile.PlayerModelSecondaryColorIndex; + int tc = dest->profile.PlayerModelTertiaryColorIndex; + int pci = dest->profile.PlayerModelPrimaryColor; + int sci = dest->profile.PlayerModelSecondaryColor; + int tci = dest->profile.PlayerModelTertiaryColor; + bool lookInverted = dest->profile.LookControlsInverted; + bool mouseLookInverted = dest->profile.MouseLookControlsInverted; + unsigned char vertSens = dest->profile.VerticalLookSensitivity; + unsigned char horizSens = dest->profile.HorizontalLookSensitivity; + memcpy(&dest->profile, base, sizeof(CUserProfile)); + dest->profile.PlayerModelPrimaryColorIndex = pc; + dest->profile.PlayerModelSecondaryColorIndex = sc; + dest->profile.PlayerModelTertiaryColorIndex = tc; + dest->profile.PlayerModelPrimaryColor = pci; + dest->profile.PlayerModelSecondaryColor = sci; + dest->profile.PlayerModelTertiaryColor = tci; + dest->profile.LookControlsInverted = lookInverted; + dest->profile.MouseLookControlsInverted = mouseLookInverted; + dest->profile.VerticalLookSensitivity = vertSens; + dest->profile.HorizontalLookSensitivity = horizSens; + } + } + + return &dest->profile; } CGamepadMapping* CGameManager::retrive_gamepad_mapping(CGameManager *self, __int64 xid) { diff --git a/src/mcc/CGamepadMapping.cpp b/src/mcc/CGamepadMapping.cpp index b428aed..6b7919a 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,189 @@ 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" + +// 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) { + 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()) { + } + + ImGui::SameLine(); + if (ImGui::Button("Delete") && selected_profile >= 0 && selected_profile < (int)profile_names.size()) { + 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; + } + } + + 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/CUserProfile.cpp b/src/mcc/CUserProfile.cpp index cb4ca37..1a4eb97 100644 --- a/src/mcc/CUserProfile.cpp +++ b/src/mcc/CUserProfile.cpp @@ -104,8 +104,18 @@ void CUserProfile::ImGuiContext() { } result |= ImGui::Checkbox("OnlineMedalFlasher", &OnlineMedalFlasher); - result |= ImGui::Checkbox("VerticalLookSensitivity", &VerticalLookSensitivity); - result |= ImGui::Checkbox("HorizontalLookSensitivity", &HorizontalLookSensitivity); + + int vertSens = VerticalLookSensitivity; + if (ImGui::InputInt("VerticalLookSensitivity", &vertSens) && vertSens >= 0 && vertSens <= 255) { + VerticalLookSensitivity = static_cast(vertSens); + result = true; + } + + int horizSens = HorizontalLookSensitivity; + if (ImGui::InputInt("HorizontalLookSensitivity", &horizSens) && horizSens >= 0 && horizSens <= 255) { + HorizontalLookSensitivity = static_cast(horizSens); + result = true; + } result |= ImGui::Checkbox("LookAcceleration", &LookAcceleration); result |= ImGui::InputFloat("LookAxialDeadZone", &LookAxialDeadZone); result |= ImGui::InputFloat("LookRadialDeadZone", &LookRadialDeadZone); diff --git a/src/mcc/CUserProfile.h b/src/mcc/CUserProfile.h index e54d47c..8bc694f 100644 --- a/src/mcc/CUserProfile.h +++ b/src/mcc/CUserProfile.h @@ -68,8 +68,8 @@ struct CUserProfile { struct Skin_t { int object; int skin; } Skins[32]; // 0xAC wchar_t ServiceTag[4]; // 0x1AC bool OnlineMedalFlasher; // 0x1B4 - bool VerticalLookSensitivity; // 0x1B5 - bool HorizontalLookSensitivity; // 0x1B6 + unsigned char VerticalLookSensitivity; // 0x1B5 + unsigned char HorizontalLookSensitivity; // 0x1B6 bool LookAcceleration; // 0x1B7 float LookAxialDeadZone; // 0x1B8 float LookRadialDeadZone; // 0x1BC diff --git a/src/mcc/splitscreen/Splitscreen.cpp b/src/mcc/splitscreen/Splitscreen.cpp index 54a0c6a..c47fc36 100644 --- a/src/mcc/splitscreen/Splitscreen.cpp +++ b/src/mcc/splitscreen/Splitscreen.cpp @@ -36,12 +36,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 +89,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(); @@ -107,22 +154,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(); 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..ee657e7 100644 --- a/src/render/imgui/ImGui.cpp +++ b/src/render/imgui/ImGui.cpp @@ -4,6 +4,9 @@ #include #include +#include +#include + #include "input/Input.h" #include "global/Global.h" #include "filesystem/Filesystem.h" @@ -11,6 +14,8 @@ #include "../D3d11/D3d11.h" #include "./game/mcc/CMCCContext.h" #include "./game/halo3/CHalo3Context.h" +#include "./game/xbox/CXboxContext.h" +#include "./game/xbox/assets/segoeui_ttf.h" #include "mcc/mcc.h" #include "mcc/CGameGlobal.h" @@ -55,12 +60,40 @@ namespace AlphaRing::Render::ImGui { io.Fonts->AddFontDefault(&config); } + // Xbox overlay font (Segoe UI at 25px, matching the original SDL2 prototype) + // Bundled with the mod so it doesn't depend on the OS having Segoe UI installed. + ImFont* xboxFont = nullptr; + static const char* segoe_path = "../../../alpha_ring/segoeui.ttf"; + if (!AlphaRing::Filesystem::Exist(segoe_path)) { + AlphaRing::Filesystem::Save(segoe_path, reinterpret_cast(segoeui_ttf), segoeui_ttf_len); + } + if (AlphaRing::Filesystem::Exist(segoe_path)) { + xboxFont = io.Fonts->AddFontFromFileTTF(segoe_path, 25.0f * scale); + } + if (!xboxFont) { + ImFontConfig cfg; + cfg.SizePixels = 25.0f * scale; + xboxFont = io.Fonts->AddFontDefault(&cfg); + } + ::ImGui::GetStyle().ScaleAllSizes(scale); + // SDL audio subsystem for Xbox menu sounds + SDL_Init(SDL_INIT_AUDIO); + Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048); + + g_pXboxContext = new CXboxContext(xboxFont); + return true; } void Render() { + AlphaRing::Input::Update(); + + bool xboxOpen = g_pXboxContext && g_pXboxContext->isOpen(); + if (!AlphaRing::Global::Global()->show_imgui && !xboxOpen) + return; + ImGui_ImplDX11_NewFrame(); ImGui_ImplWin32_NewFrame(); ::ImGui::NewFrame(); @@ -68,31 +101,31 @@ namespace AlphaRing::Render::ImGui { bool inGame = MCC::IsInGame(); auto pGameGlobal = GameGlobal(); - 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 (xboxOpen) + g_pXboxContext->render(); + if (AlphaRing::Global::Global()->show_imgui) { + g_pMCCContext->render(); - if (inGame && pGameGlobal != nullptr) { - if (pGameGlobal->current_game != 0) - { - auto context = pages[pGameGlobal->current_game]; - if (context != nullptr) - context->render(); + if (inGame && pGameGlobal != nullptr) { + // 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) + context->render(); + } } - } - if (::ImGui::BeginMainMenuBar()) { - if (inGame) - ::ImGui::Separator(); - ::ImGui::Text("%.1f fps", ::ImGui::GetIO().Framerate); - ::ImGui::EndMainMenuBar(); + if (::ImGui::BeginMainMenuBar()) { + if (inGame) + ::ImGui::Separator(); + ::ImGui::Text("%.1f fps", ::ImGui::GetIO().Framerate); + ::ImGui::EndMainMenuBar(); + } } ::ImGui::Render(); diff --git a/src/render/imgui/game/xbox/CStateMachine.cpp b/src/render/imgui/game/xbox/CStateMachine.cpp new file mode 100644 index 0000000..d59f2f9 --- /dev/null +++ b/src/render/imgui/game/xbox/CStateMachine.cpp @@ -0,0 +1,489 @@ +#pragma once + +#include "CXboxRender.hpp" +#include "CStateMachine.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +// ------------------------------------------------------------ +// Persistence (unchanged) +// ------------------------------------------------------------ +bool saveMenuStateBin(const MenuState& state, const std::string& path) { + std::ofstream ofs(path, std::ios::binary); + if (!ofs) return false; + + uint8_t useKM = state.useKM ? 1 : 0; + uint8_t invert[4]; + for (int i = 0; i < 4; ++i) invert[i] = state.invert[i] ? 1 : 0; + + ofs.write(reinterpret_cast(&state.playerCount), sizeof(state.playerCount)); + ofs.write(reinterpret_cast(&useKM), sizeof(useKM)); + ofs.write(reinterpret_cast(state.controllerIndex), sizeof(state.controllerIndex)); + ofs.write(reinterpret_cast(state.controllerProfile), sizeof(state.controllerProfile)); + ofs.write(reinterpret_cast(state.sensitivity), sizeof(state.sensitivity)); + ofs.write(reinterpret_cast(invert), sizeof(invert)); + ofs.write(reinterpret_cast(state.teamIndex), sizeof(state.teamIndex)); + ofs.write(reinterpret_cast(state.playerColors), sizeof(state.playerColors)); + + return ofs.good(); +} + +bool loadMenuStateBin(MenuState& state, const std::string& path) { + std::ifstream ifs(path, std::ios::binary); + if (!ifs) return false; + + uint8_t useKM = 0; + uint8_t invert[4] = {0, 0, 0, 0}; + + ifs.read(reinterpret_cast(&state.playerCount), sizeof(state.playerCount)); + ifs.read(reinterpret_cast(&useKM), sizeof(useKM)); + state.useKM = useKM != 0; + ifs.read(reinterpret_cast(state.controllerIndex), sizeof(state.controllerIndex)); + ifs.read(reinterpret_cast(state.controllerProfile), sizeof(state.controllerProfile)); + ifs.read(reinterpret_cast(state.sensitivity), sizeof(state.sensitivity)); + ifs.read(reinterpret_cast(invert), sizeof(invert)); + for (int i = 0; i < 4; ++i) state.invert[i] = invert[i] != 0; + ifs.read(reinterpret_cast(state.teamIndex), sizeof(state.teamIndex)); + ifs.read(reinterpret_cast(state.playerColors), sizeof(state.playerColors)); + + return ifs.good(); +} + +// ------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------ +namespace { + // The main option list groups Increment/Decrement/PointerDisplay options + // (e.g. Reduce/Display/Increase Sensitivity) into a single visual row, just + // like the render loop's row counter does. This computes the row that a + // given raw option index falls on so window-scroll math matches what's + // actually drawn. Landing on the 2nd/3rd item of such a group must report + // the same row as the 1st item, not a later one. + bool isRowGroupType(OptionType t) { + return t == OptionType::Increment || t == OptionType::Decrement || t == OptionType::PointerDisplay; + } + + int rowIndexForOption(const Page& page, int optionIndex) { + int row = 0; + bool prevWasGroup = false; + int limit = (std::min)(optionIndex, (int)page.options.size() - 1); + for (int i = 0; i <= limit; ++i) { + bool isGroup = isRowGroupType(page.options[i].type); + bool continuesRow = isGroup && prevWasGroup; + if (i > 0 && !continuesRow) row++; + prevWasGroup = isGroup; + } + return row; + } +} + +// ------------------------------------------------------------ +// Constructor +// ------------------------------------------------------------ +StateMachine::StateMachine(const Menu& menuInstance, const std::array& sounds) + : currentState{ + menuInstance, + sounds, + 0, + 0, + {0,1,2,3,4,5}, + 0, + {0,1,2,3,4,5}, + 0, + 0.25f, + Phase::Opening, + {1, false, {0,0,0,0}, {0,0,0,0}, {3,3,3,3}, {false,false,false,false}, {0,1,0,1}, {{0,0,0},{0,0,0},{0,0,0},{0,0,0}}} + }, + running(true) +{ +} + +// ------------------------------------------------------------ +// Input layer (REPLACES SDL_Event handler) +// ------------------------------------------------------------ +void StateMachine::handleInput(InputCommand cmd) +{ + if (cmd == InputCommand::Up && currentState.phase == Phase::InShiftUp) { + finishShiftUp(); + currentState.phase = Phase::InIdle; + currentState.time = 0; + handleUp(); + return; + } + + if (cmd == InputCommand::Down && currentState.phase == Phase::InShiftDown) { + finishShiftDown(); + currentState.phase = Phase::InIdle; + currentState.time = 0; + handleDown(); + return; + } + + if (cmd == InputCommand::Up && currentState.phase == Phase::ShiftUp) { + finishOptionShiftUp(); + currentState.phase = Phase::Idle; + currentState.time = 0; + handleUp(); + return; + } + + if (cmd == InputCommand::Down && currentState.phase == Phase::ShiftDown) { + finishOptionShiftDown(); + currentState.phase = Phase::Idle; + currentState.time = 0; + handleDown(); + return; + } + + if (!(currentState.phase == Phase::Idle || + currentState.phase == Phase::InIdle || + currentState.phase == Phase::PostOpening || + currentState.phase == Phase::PreClosing)) { + return; + } + + switch (cmd) { + case InputCommand::Left: handleLeft(); break; + case InputCommand::Right: handleRight(); break; + case InputCommand::Up: handleUp(); break; + case InputCommand::Down: handleDown(); break; + case InputCommand::Select: handleOption(); break; + case InputCommand::Back: handleClose(); break; + } +} + +// ------------------------------------------------------------ +// Navigation +// ------------------------------------------------------------ +void StateMachine::handleLeft() { + if (currentState.phase != Phase::Idle) return; + + auto& opt = currentState.menu.pages[currentState.pageIndex] + .options[currentState.optionIndex]; + + if (opt.type == OptionType::Increment) { + Mix_PlayChannel(-1, currentState.sounds[2], 0); + currentState.optionIndex = (std::max)(0, currentState.optionIndex - 2); + } else { + Mix_PlayChannel(-1, currentState.sounds[3], 0); + if (currentState.pageIndex > 0) { + currentState.phase = Phase::ShiftLeft; + currentState.time = 0; + } + } +} + +void StateMachine::handleRight() { + if (currentState.phase != Phase::Idle) return; + + auto& opt = currentState.menu.pages[currentState.pageIndex] + .options[currentState.optionIndex]; + + if (opt.type == OptionType::Decrement) { + Mix_PlayChannel(-1, currentState.sounds[3], 0); + currentState.optionIndex = (std::min)( + (int)currentState.menu.pages[currentState.pageIndex].options.size() - 1, + currentState.optionIndex + 2 + ); + } else { + if (currentState.pageIndex >= State().menu.MAX_PAGES - 1) + return; + + Mix_PlayChannel(-1, currentState.sounds[3], 0); + currentState.phase = Phase::ShiftRight; + currentState.time = 0; + } +} + +void StateMachine::handleUp() { + if (currentState.phase == Phase::Idle) { + Mix_PlayChannel(-1, currentState.sounds[2], 0); + auto& opt = currentState.menu.pages[currentState.pageIndex] + .options[currentState.optionIndex]; + + if (opt.type == OptionType::Increment) { + currentState.optionIndex = (std::min)( + (int)currentState.menu.pages[currentState.pageIndex].options.size() - 1, + currentState.optionIndex - 3 + ); + } else { + currentState.optionIndex = (std::max)(0, currentState.optionIndex - 1); + } + + int row = rowIndexForOption(currentState.menu.pages[currentState.pageIndex], currentState.optionIndex); + if (row < currentState.optionWindow[0]) { + currentState.phase = Phase::ShiftUp; + currentState.time = 0; + } + } + + if (currentState.phase == Phase::InIdle) { + Mix_PlayChannel(-1, currentState.sounds[2], 0); + currentState.subOptionIndex = (std::max)(0, currentState.subOptionIndex - 1); + + if (currentState.subOptionIndex < currentState.subOptionWindow[0]) { + Mix_PlayChannel(-1, currentState.sounds[2], 0); + currentState.phase = Phase::InShiftUp; + currentState.time = 0; + } + } +} + +void StateMachine::handleDown() { + if (currentState.phase == Phase::Idle) { + Mix_PlayChannel(-1, currentState.sounds[2], 0); + + int maxOption = (int)currentState.menu.pages[currentState.pageIndex].options.size(); + + auto& opt = currentState.menu.pages[currentState.pageIndex] + .options[currentState.optionIndex]; + + if (opt.type == OptionType::Decrement) { + currentState.optionIndex = (std::min)(maxOption - 1, currentState.optionIndex + 3); + } else { + currentState.optionIndex = (std::min)(maxOption - 1, currentState.optionIndex + 1); + } + + int row = rowIndexForOption(currentState.menu.pages[currentState.pageIndex], currentState.optionIndex); + int maxWin = (int)currentState.optionWindow.size(); + if (row > currentState.optionWindow[maxWin - 1]) { + currentState.phase = Phase::ShiftDown; + currentState.time = 0; + } + } + + if (currentState.phase == Phase::InIdle) { + Mix_PlayChannel(-1, currentState.sounds[2], 0); + + int maxSub = (int)currentState.menu.pages[currentState.pageIndex] + .options[currentState.optionIndex] + .subOptions.size(); + + currentState.subOptionIndex = (std::min)(maxSub - 1, currentState.subOptionIndex + 1); + + int maxWin = (int)currentState.subOptionWindow.size(); + if (currentState.subOptionIndex > currentState.subOptionWindow[maxWin - 1]) { + currentState.phase = Phase::InShiftDown; + currentState.time = 0; + } + } +} + +// ------------------------------------------------------------ +// Selection logic (audio replaced) +// ------------------------------------------------------------ +void StateMachine::handleSubOption() { + auto& sub = currentState.menu.pages[currentState.pageIndex] + .options[currentState.optionIndex] + .subOptions[currentState.subOptionIndex]; + + Mix_PlayChannel(-1, currentState.sounds[4], 0); + + switch (sub.subOptionType) { + case 0: + currentState.menuState.controllerIndex[currentState.pageIndex - 1] = currentState.subOptionIndex; + break; + case 1: + currentState.menuState.playerColors[currentState.pageIndex - 1].colors[0] = currentState.subOptionIndex; + break; + case 2: + currentState.menuState.playerColors[currentState.pageIndex - 1].colors[1] = currentState.subOptionIndex; + break; + case 3: + currentState.menuState.playerColors[currentState.pageIndex - 1].colors[2] = currentState.subOptionIndex; + break; + case 4: + currentState.menuState.controllerProfile[currentState.pageIndex - 1] = currentState.subOptionIndex; + break; + default: + break; + } +} + +void StateMachine::handleOption() { + auto& opt = currentState.menu.pages[currentState.pageIndex] + .options[currentState.optionIndex]; + + Mix_PlayChannel(-1, currentState.sounds[4], 0); + + if (currentState.pageIndex == 0) { + if (opt.type == OptionType::Increment && currentState.menuState.playerCount < 4) + currentState.menuState.playerCount++; + + if (opt.type == OptionType::Decrement && currentState.menuState.playerCount > 1) + currentState.menuState.playerCount--; + + if (opt.type == OptionType::Boolean) + currentState.menuState.useKM = !currentState.menuState.useKM; + } else { + int player = currentState.pageIndex - 1; + + if (opt.type == OptionType::Increment && currentState.menuState.sensitivity[player] < 10) + currentState.menuState.sensitivity[player]++; + + if (opt.type == OptionType::Decrement && currentState.menuState.sensitivity[player] > 1) + currentState.menuState.sensitivity[player]--; + + if (opt.type == OptionType::Boolean) + currentState.menuState.invert[player] = !currentState.menuState.invert[player]; + } + + if (opt.type == OptionType::TeamToggle) { + auto& team = currentState.menuState.teamIndex[currentState.pageIndex - 1]; + team = !team; + } + + if (currentState.phase == Phase::InIdle) { + handleSubOption(); + currentState.phase = Phase::ShiftOut; + currentState.time = 0; + } + + if (opt.type == OptionType::Subpage && currentState.phase == Phase::Idle) { + currentState.subOptionWindow = {0,1,2,3,4,5}; + currentState.phase = Phase::ShiftIn; + currentState.time = 0; + } +} + +void StateMachine::handleClose() { + Mix_PlayChannel(-1, currentState.sounds[1], 0); + currentState.phase = Phase::Closing; + currentState.time = 0; +} + +void StateMachine::finishShiftUp() { + currentState.subOptionWindow.pop_back(); + currentState.subOptionWindow.push_front(currentState.subOptionWindow.front() - 1); +} + +void StateMachine::finishShiftDown() { + currentState.subOptionWindow.pop_front(); + currentState.subOptionWindow.push_back(currentState.subOptionWindow.back() + 1); +} + +void StateMachine::finishOptionShiftUp() { + currentState.optionWindow.pop_back(); + currentState.optionWindow.push_front(currentState.optionWindow.front() - 1); +} + +void StateMachine::finishOptionShiftDown() { + currentState.optionWindow.pop_front(); + currentState.optionWindow.push_back(currentState.optionWindow.back() + 1); +} + +// ------------------------------------------------------------ +// UPDATE (unchanged logic) +// ------------------------------------------------------------ +void StateMachine::update(float dt) +{ + if (currentState.time < currentState.duration) + currentState.time += dt; + + if (currentState.time >= currentState.duration) { + + switch (currentState.phase) { + + case Phase::Opening: + currentState.phase = Phase::Idle; + currentState.time = 0; + break; + + case Phase::Closing: + running = false; + break; + + case Phase::ShiftRight: { + currentState.optionIndex = 0; + currentState.phase = Phase::FadeIn; + int maxPage = State().menu.MAX_PAGES; + currentState.pageIndex = (std::min)(maxPage - 1, currentState.pageIndex + 1); + break; + } + + case Phase::ShiftLeft: + currentState.optionIndex = 0; + currentState.phase = Phase::FadeIn; + currentState.pageIndex = (std::max)(0, currentState.pageIndex - 1); + break; + + case Phase::InShiftUp: + finishShiftUp(); + currentState.phase = Phase::InFadeIn; + break; + + case Phase::InShiftDown: + finishShiftDown(); + currentState.phase = Phase::InFadeIn; + break; + + case Phase::ShiftUp: + finishOptionShiftUp(); + currentState.phase = Phase::Idle; + currentState.time = 0; + break; + + case Phase::ShiftDown: + finishOptionShiftDown(); + currentState.phase = Phase::Idle; + currentState.time = 0; + break; + + case Phase::FadeIn: + currentState.optionIndex = 0; + currentState.optionWindow = {0,1,2,3,4,5}; + currentState.phase = Phase::Idle; + currentState.time = 0; + break; + + case Phase::InFadeIn: + currentState.phase = Phase::InIdle; + currentState.time = 0; + break; + + case Phase::ShiftIn: + currentState.subOptionIndex = 0; + currentState.phase = Phase::InFadeIn; + break; + + case Phase::ShiftOut: + currentState.phase = Phase::FadeIn; + break; + + default: + break; + } + } +} + +// ------------------------------------------------------------ +// RENDER (SDL REMOVED → ImGui backend expected elsewhere) +// ------------------------------------------------------------ +void StateMachine::render(int WINDOW_WIDTH, int WINDOW_HEIGHT, ImFont* font) +{ + // No SDL. Rendering should be moved into: + // - ImGui widgets OR + // - custom ImDrawList rendering layer + + // Example placeholder hook: + // renderMenuImGui(drawList, currentState); + renderMenu(WINDOW_WIDTH, WINDOW_HEIGHT, currentState, font); +} + +// ------------------------------------------------------------ +// Accessors +// ------------------------------------------------------------ +bool StateMachine::isRunning() const { + return running; +} + +State& StateMachine::getState() { + return currentState; +} \ No newline at end of file diff --git a/src/render/imgui/game/xbox/CStateMachine.h b/src/render/imgui/game/xbox/CStateMachine.h new file mode 100644 index 0000000..439bd89 --- /dev/null +++ b/src/render/imgui/game/xbox/CStateMachine.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include "CXboxMenu.hpp" +#include "CXboxMenuState.h" + +#include +#include +#include + +// ------------------------- +// Input abstraction (replaces SDL_Event) +// ------------------------- +enum class InputCommand { + Left, + Right, + Up, + Down, + Select, + Back +}; + +// ------------------------- +// State definitions +// ------------------------- +enum class Phase { + Opening, + PostOpening, + FadeIn, + InFadeIn, + Idle, + InIdle, + ShiftIn, + ShiftOut, + ShiftRight, + ShiftLeft, + ShiftUp, + ShiftDown, + InShiftUp, + InShiftDown, + PreClosing, + Closing +}; + +// ------------------------- +// Core runtime state +// ------------------------- +struct State { + Menu menu; + std::array sounds; + int pageIndex; + int optionIndex; + std::deque optionWindow; + int subOptionIndex; + std::deque subOptionWindow; + float time; + float duration; + Phase phase; + MenuState menuState; +}; + +// ------------------------- +// Persistence +// ------------------------- +bool saveMenuStateBin(const MenuState& state, const std::string& path); +bool loadMenuStateBin(MenuState& state, const std::string& path); + +// ------------------------- +// State machine (ImGui-driven) +// ------------------------- +class StateMachine { +public: + StateMachine(const Menu& menu, const std::array& sounds); + + void handleLeft(); + void handleRight(); + void handleUp(); + void handleDown(); + void handleOption(); + void handleSubOption(); + void handleClose(); + + void handleInput(InputCommand cmd); + void update(float dt); + void render(int WINDOW_WIDTH, int WINDOW_HEIGHT, ImFont* font); + + bool isRunning() const; + State& getState(); + +private: + void finishShiftUp(); + void finishShiftDown(); + void finishOptionShiftUp(); + void finishOptionShiftDown(); + + State currentState; + bool running; +}; \ No newline at end of file diff --git a/src/render/imgui/game/xbox/CXboxColorMapping.cpp b/src/render/imgui/game/xbox/CXboxColorMapping.cpp new file mode 100644 index 0000000..614b888 --- /dev/null +++ b/src/render/imgui/game/xbox/CXboxColorMapping.cpp @@ -0,0 +1,171 @@ +#include "CXboxColorMapping.h" + +#include +#include + +namespace CXboxColorMapping { + +// [colorIndex][column] — columns: {H4, HR, H3, ODST, H2, H2A, H1} +static const char* const k_table[kColorCount][7] = { + // 0 Steel + // {"H4_Color0_Steel", "HR_Color_Steel", "H3_COLOR0_STEEL", "ODST_COLOR_BLACK", "H2_Color_2", "H2A_Color_2", "H1_Color_2"}, + {"H1_Color_2", "H2_Color_2", "H3_COLOR0_STEEL", "H4_Color0_Steel", "H2A_Color_2", "ODST_COLOR_BLACK", "HR_Color_Steel"}, + // 1 Silver + // {"H4_Color1_Silver", "HR_Color_Silver", "H3_COLOR1_SILVER", "ODST_COLOR_GRAY", "H2_Color_2", "H2A_Color_1", "H1_Color_5"}, + {"H1_Color_5", "H2_Color_2", "H3_COLOR1_SILVER", "H4_Color1_Silver", "H2A_Color_1", "ODST_COLOR_GRAY", "HR_Color_Silver"}, + // 2 White + // {"H4_Color2_White", "HR_Color_White", "H3_COLOR2_WHITE", "ODST_COLOR_SNOW", "H2_Color_1", "H2A_Color_3", "H1_Color_1"}, + {"H1_Color_1", "H2_Color_1", "H3_COLOR2_WHITE", "H4_Color2_White", "H2A_Color_3", "ODST_COLOR_SNOW", "HR_Color_White"}, + // 3 Brown + // {"H4_Color3_Brown", "HR_Color_Brown", "H3_COLOR27_BROWN", "ODST_COLOR_BROWN", "H2_Color_17", "H2A_Color_4", "H1_Color_15"}, + {"H1_Color_15", "H2_Color_17", "H3_COLOR27_BROWN", "H4_Color3_Brown", "H2A_Color_4", "ODST_COLOR_BROWN", "HR_Color_Brown"}, + // 4 Tan + // {"H4_Color4_Tan", "HR_Color_Tan", "H3_COLOR28_TAN", "ODST_COLOR_TAN", "H2_Color_18", "H2A_Color_5", "H1_Color_16"}, + {"H1_Color_16", "H2_Color_18", "H3_COLOR28_TAN", "H4_Color4_Tan", "H2A_Color_5", "ODST_COLOR_TAN", "HR_Color_Tan"}, + // 5 Khaki + // {"H4_Color5_Khaki", "HR_Color_Khaki", "H3_COLOR29_KHAKI", "ODST_COLOR_KHAKI", "H2_Color_18", "H2A_Color_6", "H1_Color_16"}, + {"H1_Color_16", "H2_Color_18", "H3_COLOR29_KHAKI", "H4_Color5_Khaki", "H2A_Color_6", "ODST_COLOR_KHAKI", "HR_Color_Khaki"}, + // 6 Sage + // {"H4_Color6_Sage", "HR_Color_Sage", "H3_COLOR12_SAGE", "ODST_COLOR_SAGE", "H2_Color_8", "H2A_Color_7", "H1_Color_14"}, + {"H1_Color_14", "H2_Color_8", "H3_COLOR12_SAGE", "H4_Color6_Sage", "H2A_Color_7", "ODST_COLOR_SAGE", "HR_Color_Sage"}, + // 7 Olive + // {"H4_Color7_Olive", "HR_Color_Olive", "H3_COLOR14_OLIVE", "ODST_COLOR_OLIVE", "H2_Color_6", "H2A_Color_8", "H1_Color_14"}, + {"H1_Color_14", "H2_Color_6", "H3_COLOR14_OLIVE", "H4_Color7_Olive", "H2A_Color_8", "ODST_COLOR_OLIVE", "HR_Color_Olive"}, + // 8 Drab + // {"H4_Color8_Drab", "HR_Color_Drab", "H3_COLOR13_GREEN", "ODST_COLOR_DRAB", "H2_Color_7", "H2A_Color_9", "H1_Color_7"}, + {"H1_Color_7", "H2_Color_7", "H3_COLOR13_GREEN", "H4_Color8_Drab", "H2A_Color_9", "ODST_COLOR_DRAB", "HR_Color_Drab"}, + // 9 Forest + // {"H4_Color9_Forest", "HR_Color_Forest", "H3_COLOR12_SAGE", "ODST_COLOR_FOREST", "H2_Color_8", "H2A_Color_10", "H1_Color_14"}, + {"H1_Color_14", "H2_Color_8", "H3_COLOR12_SAGE", "H4_Color9_Forest", "H2A_Color_10", "ODST_COLOR_FOREST", "HR_Color_Forest"}, + // 10 Green + // {"H4_Color10_Green", "HR_Color_Green", "H3_COLOR13_GREEN", "ODST_COLOR_GREEN", "H2_Color_7", "H2A_Color_11", "H1_Color_14"}, + {"H1_Color_14", "H2_Color_7", "H3_COLOR13_GREEN", "H4_Color10_Green", "H2A_Color_11", "ODST_COLOR_GREEN", "HR_Color_Green"}, + // 11 Sea Foam + // {"H4_Color11_SeaFoam", "HR_Color_SeaFoam", "H3_COLOR15_TEAL", "ODST_COLOR_SEA_FOAM", "H2_Color_10", "H2A_Color_12", "H1_Color_13"}, + {"H1_Color_13", "H2_Color_10", "H3_COLOR15_TEAL", "H4_Color11_SeaFoam", "H2A_Color_12", "ODST_COLOR_SEA_FOAM", "HR_Color_SeaFoam"}, + // 12 Teal + // {"H4_Color12_Teal", "HR_Color_Teal", "H3_COLOR15_TEAL", "ODST_COLOR_TEAL", "H2_Color_10", "H2A_Color_13", "H1_Color_13"}, + {"H1_Color_13", "H2_Color_10", "H3_COLOR15_TEAL", "H4_Color12_Teal", "H2A_Color_13", "ODST_COLOR_TEAL", "HR_Color_Teal"}, + // 13 Aqua + // {"H4_Color13_Aqua", "HR_Color_Aqua", "H3_COLOR16_AQUA", "ODST_COLOR_AQUA", "H2_Color_9", "H2A_Color_14", "H1_Color_10"}, + {"H1_Color_10", "H2_Color_9", "H3_COLOR16_AQUA", "H4_Color13_Aqua", "H2A_Color_14", "ODST_COLOR_AQUA", "HR_Color_Aqua"}, + // 14 Cyan + // {"H4_Color14_Cyan", "HR_Color_Cyan", "H3_COLOR17_CYAN", "ODST_COLOR_CYAN", "H2_Color_9", "H2A_Color_15", "H1_Color_10"}, + {"H1_Color_10", "H2_Color_9", "H3_COLOR17_CYAN", "H4_Color14_Cyan", "H2A_Color_15", "ODST_COLOR_CYAN", "HR_Color_Cyan"}, + // 15 Blue + // {"H4_Color15_Blue", "HR_Color_Blue", "H3_COLOR18_BLUE", "ODST_COLOR_BLUE", "H2_Color_12", "H2A_Color_16", "H1_Color_4"}, + {"H1_Color_4", "H2_Color_12", "H3_COLOR18_BLUE", "H4_Color15_Blue", "H2A_Color_16", "ODST_COLOR_BLUE", "HR_Color_Blue"}, + // 16 Cobalt + {"H4_Color16_Cobalt", "HR_Color_Cobalt", "H3_COLOR19_COBALT", "ODST_COLOR_COBALT", "H2_Color_11", "H2A_Color_17", "H1_Color_11"}, + // 17 Ice + // {"H4_Color17_Ice", "HR_Color_Ice", "H3_COLOR20_SAPHIRE", "ODST_COLOR_ICE", "H2_Color_11", "H2A_Color_18", "H1_Color_11"}, + {"H1_Color_11", "H2_Color_11", "H3_COLOR20_SAPHIRE", "H4_Color17_Ice", "H2A_Color_18", "ODST_COLOR_ICE", "HR_Color_Ice"}, + // 18 Violet + // {"H4_Color18_Violet", "HR_Color_Violet", "H3_COLOR21_VIOLET", "ODST_COLOR_VIOLET", "H2_Color_13", "H2A_Color_19", "H1_Color_9"}, + {"H1_Color_9", "H2_Color_13", "H3_COLOR21_VIOLET", "H4_Color18_Violet", "H2A_Color_19", "ODST_COLOR_VIOLET", "HR_Color_Violet"}, + // 19 Orchid + // {"H4_Color19_Orchid", "HR_Color_Orchid", "H3_COLOR22_ORCHID", "ODST_COLOR_PINK", "H2_Color_15", "H2A_Color_20", "H1_Color_8"}, + {"H1_Color_8", "H2_Color_15", "H3_COLOR22_ORCHID", "H4_Color19_Orchid", "H2A_Color_20", "ODST_COLOR_PINK", "HR_Color_Orchid"}, + // 20 Lavender + // {"H4_Color20_Lavender", "HR_Color_Lavender", "H3_COLOR23_LAVENDER", "ODST_COLOR_LAVENDER", "H2_Color_14", "H2A_Color_21", "H1_Color_9"}, + {"H1_Color_9", "H2_Color_14", "H3_COLOR23_LAVENDER", "H4_Color20_Lavender", "H2A_Color_21", "ODST_COLOR_LAVENDER", "HR_Color_Lavender"}, + // 21 Maroon + // {"H4_Color21_Maroon", "HR_Color_Maroon", "H3_COLOR3_RED", "ODST_COLOR_MAROON", "H2_Color_16", "H2A_Color_22", "H1_Color_17"}, + {"H1_Color_17", "H2_Color_16", "H3_COLOR3_RED", "H4_Color21_Maroon", "H2A_Color_22", "ODST_COLOR_MAROON", "HR_Color_Maroon"}, + // 22 Brick + // {"H4_Color22_Brick", "HR_Color_Brick", "H3_COLOR3_RED", "ODST_COLOR_BRICK", "H2_Color_3", "H2A_Color_23", "H1_Color_3"}, + {"H1_Color_3", "H2_Color_3", "H3_COLOR3_RED", "H4_Color22_Brick", "H2A_Color_23", "ODST_COLOR_BRICK", "HR_Color_Brick"}, + // 23 Rose + // {"H4_Color23_Rose", "HR_Color_Rose", "H3_COLOR5_SALMON", "ODST_COLOR_ROSE", "H2_Color_15", "H2A_Color_24", "H1_Color_8"}, + {"H1_Color_8", "H2_Color_15", "H3_COLOR5_SALMON", "H4_Color23_Rose", "H2A_Color_24", "ODST_COLOR_ROSE", "HR_Color_Rose"}, + // 24 Rust + // {"H4_Color24_Rust", "HR_Color_Rust", "H3_COLOR6_ORANGE", "ODST_COLOR_COCOA", "H2_Color_4", "H2A_Color_25", "H1_Color_12"}, + {"H1_Color_12", "H2_Color_4", "H3_COLOR6_ORANGE", "H4_Color24_Rust", "H2A_Color_25", "ODST_COLOR_COCOA", "HR_Color_Rust"}, + // 25 Coral + // {"H4_Color25_Coral", "HR_Color_Coral", "H3_COLOR7_CORAL", "ODST_COLOR_CORAL", "H2_Color_4", "H2A_Color_26", "H1_Color_12"}, + {"H1_Color_12", "H2_Color_4", "H3_COLOR7_CORAL", "H4_Color25_Coral", "H2A_Color_26", "ODST_COLOR_CORAL", "HR_Color_Coral"}, + // 26 Peach + // {"H4_Color26_Peach", "HR_Color_Peach", "H3_COLOR8_PEACH", "ODST_COLOR_ROSE", "H2_Color_15", "H2A_Color_27", "H1_Color_18"}, + {"H1_Color_18", "H2_Color_15", "H3_COLOR8_PEACH", "H4_Color26_Peach", "H2A_Color_27", "ODST_COLOR_ROSE", "HR_Color_Peach"}, + // 27 Gold + // {"H4_Color27_Gold", "HR_Color_Gold", "H3_COLOR9_GOLD", "ODST_COLOR_GOLD", "H2_Color_5", "H2A_Color_28", "H1_Color_16"}, + {"H1_Color_16", "H2_Color_5", "H3_COLOR9_GOLD", "H4_Color27_Gold", "H2A_Color_28", "ODST_COLOR_GOLD", "HR_Color_Gold"}, + // 28 Yellow + // {"H4_Color28_Yellow", "HR_Color_Yellow", "H3_COLOR10_YELLOW", "ODST_COLOR_SAND", "H2_Color_5", "H2A_Color_29", "H1_Color_6"}, + {"H1_Color_6", "H2_Color_5", "H3_COLOR10_YELLOW", "H4_Color28_Yellow", "H2A_Color_29", "ODST_COLOR_SAND", "HR_Color_Yellow"}, + // 29 Pale + // {"H4_Color28_Yellow", "HR_Color_Pale", "H3_COLOR11_PALE", "ODST_COLOR_DESERT", "H2_Color_5", "H2A_Color_29", "H1_Color_6"}, + {"H1_Color_6", "H2_Color_5", "H3_COLOR11_PALE", "H4_Color28_Yellow", "H2A_Color_29", "ODST_COLOR_DESERT", "HR_Color_Pale"}, +}; + +// Maps CGameGlobal::eGame int values to table columns. +// Halo1=0, Halo2=1, Halo3=2, Halo4=3, GroundHog=4, Halo3ODST=5, HaloReach=6 +// static int GameToColumn(int game) { +// switch (game) { +// case 3: return kColHalo4; +// case 6: return kColReach; +// case 2: return kColHalo3; +// case 5: return kColODST; +// case 1: return kColHalo2; +// case 0: return kColHalo1; +// case 4: return kColHalo3; // GroundHog → H3 fallback +// default: return kColHalo3; +// } +// } + +// customization_item base index per eGame value. +// For H1/H2/H2A the colors are 1-indexed in both the string and the array, so +// the formula is base + embedded_number (same as base - 1 + 1-based index). +// For H3/H4 the colors are 0-indexed, so the formula is also base + embedded_number. +// For HR/ODST there is no embedded number; HR colors are in colorIndex order so +// base + colorIndex works. ODST colors are out of order and need a separate table. +static const int kGameColorItemBase[7] = { + 283, // Halo1 H1_Color_1 @ ci 284, colors 1-indexed + 488, // Halo2 H2_Color_1 @ ci 489 + 1233, // Halo3 H3_COLOR0 @ ci 1233 + 2025, // Halo4 H4_Color0 @ ci 2025 + 548, // GroundHog → H2A_Color_1 @ ci 549 + -1, // Halo3ODST → use kODSTColorItems + 202, // HaloReach HR_Color_Steel @ ci 202 +}; + +// customization_item indices for ODST colors by colorIndex (0-29). +// ODST entries are not contiguous in colorIndex order inside the array. +static const int kODSTColorItems[kColorCount] = { + 1588, 1589, 1590, 1594, 1597, 1598, // Steel, Silver, White, Brown, Tan, Khaki + 1603, 1604, 1605, 1606, 1607, 1608, // Sage, Olive, Drab, Forest, Green, SeaFoam + 1609, 1610, 1611, 1612, 1613, 1614, // Teal, Aqua, Cyan, Blue, Cobalt, Ice + 1615, 1617, 1616, 1591, 1592, 1593, // Violet, Orchid, Lavender, Maroon, Brick, Rose + 1596, 1600, 1593, 1601, 1602, 1599, // Rust, Coral, Peach, Gold, Yellow, Pale +}; + +const char* GetColorString(int colorIndex, int game) { + if (colorIndex < 0 || colorIndex >= kColorCount || game < 0 || game > 6) + return nullptr; + return k_table[colorIndex][game]; +} + +int GetColorIndex(int colorIndex, int game) { + const char* s = GetColorString(colorIndex, game); + if (!s) + return colorIndex; + const char* last = nullptr; + for (const char* p = s; *p; ++p) { + if (isdigit((unsigned char)*p) && (p == s || !isdigit((unsigned char)p[-1]))) + last = p; + } + if (last) + return atoi(last); + return colorIndex; +} + +int GetColorItemIndex(int colorIndex, int game) { + if (colorIndex < 0 || colorIndex >= kColorCount || game < 0 || game > 6) + return colorIndex; + if (game == 5) + return kODSTColorItems[colorIndex]; + int primary = GetColorIndex(colorIndex, game); + return kGameColorItemBase[game] + primary; +} + +} // namespace CXboxColorMapping diff --git a/src/render/imgui/game/xbox/CXboxColorMapping.h b/src/render/imgui/game/xbox/CXboxColorMapping.h new file mode 100644 index 0000000..1c46e5e --- /dev/null +++ b/src/render/imgui/game/xbox/CXboxColorMapping.h @@ -0,0 +1,26 @@ +#pragma once + +namespace CXboxColorMapping { + static constexpr int kColHalo4 = 0; + static constexpr int kColReach = 1; + static constexpr int kColHalo3 = 2; + static constexpr int kColODST = 3; + static constexpr int kColHalo2 = 4; + static constexpr int kColHalo2A = 5; + static constexpr int kColHalo1 = 6; + static constexpr int kColorCount = 30; + + // Returns the game-specific color identifier string (e.g. "H3_COLOR0_STEEL"). + // colorIndex: 0 to kColorCount-1 (AlphaRing row index from MenuState) + // game: CGameGlobal::eGame int value + const char* GetColorString(int colorIndex, int game); + + // Returns the game-specific numeric color index for PlayerModelPrimary/Secondary/TertiaryColorIndex. + // Parses the embedded number from the string (e.g. 12 from "H3_COLOR12_SAGE"). + // Falls back to colorIndex for Reach and ODST which have no embedded number. + int GetColorIndex(int colorIndex, int game); + + // Returns the customization_item array index for PlayerModelPrimary/Secondary/TertiaryColor. + // Each game stores its color entries at a different offset in the shared array. + int GetColorItemIndex(int colorIndex, int game); +} diff --git a/src/render/imgui/game/xbox/CXboxContext.cpp b/src/render/imgui/game/xbox/CXboxContext.cpp new file mode 100644 index 0000000..bc67402 --- /dev/null +++ b/src/render/imgui/game/xbox/CXboxContext.cpp @@ -0,0 +1,131 @@ +#include "CXboxContext.h" +#include "CXboxColorMapping.h" + +#include "assets/open_wav.h" +#include "assets/close_wav.h" +#include "assets/nav_wav.h" +#include "assets/nav_page_wav.h" +#include "assets/select_wav.h" +#include "assets/denied_wav.h" + +#include "mcc/CGameManager.h" +#include "global/Global.h" +#include "input/MenuConfig.h" + +#include +#include + +CXboxContext* g_pXboxContext = nullptr; + +CXboxContext::CXboxContext(ImFont* font) + : m_sounds{ + Mix_LoadWAV_RW(SDL_RWFromConstMem(open_wav, open_wav_len), 1), + Mix_LoadWAV_RW(SDL_RWFromConstMem(close_wav, close_wav_len), 1), + Mix_LoadWAV_RW(SDL_RWFromConstMem(nav_wav, nav_wav_len), 1), + Mix_LoadWAV_RW(SDL_RWFromConstMem(nav_page_wav, nav_page_wav_len), 1), + Mix_LoadWAV_RW(SDL_RWFromConstMem(select_wav, select_wav_len), 1), + Mix_LoadWAV_RW(SDL_RWFromConstMem(denied_wav, denied_wav_len), 1), + }, + m_font(font) +{ +} + +CXboxContext::~CXboxContext() { + if (m_stateMachine.has_value()) + saveMenuStateBin(m_stateMachine->getState().menuState, k_savePath); + for (auto* chunk : m_sounds) + Mix_FreeChunk(chunk); +} + +void CXboxContext::open() { + m_stateMachine.emplace(m_menu, m_sounds); + loadMenuStateBin(m_stateMachine->getState().menuState, k_savePath); + Mix_PlayChannel(-1, m_sounds[0], 0); + m_lastTick = SDL_GetTicks(); +} + +void CXboxContext::close() { + if (m_stateMachine.has_value()) + m_stateMachine->handleInput(InputCommand::Back); +} + +bool CXboxContext::isOpen() const { + return m_stateMachine.has_value(); +} + +void CXboxContext::handleInput(InputCommand cmd) { + if (m_stateMachine.has_value()) + m_stateMachine->handleInput(cmd); +} + +void CXboxContext::render() { + if (!m_stateMachine.has_value()) return; + + Uint32 now = SDL_GetTicks(); + float dt = (now - m_lastTick) / 1000.0f; + m_lastTick = now; + + m_stateMachine->update(dt); + + auto* vp = ImGui::GetMainViewport(); + m_stateMachine->render((int)vp->Size.x, (int)vp->Size.y, m_font); + + if (!m_stateMachine->isRunning()) { + const MenuState& ms = m_stateMachine->getState().menuState; + saveMenuStateBin(ms, k_savePath); + + CGameManager::apply_profiles(); + + // apply controller indices and team assignments from UI state to each player profile + auto* engine = GameEngine(); + for (int i = 0; i < 4; ++i) { + auto profile = CGameManager::get_profile(i); + if (profile) { + profile->controller_index = ms.controllerIndex[i]; + profile->profile.LookControlsInverted = ms.invert[i]; + profile->profile.MouseLookControlsInverted = ms.invert[i]; + profile->profile.VerticalLookSensitivity = static_cast(ms.sensitivity[i]); + profile->profile.HorizontalLookSensitivity = static_cast(ms.sensitivity[i]); + } + + auto xuid = CGameManager::get_xuid(i); + if (xuid && engine) + engine->change_team(xuid, ms.teamIndex[i]); + + if (profile) + g_menuConfig.ApplyControllerProfile(ms.controllerProfile[i], profile->mapping); + } + + // propagate splitscreen settings from UI state + auto p_setting = AlphaRing::Global::MCC::Splitscreen(); + if (p_setting) { + if (ms.playerCount > 1) + p_setting->b_override = true; + p_setting->player_count = ms.playerCount; + p_setting->b_player0_use_km = ms.useKM; + p_setting->b_use_player0_profile = (ms.playerCount <= 1); + } + + // apply player colors for the current game + auto* gg = GameGlobal(); + int game = gg ? static_cast(gg->current_game) : static_cast(CGameGlobal::Halo3); + for (int i = 0; i < 4; ++i) { + auto profile = CGameManager::get_profile(i); + if (profile) { + int primary = CXboxColorMapping::GetColorIndex(ms.playerColors[i].colors[0], game); + int secondary = CXboxColorMapping::GetColorIndex(ms.playerColors[i].colors[1], game); + int tertiary = CXboxColorMapping::GetColorIndex(ms.playerColors[i].colors[2], game); + profile->profile.PlayerModelPrimaryColorIndex = primary; + profile->profile.PlayerModelSecondaryColorIndex = secondary; + profile->profile.PlayerModelTertiaryColorIndex = tertiary; + profile->profile.PlayerModelPrimaryColor = CXboxColorMapping::GetColorItemIndex(ms.playerColors[i].colors[0], game); + profile->profile.PlayerModelSecondaryColor = CXboxColorMapping::GetColorItemIndex(ms.playerColors[i].colors[1], game); + profile->profile.PlayerModelTertiaryColor = CXboxColorMapping::GetColorItemIndex(ms.playerColors[i].colors[2], game); + } + } + if (engine) + engine->load_setting(); + + m_stateMachine.reset(); + } +} diff --git a/src/render/imgui/game/xbox/CXboxContext.h b/src/render/imgui/game/xbox/CXboxContext.h new file mode 100644 index 0000000..566d5f9 --- /dev/null +++ b/src/render/imgui/game/xbox/CXboxContext.h @@ -0,0 +1,32 @@ +#pragma once + +#include "render/imgui/ICContext.h" +#include +#include "CStateMachine.h" + +#include +#include + +class CXboxContext : public ICContext { +public: + CXboxContext(ImFont* font); + ~CXboxContext(); + + void open(); + void close(); + bool isOpen() const; + + void handleInput(InputCommand cmd); + void render() override; + +private: + Menu m_menu; + std::array m_sounds; + std::optional m_stateMachine; + ImFont* m_font; + Uint32 m_lastTick = 0; + + static constexpr const char* k_savePath = "./MCC/Binaries/Win64/alpha_ring_menu.bin"; +}; + +extern CXboxContext* g_pXboxContext; diff --git a/src/render/imgui/game/xbox/CXboxMenu.hpp b/src/render/imgui/game/xbox/CXboxMenu.hpp new file mode 100644 index 0000000..b9a6d35 --- /dev/null +++ b/src/render/imgui/game/xbox/CXboxMenu.hpp @@ -0,0 +1,231 @@ +#pragma once + +#include +#include "./assets/colors.h" +#include +#include +#include +#include + +// option types: +// - 0 = boolean +// - 1 = subpages +// - 2 = positive increment +// - 3 = negative increment +// - 4 = pointer display + +enum class OptionType { + Boolean, + TeamToggle, + Subpage, + SubpageOption, + Increment, + Decrement, + PointerDisplay +}; + +class Option { +public: + std::string label; + OptionType type; + std::vector