Steam Input as the primary guest-input path (optional, SDK-gated)

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>
This commit is contained in:
2026-06-20 11:51:58 +02:00
parent b1f8783321
commit edf2da69d8
8 changed files with 528 additions and 7 deletions

View File

@@ -23,9 +23,31 @@
#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.
@@ -64,7 +86,18 @@ int run()
return 1;
}
auto input = std::make_unique<coop::XInputSource>();
// 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;