From e5a10a0bbf443ec83bfa5eeb2165db620732334e Mon Sep 17 00:00:00 2001 From: Daniel Coyle Date: Wed, 26 Aug 2026 18:55:27 -0500 Subject: [PATCH 1/4] fix: prevent debug menu flicker when holding F1 --- src/input/Input.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/input/Input.cpp b/src/input/Input.cpp index f4c9966..db83695 100644 --- a/src/input/Input.cpp +++ b/src/input/Input.cpp @@ -90,11 +90,13 @@ namespace AlphaRing::Input { prevButtons = buttons; // configurable debug UI keyboard key + static bool debugKeyWasDown = false; int debugKey = g_menuConfig.debugKeyboardVKey; - if (GetAsyncKeyState(debugKey) & 0x8000) { + bool debugKeyIsDown = (GetAsyncKeyState(debugKey) & 0x8000) != 0; + if (debugKeyIsDown && !debugKeyWasDown) { AlphaRing::Global::Global()->show_imgui = !AlphaRing::Global::Global()->show_imgui; - return false; } + debugKeyWasDown = debugKeyIsDown; // configurable debug UI combo WORD debugCombo = g_menuConfig.debugComboMask; From ea11468e6eff361a92ef355cf92ca6742eb78af2 Mon Sep 17 00:00:00 2001 From: Daniel Coyle Date: Fri, 4 Sep 2026 14:49:16 -0500 Subject: [PATCH 2/4] added black bars H3, ODST, H4, H Reach --- src/mcc/module/CModule.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/mcc/module/CModule.cpp b/src/mcc/module/CModule.cpp index 19347e7..f5a8fa3 100644 --- a/src/mcc/module/CModule.cpp +++ b/src/mcc/module/CModule.cpp @@ -51,9 +51,9 @@ static struct { {"splitscreen_patch4", "force making splitscreen works with more than 2 players", 0x5153E, "\x83\xF8\x01\x74\x04", true}, }}, {Halo3EntrySet(), { {"splitscreen_patch1", "", OFFSET_HALO3_PF_COOP_JOIN, "\x31\xC0\xC3\x90", true}, - {"Remove Black Bar1", "remove black bar", 0x8AE150/*0x8AD160*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", true}, - {"Remove Black Bar2", "remove black bar", 0x8AE164/*0x8AD174*/, "\x00\x00\x00\x00\x00\x00\x00\x3F\x00\x00\x80\x3F\x00\x00\x80\x3F\x01", true}, - {"Remove Black Bar3", "remove black bar", 0x8AE1A0/*0x8AD1B0*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", true}, + {"Remove Black Bar1", "remove black bar", 0x8AE150/*0x8AD160*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, + {"Remove Black Bar2", "remove black bar", 0x8AE164/*0x8AD174*/, "\x00\x00\x00\x00\x00\x00\x00\x3F\x00\x00\x80\x3F\x00\x00\x80\x3F\x01", false}, + {"Remove Black Bar3", "remove black bar", 0x8AE1A0/*0x8AD1B0*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, }}, {nullptr, { {"splitscreen_patch1", "", OFFSET_HALO4_PF_COOP_JOIN, "\x31\xC0\xC3\x90", true}, {"splitscreen_patch2", "", OFFSET_HALO4_PF_COOP_REJOIN, "\xEB", true}, @@ -69,15 +69,15 @@ static struct { {"Remove Black Bar3", "remove black bar", OFFSET_GROUNDHOG_BLACKBAR_3 , "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, }}, {nullptr, { {"splitscreen_patch1", "", OFFSET_HALO3ODST_PF_COOP_JOIN, "\x31\xC0\xC3\x90", true}, - {"Remove Black Bar1", "remove black bar", 0x8F1FB0/*0x8F1FC0*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", true}, - {"Remove Black Bar2", "remove black bar", 0x8F1FC4/*0x8F1FD4*/, "\x00\x00\x00\x00\x00\x00\x00\x3F\x00\x00\x80\x3F\x00\x00\x80\x3F\x01", true}, - {"Remove Black Bar3", "remove black bar", 0x8F2000/*0x8F2010*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", true}, + {"Remove Black Bar1", "remove black bar", 0x8F1FB0/*0x8F1FC0*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, + {"Remove Black Bar2", "remove black bar", 0x8F1FC4/*0x8F1FD4*/, "\x00\x00\x00\x00\x00\x00\x00\x3F\x00\x00\x80\x3F\x00\x00\x80\x3F\x01", false}, + {"Remove Black Bar3", "remove black bar", 0x8F2000/*0x8F2010*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, }}, {nullptr, { {"splitscreen_patch1", "", OFFSET_HALOREACH_PF_COOP_JOIN, "\x31\xC0\xC3\x90", true}, {"splitscreen_patch2", "", OFFSET_HALOREACH_PF_COOP_REJOIN, "\xEB", true}, - {"Remove Black Bar1", "remove black bar", 0xB43CE0/*0xB43D10*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", true}, - {"Remove Black Bar2", "remove black bar", 0xB43CF4/*0xB43D24*/, "\x00\x00\x00\x00\x00\x00\x00\x3F\x00\x00\x80\x3F\x00\x00\x80\x3F\x01", true}, - {"Remove Black Bar3", "remove black bar", 0xB43D30/*0xB43D60*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", true}, + {"Remove Black Bar1", "remove black bar", 0xB43CE0/*0xB43D10*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, + {"Remove Black Bar2", "remove black bar", 0xB43CF4/*0xB43D24*/, "\x00\x00\x00\x00\x00\x00\x00\x3F\x00\x00\x80\x3F\x00\x00\x80\x3F\x01", false}, + {"Remove Black Bar3", "remove black bar", 0xB43D30/*0xB43D60*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, }}}; static std::unordered_map map_modules { From c83b8cac07a4f0bd2fd2a454157911a64f407da0 Mon Sep 17 00:00:00 2001 From: Daniel Coyle Date: Sat, 5 Sep 2026 17:39:29 -0500 Subject: [PATCH 3/4] fix: HaloReach 2p loadout invisible in stretched splitscreen Root cause: the loadout menu's own layout-lookup function only ever recognized resolution values 0, 2 and 3 - any other value (including 1, used for fully-stretched no-black-bar splitscreen) fell through to "no layout", so the loadout screen never got built. Added a hook that makes resolution=1 reuse resolution=3's known-good layout instead. This also surfaced a pre-existing wiring gap: HaloReach's CModule was never given its EntrySet (passed nullptr), so none of its function hooks (including the new one) were ever actually installed - fixed by wiring in HaloReachEntrySet(). Also carries forward earlier changes from this investigation: - fix F1 debug menu mouse cursor not auto-showing, and background mouse/keyboard input being fed to ImGui when no menu is open - remove redundant player-count control from the splitscreen window (now set exclusively via the F4 Xbox menu) - live Splitscreen Config Editor (Game -> Patch -> haloreach) for testing config table values without rebuilding - HaloReach black-bar patch byte-length/resolution-value fixes and a new (disabled by default) vertical-split experiment patch --- .gitignore | 3 + lib/game/inc/1.3528.0.0/offset_haloreach.h | 3 + src/input/Input.cpp | 1 + src/mcc/module/CModule.cpp | 33 +++--- src/mcc/module/Module.cpp | 113 +++++++++++++++++++++ src/mcc/module/entry/haloreach/loadout.cpp | 28 +++++ src/mcc/splitscreen/Splitscreen.cpp | 13 --- src/render/window/Window.cpp | 8 +- 8 files changed, 172 insertions(+), 30 deletions(-) create mode 100644 src/mcc/module/entry/haloreach/loadout.cpp diff --git a/.gitignore b/.gitignore index be97518..fa44e2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .vs/ build/ out/ +dep/ +vcpkg/ +vcpkg_installed/ diff --git a/lib/game/inc/1.3528.0.0/offset_haloreach.h b/lib/game/inc/1.3528.0.0/offset_haloreach.h index 36d8333..a7a5ea9 100644 --- a/lib/game/inc/1.3528.0.0/offset_haloreach.h +++ b/lib/game/inc/1.3528.0.0/offset_haloreach.h @@ -9,6 +9,9 @@ #define OFFSET_HALOREACH_PF_ADD_LOCAL_PLAYER 0x43DC8 +#define OFFSET_HALOREACH_PF_GET_SPLITSCREEN_PLAYER_COUNT 0xC30D0 +#define OFFSET_HALOREACH_PF_SPLITSCREEN_RESOLUTION_RESOURCE 0x2CA86C + #define OFFSET_HALOREACH_V_ENTRY_PLAYERS 0x3 #define OFFSET_HALOREACH_V_ENTRY_PLAYERS_ACTION 0x23 #define OFFSET_HALOREACH_V_ENTRY_SPLIT_SCREEN 0x2B diff --git a/src/input/Input.cpp b/src/input/Input.cpp index db83695..df70d6a 100644 --- a/src/input/Input.cpp +++ b/src/input/Input.cpp @@ -95,6 +95,7 @@ namespace AlphaRing::Input { bool debugKeyIsDown = (GetAsyncKeyState(debugKey) & 0x8000) != 0; if (debugKeyIsDown && !debugKeyWasDown) { AlphaRing::Global::Global()->show_imgui = !AlphaRing::Global::Global()->show_imgui; + AlphaRing::Global::Global()->show_imgui_mouse = AlphaRing::Global::Global()->show_imgui; } debugKeyWasDown = debugKeyIsDown; diff --git a/src/mcc/module/CModule.cpp b/src/mcc/module/CModule.cpp index f5a8fa3..37d6254 100644 --- a/src/mcc/module/CModule.cpp +++ b/src/mcc/module/CModule.cpp @@ -51,33 +51,34 @@ static struct { {"splitscreen_patch4", "force making splitscreen works with more than 2 players", 0x5153E, "\x83\xF8\x01\x74\x04", true}, }}, {Halo3EntrySet(), { {"splitscreen_patch1", "", OFFSET_HALO3_PF_COOP_JOIN, "\x31\xC0\xC3\x90", true}, - {"Remove Black Bar1", "remove black bar", 0x8AE150/*0x8AD160*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, - {"Remove Black Bar2", "remove black bar", 0x8AE164/*0x8AD174*/, "\x00\x00\x00\x00\x00\x00\x00\x3F\x00\x00\x80\x3F\x00\x00\x80\x3F\x01", false}, - {"Remove Black Bar3", "remove black bar", 0x8AE1A0/*0x8AD1B0*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, + {"Remove Black Bar1", "remove black bar", 0x8AE150/*0x8AD160*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01\x00\x00\x00", false}, + {"Remove Black Bar2", "remove black bar", 0x8AE164/*0x8AD174*/, "\x00\x00\x00\x00\x00\x00\x00\x3F\x00\x00\x80\x3F\x00\x00\x80\x3F\x01\x00\x00\x00", false}, + {"Remove Black Bar3", "remove black bar", 0x8AE1A0/*0x8AD1B0*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01\x00\x00\x00", false}, }}, {nullptr, { {"splitscreen_patch1", "", OFFSET_HALO4_PF_COOP_JOIN, "\x31\xC0\xC3\x90", true}, {"splitscreen_patch2", "", OFFSET_HALO4_PF_COOP_REJOIN, "\xEB", true}, {"splitscreen_patch3", "", OFFSET_HALO4_PF_COOP_PLAYER_LIMIT, "\x90\x90\x90\x90\x90\x90", true}, - {"Remove Black Bar1", "remove black bar", 0xE84E50/*0xE84E40*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, - {"Remove Black Bar2", "remove black bar", 0xE84E64/*0xE84E54*/, "\x00\x00\x00\x00\x00\x00\x00\x3F\x00\x00\x80\x3F\x00\x00\x80\x3F\x01", false}, - {"Remove Black Bar3", "remove black bar", 0xE84EA0/*0xE84E90*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, + {"Remove Black Bar1", "remove black bar", 0xE84E50/*0xE84E40*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01\x00\x00\x00", false}, + {"Remove Black Bar2", "remove black bar", 0xE84E64/*0xE84E54*/, "\x00\x00\x00\x00\x00\x00\x00\x3F\x00\x00\x80\x3F\x00\x00\x80\x3F\x01\x00\x00\x00", false}, + {"Remove Black Bar3", "remove black bar", 0xE84EA0/*0xE84E90*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01\x00\x00\x00", false}, }}, {nullptr, { {"splitscreen_patch1", "", OFFSET_GROUNDHOG_PF_COOP_JOIN, "\x31\xC0\xC3\x90", true}, {"splitscreen_patch2", "", OFFSET_GROUNDHOG_PF_REJOIN, "\xEB", true}, - {"Remove Black Bar1", "remove black bar", OFFSET_GROUNDHOG_BLACKBAR_1 , "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, - {"Remove Black Bar2", "remove black bar", OFFSET_GROUNDHOG_BLACKBAR_2 , "\x00\x00\x00\x00\x00\x00\x00\x3F\x00\x00\x80\x3F\x00\x00\x80\x3F\x01", false}, - {"Remove Black Bar3", "remove black bar", OFFSET_GROUNDHOG_BLACKBAR_3 , "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, + {"Remove Black Bar1", "remove black bar", OFFSET_GROUNDHOG_BLACKBAR_1 , "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01\x00\x00\x00", false}, + {"Remove Black Bar2", "remove black bar", OFFSET_GROUNDHOG_BLACKBAR_2 , "\x00\x00\x00\x00\x00\x00\x00\x3F\x00\x00\x80\x3F\x00\x00\x80\x3F\x01\x00\x00\x00", false}, + {"Remove Black Bar3", "remove black bar", OFFSET_GROUNDHOG_BLACKBAR_3 , "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01\x00\x00\x00", false}, }}, {nullptr, { {"splitscreen_patch1", "", OFFSET_HALO3ODST_PF_COOP_JOIN, "\x31\xC0\xC3\x90", true}, - {"Remove Black Bar1", "remove black bar", 0x8F1FB0/*0x8F1FC0*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, - {"Remove Black Bar2", "remove black bar", 0x8F1FC4/*0x8F1FD4*/, "\x00\x00\x00\x00\x00\x00\x00\x3F\x00\x00\x80\x3F\x00\x00\x80\x3F\x01", false}, - {"Remove Black Bar3", "remove black bar", 0x8F2000/*0x8F2010*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, -}}, {nullptr, { + {"Remove Black Bar1", "remove black bar", 0x8F1FB0/*0x8F1FC0*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01\x00\x00\x00", false}, + {"Remove Black Bar2", "remove black bar", 0x8F1FC4/*0x8F1FD4*/, "\x00\x00\x00\x00\x00\x00\x00\x3F\x00\x00\x80\x3F\x00\x00\x80\x3F\x01\x00\x00\x00", false}, + {"Remove Black Bar3", "remove black bar", 0x8F2000/*0x8F2010*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01\x00\x00\x00", false}, +}}, {HaloReachEntrySet(), { {"splitscreen_patch1", "", OFFSET_HALOREACH_PF_COOP_JOIN, "\x31\xC0\xC3\x90", true}, {"splitscreen_patch2", "", OFFSET_HALOREACH_PF_COOP_REJOIN, "\xEB", true}, - {"Remove Black Bar1", "remove black bar", 0xB43CE0/*0xB43D10*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, - {"Remove Black Bar2", "remove black bar", 0xB43CF4/*0xB43D24*/, "\x00\x00\x00\x00\x00\x00\x00\x3F\x00\x00\x80\x3F\x00\x00\x80\x3F\x01", false}, - {"Remove Black Bar3", "remove black bar", 0xB43D30/*0xB43D60*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01", false}, + {"Remove Black Bar1", "remove black bar", 0xB43CE0/*0xB43D10*/, "\xCD\xCC\x4C\x3D\x00\x00\x00\x00\x33\x33\x73\x3F\x00\x00\x00\x3F\x03\x00\x00\x00", false}, + {"Remove Black Bar2", "remove black bar", 0xB43CF4/*0xB43D24*/, "\xCD\xCC\x4C\x3D\x00\x00\x00\x3F\x33\x33\x73\x3F\x00\x00\x80\x3F\x03\x00\x00\x00", false}, + {"Remove Black Bar3", "remove black bar", 0xB43D30/*0xB43D60*/, "\xCD\xCC\x4C\x3D\x00\x00\x00\x00\x33\x33\x73\x3F\x00\x00\x00\x3F\x03\x00\x00\x00", false}, + {"Disable Black Bar Overlay", "neuters the function that paints black-bar/divider overlays for 2p/3p/4p splitscreen (uses only slot 0's x0/x1 for pillarbox bars, spanning full height - assumes horizontal top/bottom splits only, paints over anything past slot 0's x1 for vertical/custom splits)", 0x2C6D84, "\x31\xC0\xC3\x90", false}, }}}; static std::unordered_map map_modules { diff --git a/src/mcc/module/Module.cpp b/src/mcc/module/Module.cpp index 6d3da9f..e4e31f2 100644 --- a/src/mcc/module/Module.cpp +++ b/src/mcc/module/Module.cpp @@ -1,6 +1,7 @@ #include "Module.h" #include +#include #include "tinyxml2.h" @@ -231,6 +232,118 @@ namespace MCC::Module { for (auto patch : p_patches->patches()) p_print(patch); + if (i == MODULE_HALOREACH) { + auto hModule = GetSubModule((eModule)i)->info().hModule; + + constexpr __int64 base_offset = 0xB43C40; + constexpr int entry_size = 20; + constexpr int block_count = 5; + constexpr int slot_count = 4; + + if (ImGui::CollapsingHeader("Splitscreen Config Editor")) { + if (hModule == 0) { + ImGui::TextColored(ImVec4(1, 0.3f, 0.3f, 1), "haloreach.dll not loaded"); + } else { + static const char* block_labels[block_count] = { + "0: alias of 4p", "1: 1 player", "2: 2 players", + "3: 3 players", "4: 4 players" + }; + + auto write_bytes = [](void* dst, const void* src, size_t size) { + DWORD oldProtect; + if (VirtualProtect(dst, size, PAGE_EXECUTE_READWRITE, &oldProtect)) { + memcpy(dst, src, size); + VirtualProtect(dst, size, oldProtect, &oldProtect); + } + }; + + ImGui::TextDisabled("Edits write directly to live game memory. No rebuild needed;"); + ImGui::TextDisabled("changes are lost on game restart unless also set as a patch."); + + if (ImGui::BeginTable("splitscreen_config", 6, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | + ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("Slot", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableSetupColumn("x0", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableSetupColumn("y0", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableSetupColumn("x1", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableSetupColumn("y1", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableSetupColumn("resolution", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableHeadersRow(); + + for (int block = 0; block < block_count; ++block) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s", block_labels[block]); + + for (int slot = 0; slot < slot_count; ++slot) { + int index = block * slot_count + slot; + auto p_entry = (unsigned char*)(hModule + base_offset + (__int64)index * entry_size); + + float vals[4]; + __int32 res; + memcpy(vals, p_entry, 16); + memcpy(&res, p_entry + 16, 4); + + ImGui::TableNextRow(); + ImGui::PushID(index); + + ImGui::TableSetColumnIndex(0); + ImGui::Text(" slot %d", slot); + + const char* field_labels[4] = {"##x0", "##y0", "##x1", "##y1"}; + for (int f = 0; f < 4; ++f) { + ImGui::TableSetColumnIndex(1 + f); + ImGui::SetNextItemWidth(-1); + if (ImGui::InputFloat(field_labels[f], &vals[f], 0.0f, 0.0f, "%.4f")) + write_bytes(p_entry + f * 4, &vals[f], 4); + } + + ImGui::TableSetColumnIndex(5); + ImGui::SetNextItemWidth(-1); + if (ImGui::InputInt("##res", &res, 0, 0)) + write_bytes(p_entry + 16, &res, 4); + + ImGui::PopID(); + } + } + ImGui::EndTable(); + } + } + } + + if (ImGui::Button("Dump Splitscreen Config Table to Log")) { + if (hModule == 0) { + LOG_ERROR("Dump Splitscreen Config Table: haloreach.dll not loaded"); + } else { + LOG_INFO("=== c_splitscreen_config::m_config_table @ base+0x{:X} ===", base_offset); + + for (int block = 0; block < block_count; ++block) { + for (int slot = 0; slot < slot_count; ++slot) { + int index = block * slot_count + slot; + auto p_entry = (unsigned char*)(hModule + base_offset + (__int64)index * entry_size); + + float f0, f1, f2, f3; + __int32 res; + memcpy(&f0, p_entry + 0, 4); + memcpy(&f1, p_entry + 4, 4); + memcpy(&f2, p_entry + 8, 4); + memcpy(&f3, p_entry + 12, 4); + memcpy(&res, p_entry + 16, 4); + + LOG_INFO("block {} slot {} (entry {}, offset 0x{:X}): raw=({:.4f}, {:.4f}, {:.4f}, {:.4f}) res={}", + block, slot, index, base_offset + (__int64)index * entry_size, f0, f1, f2, f3, res); + } + } + + LOG_INFO("=== end dump ==="); + } + } + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Reads the live, unpatched-if-not-toggled bytes directly from haloreach.dll and logs them. Run this BEFORE toggling any Remove Black Bar checkbox to capture true defaults."); + } + ImGui::EndTabItem(); } } diff --git a/src/mcc/module/entry/haloreach/loadout.cpp b/src/mcc/module/entry/haloreach/loadout.cpp new file mode 100644 index 0000000..08672d1 --- /dev/null +++ b/src/mcc/module/entry/haloreach/loadout.cpp @@ -0,0 +1,28 @@ +#include "haloreach.h" + +#include "common.h" + +namespace HaloReach::Entry::Loadout { + typedef unsigned __int64 (*GetSplitscreenPlayerCount_t)(); + + // The game's own resolution->resource-ID lookup only recognizes resolution + // values 0, 2 and 3 (each maps to a registered loadout layout template); any + // other value, including 1, falls through to a "not found" default and the + // loadout screen never gets built. resolution=1 is otherwise the cleanest + // splitscreen mode (no HUD/crosshair artifacts), so we reuse resolution=3's + // already-working template ID for it instead of patching the game's own + // (space-constrained) lookup function directly. + HaloReachEntry(entry, OFFSET_HALOREACH_PF_SPLITSCREEN_RESOLUTION_RESOURCE, int, detour, int slot_index) { + __int64 hModule = entry.m_target - entry.m_offset; + + if (slot_index >= 0 && slot_index < 4) { + auto GetSplitscreenPlayerCount = (GetSplitscreenPlayerCount_t)(hModule + OFFSET_HALOREACH_PF_GET_SPLITSCREEN_PLAYER_COUNT); + int player_count = (int)GetSplitscreenPlayerCount(); + int* p_resolution = (int*)(hModule + 0xB43C40 + 0x10 + (size_t)(slot_index + player_count * 4) * 20); + + if (*p_resolution == 1) return 0x80046; + } + + return ((detour_t)entry.m_pOriginal)(slot_index); + } +} diff --git a/src/mcc/splitscreen/Splitscreen.cpp b/src/mcc/splitscreen/Splitscreen.cpp index c47fc36..d7bc352 100644 --- a/src/mcc/splitscreen/Splitscreen.cpp +++ b/src/mcc/splitscreen/Splitscreen.cpp @@ -186,20 +186,7 @@ namespace MCC::Splitscreen { ); 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) { - p_setting->player_count = count; - dirty = true; - } - ImGui::PopItemWidth(); ImGui::EndMenuBar(); -#pragma endregion } if (ImGui::BeginTabBar("Players")) { diff --git a/src/render/window/Window.cpp b/src/render/window/Window.cpp index a2baa84..30b516a 100644 --- a/src/render/window/Window.cpp +++ b/src/render/window/Window.cpp @@ -48,7 +48,13 @@ namespace AlphaRing::Render::Window { } } - if (ImGui_ImplWin32_WndProcHandler(hWnd, uMsg, wParam, lParam)) + // Only feed ImGui real WndProc input while one of our menus is actually + // visible — otherwise ImGui silently queues mouse/keyboard events for the + // entire play session with nothing ever consuming them (NewFrame() is + // also gated on menu visibility), which causes a glitch/jump once the + // menu is finally opened again after a long session. + bool menuActive = xboxOpen || AlphaRing::Global::Global()->show_imgui; + if (menuActive && ImGui_ImplWin32_WndProcHandler(hWnd, uMsg, wParam, lParam)) return true; // Keyboard trigger to open the menu (only reached when menu is closed) From d18853b180a4f282cafa8b366173afdef77b2efe Mon Sep 17 00:00:00 2001 From: Daniel Coyle Date: Sat, 5 Sep 2026 19:03:28 -0500 Subject: [PATCH 4/4] feat: safe, clearly-scoped black-bar removal for HaloReach splitscreen Replaces the three raw "Remove Black Bar1/2/3" checkboxes (Dev Tools -> haloreach) with two controls scoped by which player/slot they actually affect: - "Player 1 (Top, 2 or 3 Player)" - toggles the 2p and 3p top slots together, since they're the same shape either way. Both now write the validated x0=0/x1=1/resolution=1 combo (previously 0.05/0.95/res=3), matching the resolution=1 loadout fix from the previous commit. - "Player 2 (Bottom, 2 Player Only)" - only affects 2-player's bottom slot; disabled until Player 1 is also on, since the game's black-bar overlay painter only ever reads Player 1's bounds and can't visually remove Player 2's bar on its own. Auto-disables if Player 1 is turned back off, so it can't get stuck in that broken combination. Also renames the "Patch" window to "Dev Tools" (still under Game), matching its actual purpose better as it grows. --- src/mcc/module/CModule.cpp | 6 +-- src/mcc/module/Module.cpp | 85 ++++++++++++++++++++++++++++++++++---- 2 files changed, 80 insertions(+), 11 deletions(-) diff --git a/src/mcc/module/CModule.cpp b/src/mcc/module/CModule.cpp index 37d6254..91d0cbd 100644 --- a/src/mcc/module/CModule.cpp +++ b/src/mcc/module/CModule.cpp @@ -75,9 +75,9 @@ static struct { }}, {HaloReachEntrySet(), { {"splitscreen_patch1", "", OFFSET_HALOREACH_PF_COOP_JOIN, "\x31\xC0\xC3\x90", true}, {"splitscreen_patch2", "", OFFSET_HALOREACH_PF_COOP_REJOIN, "\xEB", true}, - {"Remove Black Bar1", "remove black bar", 0xB43CE0/*0xB43D10*/, "\xCD\xCC\x4C\x3D\x00\x00\x00\x00\x33\x33\x73\x3F\x00\x00\x00\x3F\x03\x00\x00\x00", false}, - {"Remove Black Bar2", "remove black bar", 0xB43CF4/*0xB43D24*/, "\xCD\xCC\x4C\x3D\x00\x00\x00\x3F\x33\x33\x73\x3F\x00\x00\x80\x3F\x03\x00\x00\x00", false}, - {"Remove Black Bar3", "remove black bar", 0xB43D30/*0xB43D60*/, "\xCD\xCC\x4C\x3D\x00\x00\x00\x00\x33\x33\x73\x3F\x00\x00\x00\x3F\x03\x00\x00\x00", false}, + {"Remove Black Bar1", "remove black bar", 0xB43CE0/*0xB43D10*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01\x00\x00\x00", false}, + {"Remove Black Bar2", "remove black bar", 0xB43CF4/*0xB43D24*/, "\x00\x00\x00\x00\x00\x00\x00\x3F\x00\x00\x80\x3F\x00\x00\x80\x3F\x01\x00\x00\x00", false}, + {"Remove Black Bar3", "remove black bar", 0xB43D30/*0xB43D60*/, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x3F\x00\x00\x00\x3F\x01\x00\x00\x00", false}, {"Disable Black Bar Overlay", "neuters the function that paints black-bar/divider overlays for 2p/3p/4p splitscreen (uses only slot 0's x0/x1 for pillarbox bars, spanning full height - assumes horizontal top/bottom splits only, paints over anything past slot 0's x1 for vertical/custom splits)", 0x2C6D84, "\x31\xC0\xC3\x90", false}, }}}; diff --git a/src/mcc/module/Module.cpp b/src/mcc/module/Module.cpp index e4e31f2..8343545 100644 --- a/src/mcc/module/Module.cpp +++ b/src/mcc/module/Module.cpp @@ -121,25 +121,25 @@ bool MCC::Module::ReloadPatch(const char *xml_path) { #include "global/Global.h" namespace MCC::Module { - void ContextPatch(); + void ContextDevTools(); void ContextEngine(); void ImGuiContext() { - static bool show_patch; + static bool show_devtools; static bool show_engine; if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("Game")) { ImGui::MenuItem("Engine", nullptr, &show_engine); - ImGui::MenuItem("Patch", nullptr, &show_patch); ImGui::EndMenu(); } + ImGui::MenuItem("Dev Tools", nullptr, &show_devtools); ImGui::EndMainMenuBar(); } - if (show_patch) { - if (ImGui::Begin("Patch", &show_patch, ImGuiWindowFlags_MenuBar)) - ContextPatch(); + if (show_devtools) { + if (ImGui::Begin("Dev Tools", &show_devtools, ImGuiWindowFlags_MenuBar)) + ContextDevTools(); ImGui::End(); } @@ -197,7 +197,7 @@ namespace MCC::Module { #pragma endregion } - void ContextPatch() { + void ContextDevTools() { static int counter; auto p_print = [](CPatch* patch) { bool enabled = patch->enabled(); @@ -210,6 +210,12 @@ namespace MCC::Module { ImGui::SetTooltip("%s", patch->desc()); }; + auto find_patch = [](CPatchSet* p_patches, const char* name) -> CPatch* { + for (auto patch : p_patches->embed_patches()) + if (strcmp(patch->name(), name) == 0) return patch; + return nullptr; + }; + if (ImGui::BeginMenuBar()) { if (ImGui::MenuItem("Reload Patch")) ReloadPatch(); @@ -225,8 +231,71 @@ namespace MCC::Module { if (ImGui::BeginTabItem(cModuleName[i])) { ImGui::Text("Embed Patches"); - for (auto patch : p_patches->embed_patches()) + for (auto patch : p_patches->embed_patches()) { + // HaloReach's black-bar patches get replaced below with two + // controls scoped by which player/slot they actually affect, + // instead of three raw, easy-to-misread checkboxes. + if (i == MODULE_HALOREACH && + (strcmp(patch->name(), "Remove Black Bar1") == 0 || + strcmp(patch->name(), "Remove Black Bar2") == 0 || + strcmp(patch->name(), "Remove Black Bar3") == 0)) + continue; + p_print(patch); + } + + if (i == MODULE_HALOREACH) { + auto p_bar1 = find_patch(p_patches, "Remove Black Bar1"); + auto p_bar2 = find_patch(p_patches, "Remove Black Bar2"); + auto p_bar3 = find_patch(p_patches, "Remove Black Bar3"); + + ImGui::Separator(); + ImGui::Text("Splitscreen Display"); + + if (p_bar1 != nullptr && p_bar3 != nullptr) { + bool top = p_bar1->enabled() && p_bar3->enabled(); + ImGui::PushID(counter++); + if (ImGui::Checkbox("Remove Black Bars - Player 1 (Top, 2 or 3 Player)", &top)) { + p_bar1->setState(top); + p_bar3->setState(top); + + // The game's black-bar overlay is a single shared + // painter that only ever reads Player 1's bounds, + // so Player 2's bar physically cannot disappear + // while Player 1 still has one - turning Player 1 + // back on makes that combination broken again. + if (!top && p_bar2 != nullptr && p_bar2->enabled()) + p_bar2->setState(false); + } + ImGui::PopID(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Removes the black bar for whichever player occupies the top half of the screen. Applies to both 2-player and 3-player splitscreen - that slot is the same shape in either mode."); + } + + if (p_bar2 != nullptr) { + bool player1_on = p_bar1 != nullptr && p_bar1->enabled(); + bool bottom = p_bar2->enabled(); + + ImGui::BeginDisabled(!player1_on); + ImGui::PushID(counter++); + if (ImGui::Checkbox("Remove Black Bars - Player 2 (Bottom, 2 Player Only)", &bottom)) + p_bar2->setState(bottom); + ImGui::PopID(); + ImGui::EndDisabled(); + + // ImGui suppresses IsItemHovered() by default for items inside + // BeginDisabled()/EndDisabled() - AllowWhenDisabled is required + // so the explanation tooltip still shows while greyed out. + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { + if (player1_on) + ImGui::SetTooltip("Removes the black bar for player 2's bottom half of the screen. 2-player only - in 3-player mode, players 2 and 3 already fill their quarter of the screen with no black bars."); + else + ImGui::SetTooltip("Requires Player 1's black bar removed too - the game's bar-painting logic is shared and only reads Player 1's bounds, so Player 2's bar can't disappear on its own."); + } + } + + ImGui::Separator(); + } ImGui::Text("Patches"); for (auto patch : p_patches->patches())