Files
CoopAllTheThings/host/src/main.cpp
BlackMark 056a478e19 Forward rumble back to the guest controller (both backends)
The XInput hook used to swallow XInputSetState; now it records the requested
left/right motor speeds into the status back-channel (protocol v8->v9: per-slot
rumble_left/right in HookStatus). Each frame the host reads them and, only on
change, drives the guest's actuator via the active backend:
- XInput: XInputSetState on the guest's slot (the open question is whether Steam's
  RPT virtual pad accepts vibration and routes it to the guest -- needs live RPT);
- Steam Input: SteamInput TriggerVibration on the slot's controller handle, with the
  XInput fallback for slots Steam isn't driving.

InputSource gains a set_rumble(slot,left,right) hook (default no-op) implemented by
both backends; SteamInputSource now tracks per-slot controller handles + which slots
it drives.

Verified: hook_selftest (x64 + x86) now asserts the hook records the rumble from
XInputSetState into the status; full build x64 + x86 clean; ctest x64 9/9, x86 3/3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 05:11:33 +02:00

248 lines
7.4 KiB
C++

// 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 <memory>
#include <windows.h>
#include <winrt/Windows.Foundation.h>
#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 <string>
#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<float>(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<coop::SteamInputSource> 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;
// Last rumble forwarded per slot, so we only re-send on change.
std::uint16_t last_rumble_l[coop::kMaxPads] = {};
std::uint16_t last_rumble_r[coop::kMaxPads] = {};
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<coop::SteamInputSource>();
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()
// Forward the rumble the game requested back to the guest's controller (only
// when it changes, to avoid spamming XInputSetState / TriggerVibration).
{
const coop::HookStatusView hs = injection.hook_status();
for (int i = 0; i < static_cast<int>(coop::kMaxPads); ++i)
{
if (hs.rumble_left[i] != last_rumble_l[i] || hs.rumble_right[i] != last_rumble_r[i])
{
input->set_rumble(i, hs.rumble_left[i], hs.rumble_right[i]);
last_rumble_l[i] = hs.rumble_left[i];
last_rumble_r[i] = hs.rumble_right[i];
}
}
}
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<std::uint32_t>(client.right - client.left);
const auto dst_h = static_cast<std::uint32_t>(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;
}