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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -10,3 +10,7 @@ CMakeUserPresets.json
|
|||||||
x64
|
x64
|
||||||
Debug
|
Debug
|
||||||
Release
|
Release
|
||||||
|
|
||||||
|
# Steamworks SDK: vendored locally, not redistributable -- never commit it.
|
||||||
|
/steamworks_sdk_*.zip
|
||||||
|
/third_party/steamworks_sdk/
|
||||||
|
|||||||
29
README.md
29
README.md
@@ -19,7 +19,7 @@ XInput game becomes Remote-Play-Together-able.
|
|||||||
|
|
||||||
| Concern | Mechanism | Component | Status |
|
| Concern | Mechanism | Component | Status |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Receive guest input | XInput (RPT delivers guest pads to the focused window) | `coop_host.exe` | done |
|
| Receive guest input | Steam Input (primary, when built with the Steamworks SDK) with XInput fallback; RPT delivers guest pads to the focused window | `coop_host.exe` | done |
|
||||||
| Forward input to game | DLL injection + XInput hook (SafetyHook) — game sees *only* our pad | `coop_hook.dll` | done |
|
| Forward input to game | DLL injection + XInput hook (SafetyHook) — game sees *only* our pad | `coop_hook.dll` | done |
|
||||||
| Keep game running unfocused | Hook spoofs focus so the game polls while the host holds OS focus | `coop_hook.dll` | done |
|
| Keep game running unfocused | Hook spoofs focus so the game polls while the host holds OS focus | `coop_hook.dll` | done |
|
||||||
| Mirror video | Windows Graphics Capture of the game window, letterboxed into the host window | `coop_host.exe` | done |
|
| Mirror video | Windows Graphics Capture of the game window, letterboxed into the host window | `coop_host.exe` | done |
|
||||||
@@ -155,10 +155,21 @@ Done:
|
|||||||
all subsystems hooked, and the IPC channels (status, audio ring, video share,
|
all subsystems hooked, and the IPC channels (status, audio ring, video share,
|
||||||
log) all flow across the x64↔x86 boundary.
|
log) all flow across the x64↔x86 boundary.
|
||||||
|
|
||||||
Future work, roughly in priority order:
|
- **Steam Input. ✅** When the host is built with the Steamworks SDK
|
||||||
|
(auto-detected under `third_party/steamworks_sdk/`), guest input comes through
|
||||||
|
the Steam Input API (action-based) as the primary path, falling back to XInput
|
||||||
|
per slot — and to pure XInput if Steam isn't available, so the host always runs.
|
||||||
|
It initializes SteamAPI + Steam Input and loads a bundled action manifest
|
||||||
|
(`steam_input_actions.vdf`) via `SetInputActionManifestFilePath`, so it needs no
|
||||||
|
partner-backend config. Verified to initialize against the live Steam client and
|
||||||
|
enumerate controllers (`coop_steam_input_probe`). Reading actual button/stick
|
||||||
|
state still requires a controller bound through Steam Input for the running
|
||||||
|
appid (the donor appid under RPT) — without that binding the XInput path carries
|
||||||
|
the guest input, as before.
|
||||||
|
|
||||||
- **Steam Input:** consume guest input through the Steam Input API directly
|
All planned phases are now implemented. Possible later work: per-stream audio
|
||||||
rather than XInput.
|
mixing for multi-stream games, per-stream format detection for the audio hook, and
|
||||||
|
hooking D3D9 / pure-D3D12 present paths for the video hook.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
@@ -180,8 +191,14 @@ configured into `build/x86/`) that produces `coop_hook_x86.dll` and
|
|||||||
it with `-DCOOP_BUILD_X86_HELPER=OFF` if you don't need 32-bit support.
|
it with `-DCOOP_BUILD_X86_HELPER=OFF` if you don't need 32-bit support.
|
||||||
|
|
||||||
Third-party dependencies (Dear ImGui, SafetyHook) are git submodules under
|
Third-party dependencies (Dear ImGui, SafetyHook) are git submodules under
|
||||||
`third_party/`; the Steamworks SDK is vendored manually there when wired. No
|
`third_party/`. No vcpkg / package manager is used.
|
||||||
vcpkg / package manager is used.
|
|
||||||
|
**Steam Input is optional.** It's enabled automatically when the Steamworks SDK is
|
||||||
|
vendored at `third_party/steamworks_sdk/` (extract the `steamworks_sdk_*.zip`
|
||||||
|
there). The SDK isn't redistributable, so it's gitignored and never committed; if
|
||||||
|
it's absent the host builds XInput-only (no other features depend on it). When
|
||||||
|
present, the build links `steam_api64.lib`, stages `steam_api64.dll` and the
|
||||||
|
action manifest next to the host, and also builds `coop_steam_input_probe`.
|
||||||
|
|
||||||
### clangd / IDE setup
|
### clangd / IDE setup
|
||||||
|
|
||||||
|
|||||||
@@ -37,3 +37,50 @@ target_link_libraries(coop_host PRIVATE
|
|||||||
ole32)
|
ole32)
|
||||||
|
|
||||||
set_target_properties(coop_host PROPERTIES OUTPUT_NAME "coop_host")
|
set_target_properties(coop_host PROPERTIES OUTPUT_NAME "coop_host")
|
||||||
|
|
||||||
|
# Optional Steam Input: enabled automatically when the Steamworks SDK is vendored
|
||||||
|
# under third_party/steamworks_sdk (it isn't redistributable, so it's gitignored
|
||||||
|
# and never committed). Without it, the host builds XInput-only.
|
||||||
|
set(COOP_STEAM_SDK_DIR "${CMAKE_SOURCE_DIR}/third_party/steamworks_sdk/sdk")
|
||||||
|
if(EXISTS "${COOP_STEAM_SDK_DIR}/public/steam/steam_api.h")
|
||||||
|
message(STATUS "Steamworks SDK found -> Steam Input enabled")
|
||||||
|
target_sources(coop_host PRIVATE src/input/steam_input_source.cpp)
|
||||||
|
target_compile_definitions(coop_host PRIVATE COOP_WITH_STEAM)
|
||||||
|
target_include_directories(coop_host SYSTEM PRIVATE "${COOP_STEAM_SDK_DIR}/public")
|
||||||
|
# The SDK headers trip /W4 (strncpy, non-UTF-8 bytes); silence for this TU only.
|
||||||
|
set_source_files_properties(src/input/steam_input_source.cpp PROPERTIES
|
||||||
|
COMPILE_OPTIONS "/wd4996;/wd4828")
|
||||||
|
target_link_libraries(coop_host PRIVATE
|
||||||
|
"${COOP_STEAM_SDK_DIR}/redistributable_bin/win64/steam_api64.lib")
|
||||||
|
# Stage steam_api64.dll + the action manifest next to coop_host.exe.
|
||||||
|
add_custom_command(TARGET coop_host POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${COOP_STEAM_SDK_DIR}/redistributable_bin/win64/steam_api64.dll"
|
||||||
|
"$<TARGET_FILE_DIR:coop_host>/steam_api64.dll"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/assets/steam_input_actions.vdf"
|
||||||
|
"$<TARGET_FILE_DIR:coop_host>/steam_input_actions.vdf"
|
||||||
|
VERBATIM)
|
||||||
|
|
||||||
|
# Console smoke test for Steam Input (reuses the shipping source). Not a ctest
|
||||||
|
# (needs Steam running + an appid); run manually from bin/<config>/.
|
||||||
|
add_executable(coop_steam_input_probe
|
||||||
|
${CMAKE_SOURCE_DIR}/tools/steam_input_probe/main.cpp
|
||||||
|
src/input/steam_input_source.cpp
|
||||||
|
src/input/xinput_source.cpp)
|
||||||
|
target_include_directories(coop_steam_input_probe PRIVATE src)
|
||||||
|
target_include_directories(coop_steam_input_probe SYSTEM PRIVATE "${COOP_STEAM_SDK_DIR}/public")
|
||||||
|
target_compile_definitions(coop_steam_input_probe PRIVATE COOP_WITH_STEAM)
|
||||||
|
set_source_files_properties(src/input/steam_input_source.cpp PROPERTIES COMPILE_OPTIONS "/wd4996;/wd4828")
|
||||||
|
target_link_libraries(coop_steam_input_probe PRIVATE
|
||||||
|
coop_common
|
||||||
|
xinput
|
||||||
|
"${COOP_STEAM_SDK_DIR}/redistributable_bin/win64/steam_api64.lib")
|
||||||
|
add_custom_command(TARGET coop_steam_input_probe POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${COOP_STEAM_SDK_DIR}/redistributable_bin/win64/steam_api64.dll"
|
||||||
|
"$<TARGET_FILE_DIR:coop_steam_input_probe>/steam_api64.dll"
|
||||||
|
VERBATIM)
|
||||||
|
else()
|
||||||
|
message(STATUS "Steamworks SDK not found -> Steam Input disabled (XInput only)")
|
||||||
|
endif()
|
||||||
|
|||||||
84
host/assets/steam_input_actions.vdf
Normal file
84
host/assets/steam_input_actions.vdf
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// Steam Input In-Game Actions (IGA) manifest for CoopAllTheThings.
|
||||||
|
//
|
||||||
|
// SteamInputSource points Steam Input at this file via
|
||||||
|
// ISteamInput::SetInputActionManifestFilePath, so the host doesn't depend on a
|
||||||
|
// partner-backend-registered config. It declares one action set, "GameControls",
|
||||||
|
// with the standard gamepad buttons + two sticks + two triggers. The action names
|
||||||
|
// here must match host/src/input/steam_input_source.cpp.
|
||||||
|
"In Game Actions"
|
||||||
|
{
|
||||||
|
"actions"
|
||||||
|
{
|
||||||
|
"GameControls"
|
||||||
|
{
|
||||||
|
"title" "#Set_GameControls"
|
||||||
|
"legacy_set" "1"
|
||||||
|
|
||||||
|
"StickPadGyro"
|
||||||
|
{
|
||||||
|
"LeftStick"
|
||||||
|
{
|
||||||
|
"title" "#Action_LeftStick"
|
||||||
|
"input_mode" "joystick_move"
|
||||||
|
}
|
||||||
|
"RightStick"
|
||||||
|
{
|
||||||
|
"title" "#Action_RightStick"
|
||||||
|
"input_mode" "joystick_move"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"AnalogTrigger"
|
||||||
|
{
|
||||||
|
"LeftTrigger" "#Action_LeftTrigger"
|
||||||
|
"RightTrigger" "#Action_RightTrigger"
|
||||||
|
}
|
||||||
|
|
||||||
|
"Button"
|
||||||
|
{
|
||||||
|
"A" "#Action_A"
|
||||||
|
"B" "#Action_B"
|
||||||
|
"X" "#Action_X"
|
||||||
|
"Y" "#Action_Y"
|
||||||
|
"LB" "#Action_LB"
|
||||||
|
"RB" "#Action_RB"
|
||||||
|
"Back" "#Action_Back"
|
||||||
|
"Start" "#Action_Start"
|
||||||
|
"LStickClick" "#Action_LStickClick"
|
||||||
|
"RStickClick" "#Action_RStickClick"
|
||||||
|
"DPadUp" "#Action_DPadUp"
|
||||||
|
"DPadDown" "#Action_DPadDown"
|
||||||
|
"DPadLeft" "#Action_DPadLeft"
|
||||||
|
"DPadRight" "#Action_DPadRight"
|
||||||
|
"Guide" "#Action_Guide"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"localization"
|
||||||
|
{
|
||||||
|
"english"
|
||||||
|
{
|
||||||
|
"Set_GameControls" "Game Controls"
|
||||||
|
"Action_LeftStick" "Left Stick"
|
||||||
|
"Action_RightStick" "Right Stick"
|
||||||
|
"Action_LeftTrigger" "Left Trigger"
|
||||||
|
"Action_RightTrigger" "Right Trigger"
|
||||||
|
"Action_A" "A"
|
||||||
|
"Action_B" "B"
|
||||||
|
"Action_X" "X"
|
||||||
|
"Action_Y" "Y"
|
||||||
|
"Action_LB" "Left Bumper"
|
||||||
|
"Action_RB" "Right Bumper"
|
||||||
|
"Action_Back" "Back"
|
||||||
|
"Action_Start" "Start"
|
||||||
|
"Action_LStickClick" "Left Stick Click"
|
||||||
|
"Action_RStickClick" "Right Stick Click"
|
||||||
|
"Action_DPadUp" "D-Pad Up"
|
||||||
|
"Action_DPadDown" "D-Pad Down"
|
||||||
|
"Action_DPadLeft" "D-Pad Left"
|
||||||
|
"Action_DPadRight" "D-Pad Right"
|
||||||
|
"Action_Guide" "Guide"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
205
host/src/input/steam_input_source.cpp
Normal file
205
host/src/input/steam_input_source.cpp
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
#include "input/steam_input_source.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <xinput.h> // XINPUT_GAMEPAD_* button bits
|
||||||
|
|
||||||
|
#include <steam/steam_api.h>
|
||||||
|
|
||||||
|
namespace coop
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Digital actions in the manifest, paired with the XInput button bit they map to.
|
||||||
|
// Names must match host/assets/steam_input_actions.vdf.
|
||||||
|
struct ButtonAction
|
||||||
|
{
|
||||||
|
const char* action;
|
||||||
|
std::uint16_t xinput_bit;
|
||||||
|
};
|
||||||
|
const ButtonAction kButtons[kSteamButtonActions] = {
|
||||||
|
{"A", XINPUT_GAMEPAD_A},
|
||||||
|
{"B", XINPUT_GAMEPAD_B},
|
||||||
|
{"X", XINPUT_GAMEPAD_X},
|
||||||
|
{"Y", XINPUT_GAMEPAD_Y},
|
||||||
|
{"LB", XINPUT_GAMEPAD_LEFT_SHOULDER},
|
||||||
|
{"RB", XINPUT_GAMEPAD_RIGHT_SHOULDER},
|
||||||
|
{"Back", XINPUT_GAMEPAD_BACK},
|
||||||
|
{"Start", XINPUT_GAMEPAD_START},
|
||||||
|
{"LStickClick", XINPUT_GAMEPAD_LEFT_THUMB},
|
||||||
|
{"RStickClick", XINPUT_GAMEPAD_RIGHT_THUMB},
|
||||||
|
{"DPadUp", XINPUT_GAMEPAD_DPAD_UP},
|
||||||
|
{"DPadDown", XINPUT_GAMEPAD_DPAD_DOWN},
|
||||||
|
{"DPadLeft", XINPUT_GAMEPAD_DPAD_LEFT},
|
||||||
|
{"DPadRight", XINPUT_GAMEPAD_DPAD_RIGHT},
|
||||||
|
{"Guide", 0x0400}, // unofficial XINPUT_GAMEPAD_GUIDE
|
||||||
|
};
|
||||||
|
|
||||||
|
std::int16_t to_axis(float v)
|
||||||
|
{
|
||||||
|
if (v > 1.0f)
|
||||||
|
{
|
||||||
|
v = 1.0f;
|
||||||
|
}
|
||||||
|
if (v < -1.0f)
|
||||||
|
{
|
||||||
|
v = -1.0f;
|
||||||
|
}
|
||||||
|
return static_cast<std::int16_t>(v * 32767.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint8_t to_trigger(float v)
|
||||||
|
{
|
||||||
|
if (v > 1.0f)
|
||||||
|
{
|
||||||
|
v = 1.0f;
|
||||||
|
}
|
||||||
|
if (v < 0.0f)
|
||||||
|
{
|
||||||
|
v = 0.0f;
|
||||||
|
}
|
||||||
|
return static_cast<std::uint8_t>(v * 255.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
SteamInputSource::~SteamInputSource()
|
||||||
|
{
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SteamInputSource::init(const std::string& manifest_absolute_path)
|
||||||
|
{
|
||||||
|
// Running standalone (not launched by Steam) without a steam_appid.txt makes
|
||||||
|
// SteamAPI_Init fail; that's fine -- we degrade to XInput.
|
||||||
|
if (!SteamAPI_Init())
|
||||||
|
{
|
||||||
|
std::printf("SteamInput: SteamAPI_Init failed (not under Steam?); using XInput.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (SteamInput() == nullptr)
|
||||||
|
{
|
||||||
|
std::printf("SteamInput: ISteamInput unavailable; using XInput.\n");
|
||||||
|
SteamAPI_Shutdown();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Point Steam Input at our bundled action manifest so we don't depend on a
|
||||||
|
// partner-backend-registered config. Must be called before Init().
|
||||||
|
if (!manifest_absolute_path.empty())
|
||||||
|
{
|
||||||
|
SteamInput()->SetInputActionManifestFilePath(manifest_absolute_path.c_str());
|
||||||
|
}
|
||||||
|
if (!SteamInput()->Init(/*bExplicitlyCallRunFrame=*/false))
|
||||||
|
{
|
||||||
|
std::printf("SteamInput: ISteamInput::Init failed; using XInput.\n");
|
||||||
|
SteamAPI_Shutdown();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
steam_ready_ = true;
|
||||||
|
name_ = "Steam Input (XInput fallback)";
|
||||||
|
resolve_handles();
|
||||||
|
std::printf("SteamInput: initialized (manifest=%s).\n", manifest_absolute_path.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SteamInputSource::shutdown()
|
||||||
|
{
|
||||||
|
if (steam_ready_)
|
||||||
|
{
|
||||||
|
SteamInput()->Shutdown();
|
||||||
|
SteamAPI_Shutdown();
|
||||||
|
steam_ready_ = false;
|
||||||
|
name_ = "XInput";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SteamInputSource::resolve_handles()
|
||||||
|
{
|
||||||
|
action_set_ = SteamInput()->GetActionSetHandle("GameControls");
|
||||||
|
for (int i = 0; i < kSteamButtonActions; ++i)
|
||||||
|
{
|
||||||
|
button_handles_[i] = SteamInput()->GetDigitalActionHandle(kButtons[i].action);
|
||||||
|
}
|
||||||
|
left_stick_ = SteamInput()->GetAnalogActionHandle("LeftStick");
|
||||||
|
right_stick_ = SteamInput()->GetAnalogActionHandle("RightStick");
|
||||||
|
left_trigger_ = SteamInput()->GetAnalogActionHandle("LeftTrigger");
|
||||||
|
right_trigger_ = SteamInput()->GetAnalogActionHandle("RightTrigger");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SteamInputSource::read_steam_pad(std::uint64_t controller, PadInfo& out) const
|
||||||
|
{
|
||||||
|
SteamInput()->ActivateActionSet(controller, action_set_);
|
||||||
|
|
||||||
|
CoopPadState st{};
|
||||||
|
st.connected = 1;
|
||||||
|
bool any_active = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < kSteamButtonActions; ++i)
|
||||||
|
{
|
||||||
|
const InputDigitalActionData_t d = SteamInput()->GetDigitalActionData(controller, button_handles_[i]);
|
||||||
|
any_active = any_active || d.bActive;
|
||||||
|
if (d.bState)
|
||||||
|
{
|
||||||
|
st.buttons |= kButtons[i].xinput_bit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const InputAnalogActionData_t ls = SteamInput()->GetAnalogActionData(controller, left_stick_);
|
||||||
|
const InputAnalogActionData_t rs = SteamInput()->GetAnalogActionData(controller, right_stick_);
|
||||||
|
const InputAnalogActionData_t lt = SteamInput()->GetAnalogActionData(controller, left_trigger_);
|
||||||
|
const InputAnalogActionData_t rt = SteamInput()->GetAnalogActionData(controller, right_trigger_);
|
||||||
|
any_active = any_active || ls.bActive || rs.bActive || lt.bActive || rt.bActive;
|
||||||
|
|
||||||
|
st.thumb_lx = to_axis(ls.x);
|
||||||
|
st.thumb_ly = to_axis(ls.y);
|
||||||
|
st.thumb_rx = to_axis(rs.x);
|
||||||
|
st.thumb_ry = to_axis(rs.y);
|
||||||
|
st.left_trigger = to_trigger(lt.x);
|
||||||
|
st.right_trigger = to_trigger(rt.x);
|
||||||
|
|
||||||
|
// No action is bound/active (e.g. the controller isn't using our manifest) ->
|
||||||
|
// let the XInput fallback handle this slot instead of reporting an empty pad.
|
||||||
|
if (!any_active)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::uint32_t s_packet = 0;
|
||||||
|
st.packet = ++s_packet;
|
||||||
|
out.connected = true;
|
||||||
|
out.state = st;
|
||||||
|
out.source = "Steam Input";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SteamInputSource::poll()
|
||||||
|
{
|
||||||
|
// XInput first so every slot has a fallback value; Steam overrides where active.
|
||||||
|
xinput_.poll();
|
||||||
|
pads_ = xinput_.pads();
|
||||||
|
|
||||||
|
if (!steam_ready_)
|
||||||
|
{
|
||||||
|
steam_count_ = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SteamAPI_RunCallbacks();
|
||||||
|
InputHandle_t handles[STEAM_INPUT_MAX_COUNT] = {};
|
||||||
|
steam_count_ = SteamInput()->GetConnectedControllers(handles);
|
||||||
|
|
||||||
|
for (int i = 0; i < steam_count_ && i < static_cast<int>(kMaxPads); ++i)
|
||||||
|
{
|
||||||
|
PadInfo steam_pad;
|
||||||
|
if (read_steam_pad(handles[i], steam_pad))
|
||||||
|
{
|
||||||
|
pads_[i] = steam_pad; // Steam controller active on this slot -> use it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace coop
|
||||||
72
host/src/input/steam_input_source.hpp
Normal file
72
host/src/input/steam_input_source.hpp
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// Receives guest controllers through the Steam Input API (action-based) -- the
|
||||||
|
// plan's "primary" input path -- and falls back to XInput per slot for any slot
|
||||||
|
// Steam Input doesn't cover. If SteamAPI / Steam Input can't initialize (e.g. the
|
||||||
|
// host isn't running under Steam, or no action manifest binds), this transparently
|
||||||
|
// behaves as plain XInput, so it's always safe to use.
|
||||||
|
//
|
||||||
|
// Compiled only when the host is built with the Steamworks SDK (COOP_WITH_STEAM).
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "input/input_source.hpp"
|
||||||
|
#include "input/xinput_source.hpp"
|
||||||
|
|
||||||
|
namespace coop
|
||||||
|
{
|
||||||
|
|
||||||
|
// Number of digital (button) actions in the bundled action manifest.
|
||||||
|
inline constexpr int kSteamButtonActions = 15;
|
||||||
|
|
||||||
|
class SteamInputSource final : public InputSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
~SteamInputSource() override;
|
||||||
|
|
||||||
|
// Initializes SteamAPI + Steam Input and loads the action manifest at the given
|
||||||
|
// absolute path. Returns true if Steam Input is live; false means it will run
|
||||||
|
// as XInput-only (poll() still works either way).
|
||||||
|
bool init(const std::string& manifest_absolute_path);
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
|
[[nodiscard]] const char* name() const override
|
||||||
|
{
|
||||||
|
return name_;
|
||||||
|
}
|
||||||
|
void poll() override;
|
||||||
|
[[nodiscard]] const std::array<PadInfo, kMaxPads>& pads() const override
|
||||||
|
{
|
||||||
|
return pads_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool steam_active() const
|
||||||
|
{
|
||||||
|
return steam_ready_;
|
||||||
|
}
|
||||||
|
[[nodiscard]] int steam_controllers() const
|
||||||
|
{
|
||||||
|
return steam_count_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void resolve_handles();
|
||||||
|
bool read_steam_pad(std::uint64_t controller, PadInfo& out) const;
|
||||||
|
|
||||||
|
XInputSource xinput_; // fallback for slots Steam Input doesn't fill
|
||||||
|
std::array<PadInfo, kMaxPads> pads_;
|
||||||
|
|
||||||
|
bool steam_ready_ = false;
|
||||||
|
int steam_count_ = 0;
|
||||||
|
const char* name_ = "XInput";
|
||||||
|
|
||||||
|
std::uint64_t action_set_ = 0;
|
||||||
|
std::uint64_t button_handles_[kSteamButtonActions] = {};
|
||||||
|
std::uint64_t left_stick_ = 0;
|
||||||
|
std::uint64_t right_stick_ = 0;
|
||||||
|
std::uint64_t left_trigger_ = 0;
|
||||||
|
std::uint64_t right_trigger_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace coop
|
||||||
@@ -23,9 +23,31 @@
|
|||||||
#include "log_panel.hpp"
|
#include "log_panel.hpp"
|
||||||
#include "ui/app_chrome.hpp"
|
#include "ui/app_chrome.hpp"
|
||||||
|
|
||||||
|
#ifdef COOP_WITH_STEAM
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "input/steam_input_source.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace
|
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
|
// 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
|
// the way back. The window is borderless and non-interactive so it never steals a
|
||||||
// click or a frame from the mirror underneath.
|
// click or a frame from the mirror underneath.
|
||||||
@@ -64,7 +86,18 @@ int run()
|
|||||||
return 1;
|
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::ControllersPanel controllers;
|
||||||
coop::InjectionPanel injection;
|
coop::InjectionPanel injection;
|
||||||
coop::AudioPanel audio;
|
coop::AudioPanel audio;
|
||||||
|
|||||||
59
tools/steam_input_probe/main.cpp
Normal file
59
tools/steam_input_probe/main.cpp
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// coop_steam_input_probe -- console smoke test for SteamInputSource. Brings up
|
||||||
|
// Steam Input via the shipping host code (no GUI), prints whether it initialized,
|
||||||
|
// how many Steam controllers are connected, and a few polled snapshots, then
|
||||||
|
// exits. Needs Steam running and an appid context: either launch it under Steam,
|
||||||
|
// or drop a steam_appid.txt (e.g. "480") next to the exe. Built only when the
|
||||||
|
// Steamworks SDK is vendored.
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "input/steam_input_source.hpp"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::string 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";
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
coop::SteamInputSource src;
|
||||||
|
const bool ok = src.init(manifest_path());
|
||||||
|
std::printf("init=%d steam_active=%d backend=\"%s\"\n", ok ? 1 : 0, src.steam_active() ? 1 : 0,
|
||||||
|
src.name());
|
||||||
|
|
||||||
|
for (int frame = 0; frame < 20; ++frame)
|
||||||
|
{
|
||||||
|
src.poll();
|
||||||
|
Sleep(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("steam_controllers=%d\n", src.steam_controllers());
|
||||||
|
const auto& pads = src.pads();
|
||||||
|
for (std::uint32_t i = 0; i < coop::kMaxPads; ++i)
|
||||||
|
{
|
||||||
|
const coop::PadInfo& p = pads[i];
|
||||||
|
if (p.connected)
|
||||||
|
{
|
||||||
|
std::printf(" slot %u: source=\"%s\" buttons=0x%04X LX=%d LY=%d LT=%u RT=%u\n", i,
|
||||||
|
p.source.c_str(), p.state.buttons, p.state.thumb_lx, p.state.thumb_ly,
|
||||||
|
p.state.left_trigger, p.state.right_trigger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
src.shutdown();
|
||||||
|
std::printf("done.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user