cmake_minimum_required(VERSION 3.21) project(CoopAllTheThings VERSION 0.0.1 DESCRIPTION "Steam Remote Play Together for any XInput game" LANGUAGES CXX) if(NOT WIN32) message(FATAL_ERROR "CoopAllTheThings targets Windows only.") endif() set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # Emit compile_commands.json so clangd resolves includes/flags. Ignored by the # Visual Studio generator, so configure the Ninja build (build-clangd/) for this; # .clangd points clangd at that compilation database. set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Single output dir keeps the donor-launch / inject workflow simple: host exe and # hook dll land next to each other under /bin (already in .gitignore). set(COOP_OUTPUT_DIR "${CMAKE_SOURCE_DIR}/bin/$") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${COOP_OUTPUT_DIR}") set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${COOP_OUTPUT_DIR}") if(MSVC) add_compile_options(/W4 /permissive- /Zc:__cplusplus /utf-8 /MP) add_compile_definitions(UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX) endif() # --- x86 sub-build (internal, re-entrant) ---------------------------------- # When the ExternalProject below reconfigures this same tree with -A Win32 and # -DCOOP_X86_HELPER_BUILD=ON, build ONLY the 32-bit hook DLL + injector helper # (staged alongside the x64 binaries) and stop -- no host, tests, or imgui, and # crucially no recursive ExternalProject. option(COOP_X86_HELPER_BUILD "Internal: x86 sub-build (hook DLL + injector helper only)" OFF) if(COOP_X86_HELPER_BUILD) add_subdirectory(common) set(SAFETYHOOK_BUILD_TEST OFF CACHE BOOL "" FORCE) set(SAFETYHOOK_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) set(SAFETYHOOK_BUILD_DOCS OFF CACHE BOOL "" FORCE) add_subdirectory(third_party/safetyhook) add_subdirectory(hook) add_subdirectory(tools/inject_helper) # x86 reproduction of the XInput self-test. The x64 hook_selftest passes, so an # x86 build of the same logic is how we reproduce (and then guard against) the # 32-bit input-forwarding crash on real games like Slaps and Beans. enable_testing() add_executable(hook_selftest_x86 tests/hook_selftest.cpp hook/src/xinput_hook.cpp hook/src/hook_registry.cpp) target_include_directories(hook_selftest_x86 PRIVATE hook/src) target_link_libraries(hook_selftest_x86 PRIVATE coop_common safetyhook::safetyhook xinput) add_test(NAME hook_selftest_x86 COMMAND hook_selftest_x86) # x86 build of the Present-hook test. It drives a real swapchain through the # SafetyHook trampoline, so it catches the x86-only calling-convention bug that # froze 32-bit games (SafetyHook's call() is __cdecl; Present is __stdcall -> # ESP imbalance / Run-Time Check Failure #0). The x64 present_hook_test can't see # it (one calling convention), so this 32-bit build is the regression guard. add_executable(present_hook_test_x86 tests/present_hook_test.cpp hook/src/present_hook.cpp hook/src/debug_log.cpp hook/src/hook_registry.cpp) target_include_directories(present_hook_test_x86 PRIVATE hook/src) target_link_libraries(present_hook_test_x86 PRIVATE coop_common safetyhook::safetyhook d3d11 dxgi) add_test(NAME present_hook_test_x86 COMMAND present_hook_test_x86) # x86 build of the audio render-hook test. It installs the WASAPI hooks and then # creates a *fresh* IAudioClient/IAudioRenderClient that calls Initialize / # GetService / GetBuffer / ReleaseBuffer back through the SafetyHook trampolines # -- the exact ordering that crashed 32-bit Slaps & Beans (FMOD) when audio hooks # were live as the game initialized its output. The x64 audio_hook_test passes, # so this 32-bit build is the regression guard for any x86-only fault in the # audio setup detours / trampoline relocation of AudioSes.dll prologues. add_executable(audio_hook_test_x86 tests/audio_hook_test.cpp hook/src/audio_hook.cpp hook/src/debug_log.cpp hook/src/hook_registry.cpp) target_include_directories(audio_hook_test_x86 PRIVATE hook/src) target_compile_definitions(audio_hook_test_x86 PRIVATE NTDDI_VERSION=0x0A00000B) target_link_libraries(audio_hook_test_x86 PRIVATE coop_common safetyhook::safetyhook ole32 mmdevapi) add_test(NAME audio_hook_test_x86 COMMAND audio_hook_test_x86) return() endif() add_subdirectory(third_party) add_subdirectory(common) add_subdirectory(host) # The injected hook DLL pulls in SafetyHook, which fetches Zydis via FetchContent # at configure time (allowed for transitive deps). option(COOP_BUILD_HOOK "Build the injected game-side hook DLL" ON) if(COOP_BUILD_HOOK) # SafetyHook's own tests/examples/docs are noise for us. set(SAFETYHOOK_BUILD_TEST OFF CACHE BOOL "" FORCE) set(SAFETYHOOK_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) set(SAFETYHOOK_BUILD_DOCS OFF CACHE BOOL "" FORCE) add_subdirectory(third_party/safetyhook) add_subdirectory(hook) enable_testing() add_subdirectory(tools/audio_tone) # coop_tone: audio source for the loopback test add_subdirectory(tools/audio_probe) # coop_audio_probe: inject + diagnose the render-hook add_subdirectory(tools/input_probe) # coop_input_probe: inject + forward synthetic input add_subdirectory(tests) endif() # --- x86 helper artifacts for 32-bit (WOW64) games ------------------------- # A 64-bit process can't cleanly inject a 32-bit one, so build the x86 hook DLL # (coop_hook_x86.dll) + a 32-bit injector helper (coop_inject_x86.exe) via a # nested Win32 configure of this same tree, staged next to the x64 binaries. Only # the multi-config Visual Studio generator is supported here (the Ninja # build-clangd/ tree is just for compile_commands and skips this). option(COOP_BUILD_X86_HELPER "Build the x86 hook DLL + injector helper for 32-bit games" ON) if(COOP_BUILD_HOOK AND COOP_BUILD_X86_HELPER AND CMAKE_GENERATOR MATCHES "Visual Studio") include(ExternalProject) ExternalProject_Add(coop_x86 SOURCE_DIR "${CMAKE_SOURCE_DIR}" BINARY_DIR "${CMAKE_BINARY_DIR}/x86" CMAKE_GENERATOR "${CMAKE_GENERATOR}" CMAKE_GENERATOR_PLATFORM "Win32" CMAKE_ARGS "-DCOOP_X86_HELPER_BUILD=ON" BUILD_COMMAND "${CMAKE_COMMAND}" --build --config $ INSTALL_COMMAND "" BUILD_ALWAYS ON USES_TERMINAL_CONFIGURE ON USES_TERMINAL_BUILD ON) endif()