Phase 0: donor-launch spike foundation

Scaffold CoopAllTheThings: Remote Play Together for any XInput game via a
mirror app under a donor appid (real game keeps its own appid, so DRM,
achievements, and playtime stay intact).

- Build: CMake skeleton, ImGui + SafetyHook submodules (no vcpkg)
- common/: host<->hook IPC contract (seqlock pad state, shared-memory RAII)
- host/: borderless D3D11 window + ImGui overlay listing visible XInput pads,
  behind an InputSource interface (Steam Input slots in later)
- README documents the Phase 0 donor-launch validation procedure, anti-cheat
  limitation, and XInput/bitness constraints

Phase 0 validates the riskiest assumption (Steam RPT streams an arbitrary
window under a donor appid and routes guest input to it) before capture and
injection are built.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 22:48:31 +02:00
commit cf058aecfa
23 changed files with 1129 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
#include "debug_overlay.hpp"
#include <imgui.h>
namespace coop
{
namespace
{
struct ButtonBit
{
std::uint16_t mask;
const char* label;
};
// XINPUT_GAMEPAD_* bit values (kept local so this file needn't include Xinput.h).
constexpr ButtonBit kButtons[] = {
{0x0001, "Up"}, {0x0002, "Down"}, {0x0004, "Left"}, {0x0008, "Right"}, {0x0010, "Start"},
{0x0020, "Back"}, {0x0040, "LS"}, {0x0080, "RS"}, {0x0100, "LB"}, {0x0200, "RB"},
{0x1000, "A"}, {0x2000, "B"}, {0x4000, "X"}, {0x8000, "Y"},
};
void draw_pad(int index, const PadInfo& pad)
{
ImGui::PushID(index);
if (!pad.connected)
{
ImGui::TextDisabled("Slot %d: disconnected", index);
ImGui::PopID();
return;
}
ImGui::Text("Slot %d [%s]", index, pad.source.c_str());
ImGui::Text("LT %3u RT %3u", pad.state.left_trigger, pad.state.right_trigger);
ImGui::Text("L (%6d, %6d) R (%6d, %6d)", pad.state.thumb_lx, pad.state.thumb_ly, pad.state.thumb_rx,
pad.state.thumb_ry);
bool first = true;
ImGui::TextUnformatted("Buttons: ");
for (const ButtonBit& b : kButtons)
{
if ((pad.state.buttons & b.mask) != 0)
{
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "%s%s", first ? "" : ", ", b.label);
first = false;
}
}
if (first)
{
ImGui::SameLine();
ImGui::TextDisabled("(none)");
}
ImGui::Separator();
ImGui::PopID();
}
} // namespace
void draw_debug_overlay(const InputSource& input)
{
ImGui::SetNextWindowPos(ImVec2(24, 24), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(420, 0), ImGuiCond_FirstUseEver);
ImGui::Begin("CoopAllTheThings - Phase 0 spike");
ImGui::Text("Input backend: %s", input.name());
ImGui::Text("%.1f FPS (%.2f ms)", ImGui::GetIO().Framerate, 1000.0f / ImGui::GetIO().Framerate);
ImGui::TextDisabled("This window is what Remote Play Together captures.");
ImGui::TextDisabled("Press Esc to quit.");
ImGui::Separator();
const auto& pads = input.pads();
for (int i = 0; i < static_cast<int>(pads.size()); ++i)
{
draw_pad(i, pads[i]);
}
ImGui::End();
}
} // namespace coop