// CoopAllTheThings host. // // Borderless window that Steam Remote Play Together captures, plus the overlay // that drives the tool: it forwards controller input into an injected game // (Injection panel), spoofs the game's focus, and mirrors the game's window into // this window via Windows Graphics Capture (Video mirror panel). #include #include #include #include "imgui.h" #include "audio_panel.hpp" #include "capture_panel.hpp" #include "controllers_panel.hpp" #include "d3d11_window.hpp" #include "imgui_layer.hpp" #include "inject/mkb_forward.hpp" #include "injection_panel.hpp" #include "input/xinput_source.hpp" #include "log_panel.hpp" #include "ui/app_chrome.hpp" #ifdef COOP_WITH_STEAM #include #include "input/steam_input_source.hpp" #endif namespace { #ifdef COOP_WITH_STEAM // Absolute path to the bundled Steam Input action manifest (next to the exe). std::string steam_manifest_path() { char buffer[MAX_PATH] = {}; const DWORD len = GetModuleFileNameA(nullptr, buffer, MAX_PATH); std::string path(buffer, len); const std::size_t slash = path.find_last_of("\\/"); if (slash != std::string::npos) { path.resize(slash + 1); } return path + "steam_input_actions.vdf"; } #endif // When the overlay is hidden, briefly show a fading hint so the operator can find // the way back. The window is borderless and non-interactive so it never steals a // click or a frame from the mirror underneath. void draw_overlay_hidden_hint(double seconds_hidden) { const float fade = 1.0f - static_cast(seconds_hidden) / 4.0f; if (fade <= 0.0f) { return; // fully faded -> truly clean window for RPT capture } ImGui::SetNextWindowPos(ImVec2(12.0f, 12.0f)); ImGui::SetNextWindowBgAlpha(0.35f * fade); const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; ImGui::Begin("##overlay_hint", nullptr, flags); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, fade)); ImGui::TextUnformatted("F1: show overlay"); ImGui::PopStyleColor(); ImGui::End(); } int run() { coop::D3D11Window window; if (!window.create(L"CoopAllTheThings")) { MessageBoxW(nullptr, L"Failed to create the D3D11 window.", L"CoopAllTheThings", MB_ICONERROR); return 1; } coop::ImGuiLayer imgui; if (!imgui.init(window.hwnd(), window.device(), window.context())) { MessageBoxW(nullptr, L"Failed to initialize ImGui.", L"CoopAllTheThings", MB_ICONERROR); return 1; } // XInput is the default guest-input path: Remote Play Together delivers guest // pads as XInput, and it Just Works. Steam Input is opt-in (Controllers panel) -- // merely initializing it activates Steam's in-process XInput interception, which // hides controllers from XInput unless they're bound to our action set for this // app, so making it the default can silently break input. We switch the active // backend at runtime to match the toggle. coop::XInputSource xinput; coop::InputSource* input = &xinput; #ifdef COOP_WITH_STEAM std::unique_ptr steam; #endif coop::ControllersPanel controllers; coop::InjectionPanel injection; coop::AudioPanel audio; coop::CapturePanel capture; coop::LogPanel log; if (!capture.init(window.device())) { MessageBoxW(nullptr, L"Failed to initialize the video mirror.", L"CoopAllTheThings", MB_ICONERROR); return 1; } capture.set_injection(&injection); // for the Present-hook (Hooked) video source // The overlay can be hidden (F1) so the window is a clean mirror for Remote // Play Together; the pipelines keep running underneath either way. bool show_overlay = true; double overlay_hidden_at = 0.0; coop::UiState ui; coop::FrameStats stats; while (window.pump_messages()) { #ifdef COOP_WITH_STEAM // Switch the active input backend to match the Controllers-panel toggle. const bool want_steam = controllers.steam_input_requested(); if (want_steam && steam == nullptr) { steam = std::make_unique(); if (steam->init(steam_manifest_path())) { input = steam.get(); controllers.set_steam_active(true); } else { steam.reset(); controllers.on_steam_init_failed(); } } else if (!want_steam && steam != nullptr) { steam->shutdown(); steam.reset(); input = &xinput; controllers.set_steam_active(false); } #endif input->poll(); injection.set_test_input(controllers.test_input()); // toggle lives in the Controllers panel injection.publish(input->pads()); injection.tick(); // refresh target liveness before the mirror panels read game_hwnd() const HWND game = injection.game_hwnd(); capture.set_target(game); audio.set_target(game); imgui.begin_frame(); stats.tick(ImGui::GetIO().DeltaTime * 1000.0f); log.pull(injection); // drain hook log lines even while the Log window is hidden if (ImGui::IsKeyPressed(ImGuiKey_F1, false)) { show_overlay = !show_overlay; if (!show_overlay) { overlay_hidden_at = ImGui::GetTime(); } } if (show_overlay) { coop::draw_main_menu_bar(ui, stats); if (ui.show_controllers) { controllers.draw(*input, injection.hook_status(), ui.debug_details); } if (ui.show_injection) { injection.draw(ui.debug_details); } if (ui.show_audio) { audio.draw_ui(injection.hook_status(), ui.debug_details); } if (ui.show_video) { capture.draw_ui(stats); } if (ui.show_log) { log.draw(); } } else { draw_overlay_hidden_hint(ImGui::GetTime() - overlay_hidden_at); } coop::apply_layout_end_frame(); // clear the one-shot "Reset layout" force // Forward the host window's mouse/keyboard into the game (when the MKB // subsystem is on, we're focused, and ImGui isn't using the event). coop::forward_mkb_frame(injection, window.hwnd(), capture.mirroring(), capture.source_hooked()); RECT client = {}; GetClientRect(window.hwnd(), &client); const auto dst_w = static_cast(client.right - client.left); const auto dst_h = static_cast(client.bottom - client.top); // Mirrored frame first (the window background), ImGui overlay on top. window.render_frame([&]() { capture.render(window.context(), dst_w, dst_h); imgui.end_frame(); }); } return 0; } } // namespace int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) { // WGC requires an initialized apartment; multi-threaded suits the // free-threaded frame pool. winrt::init_apartment(winrt::apartment_type::multi_threaded); const int result = run(); winrt::uninit_apartment(); return result; }