Add SteamInputSource: initializes SteamAPI + Steam Input, loads a bundled action manifest via SetInputActionManifestFilePath (no partner-backend config needed), and reads the GameControls action set into CoopPadState -- falling back to XInput per slot, and to pure XInput if Steam isn't available, so the host always runs. Enabled automatically when the Steamworks SDK is vendored at third_party/steamworks_sdk/ (auto-detected by CMake; gitignored and never committed -- the build is XInput-only without it). Stages steam_api64.dll + the manifest next to the host and builds coop_steam_input_probe (a console smoke test). Verified: the probe initializes against the live Steam client and enumerates controllers; the host degrades gracefully when launched standalone. All 5 tests pass. Reading actual controller state needs a pad bound through Steam Input for the running (donor) appid, which XInput otherwise covers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
196 lines
5.4 KiB
C++
196 lines
5.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 "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;
|
|
}
|
|
|
|
// Steam Input is the plan's primary guest-input path (it falls back to XInput
|
|
// per slot, and to pure XInput if Steam isn't available). When the host is built
|
|
// without the Steamworks SDK, use XInput directly.
|
|
#ifdef COOP_WITH_STEAM
|
|
std::unique_ptr<coop::InputSource> input = [] {
|
|
auto steam = std::make_unique<coop::SteamInputSource>();
|
|
steam->init(steam_manifest_path());
|
|
return steam;
|
|
}();
|
|
#else
|
|
std::unique_ptr<coop::InputSource> input = std::make_unique<coop::XInputSource>();
|
|
#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())
|
|
{
|
|
input->poll();
|
|
injection.publish(input->pads());
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|