Restructure bin/: deployables at root, tests and tools in subfolders
Only the shipping artifacts (coop_host.exe, coop_hook.dll, coop_hook_x86.dll, coop_inject_x86.exe, steam_api64.dll, steam_input_actions.vdf) now land in the bin/<config>/ root, so it can be copied wholesale into a donor game folder. Test exes (plus the coop_tone fixture) build into bin/<config>/tests/ and the dev probes into bin/<config>/tools/, via a new coop_output_subdir() CMake helper. The probes resolve coop_hook.dll / the x86 injector from the deployable root one level up (new common/coop/tool_paths.hpp: deployed_artifact_path checks next-to-exe then parent). coop_tone is co-located with the tests so audio_loopback_test's "spawn coop_tone.exe next to me" lookup is unchanged. Verified from a clean bin/: root holds only deployables; ctest x64 7/7 and x86 3/3 green (incl. audio_loopback_test driving coop_tone from tests/). Also convert the roadmap Planned list to bullets and drop this (now-done) item. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,17 @@ set(COOP_OUTPUT_DIR "${CMAKE_SOURCE_DIR}/bin/$<CONFIG>")
|
|||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${COOP_OUTPUT_DIR}")
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${COOP_OUTPUT_DIR}")
|
||||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${COOP_OUTPUT_DIR}")
|
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${COOP_OUTPUT_DIR}")
|
||||||
|
|
||||||
|
# Only the deployable artifacts (host, hook DLLs, injector helper, Steam files) live
|
||||||
|
# in the bin/<config>/ root, so it can be copied wholesale into a donor game folder.
|
||||||
|
# Tests and dev tools are staged into subfolders via this helper instead.
|
||||||
|
# Usage: coop_output_subdir(<subdir> target1 [target2 ...]).
|
||||||
|
function(coop_output_subdir subdir)
|
||||||
|
foreach(tgt IN LISTS ARGN)
|
||||||
|
set_target_properties(${tgt} PROPERTIES
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${COOP_OUTPUT_DIR}/${subdir}")
|
||||||
|
endforeach()
|
||||||
|
endfunction()
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
add_compile_options(/W4 /permissive- /Zc:__cplusplus /utf-8 /MP)
|
add_compile_options(/W4 /permissive- /Zc:__cplusplus /utf-8 /MP)
|
||||||
add_compile_definitions(UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX)
|
add_compile_definitions(UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX)
|
||||||
@@ -86,6 +97,10 @@ if(COOP_X86_HELPER_BUILD)
|
|||||||
target_compile_definitions(audio_hook_test_x86 PRIVATE NTDDI_VERSION=0x0A00000B)
|
target_compile_definitions(audio_hook_test_x86 PRIVATE NTDDI_VERSION=0x0A00000B)
|
||||||
target_link_libraries(audio_hook_test_x86 PRIVATE coop_common safetyhook::safetyhook ole32 mmdevapi)
|
target_link_libraries(audio_hook_test_x86 PRIVATE coop_common safetyhook::safetyhook ole32 mmdevapi)
|
||||||
add_test(NAME audio_hook_test_x86 COMMAND audio_hook_test_x86)
|
add_test(NAME audio_hook_test_x86 COMMAND audio_hook_test_x86)
|
||||||
|
|
||||||
|
# x86 test exes go in tests/; the x86 hook DLL + injector helper stay in the
|
||||||
|
# deployable root (set via CMAKE_RUNTIME_OUTPUT_DIRECTORY above).
|
||||||
|
coop_output_subdir(tests hook_selftest_x86 present_hook_test_x86 audio_hook_test_x86)
|
||||||
return()
|
return()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|||||||
70
README.md
70
README.md
@@ -75,25 +75,7 @@ is removed from this list once done — so the top item is always next. The
|
|||||||
self-verifiable tooling / UI / input items come first; the game-pipeline items that
|
self-verifiable tooling / UI / input items come first; the game-pipeline items that
|
||||||
need a real game (and Remote Play) to fully validate come last.
|
need a real game (and Remote Play) to fully validate come last.
|
||||||
|
|
||||||
1. **Stage only deployable artifacts directly under `bin/`.** Today every target —
|
- **Detect a terminated target and reflect it in the UI.** The Injection panel keeps
|
||||||
host, both hook DLLs, the injector helper, tests, probes, `coop_tone` — lands in
|
|
||||||
`bin/<config>/`, so deploying to a donor folder means hand-picking files. Keep
|
|
||||||
the **deployable set** at the `bin/<config>/` root (`coop_host.exe`,
|
|
||||||
`coop_hook.dll`, `coop_hook_x86.dll`, `coop_inject_x86.exe`, and — when Steam is
|
|
||||||
built — `steam_api64.dll` + `steam_input_actions.vdf`) and push everything else
|
|
||||||
into subfolders: tests → `bin/<config>/tests/`, debug/probe tools
|
|
||||||
(`coop_audio_probe`, `coop_input_probe`, `coop_tone`, `coop_steam_input_probe`) →
|
|
||||||
`bin/<config>/tools/`. *How:* give those targets a per-target
|
|
||||||
`RUNTIME_OUTPUT_DIRECTORY[_<CONFIG>]` (the global `CMAKE_RUNTIME_OUTPUT_DIRECTORY`
|
|
||||||
stays the deployable root; a small helper or `set_target_properties` overrides the
|
|
||||||
non-deployable ones). The x86 sub-build must still stage `coop_hook_x86.dll` +
|
|
||||||
`coop_inject_x86.exe` into the deployable root while its x86 *test* exes go to
|
|
||||||
`tests/`. Watch the cross-target paths: `audio_loopback_test` spawns
|
|
||||||
`coop_tone.exe`, and the host's post-build copy of `steam_api64.dll` / the `.vdf`
|
|
||||||
must follow the host. End result: copying `bin/<config>/` non-recursively yields a
|
|
||||||
clean deployable bundle.
|
|
||||||
|
|
||||||
2. **Detect a terminated target and reflect it in the UI.** The Injection panel keeps
|
|
||||||
showing "Attached" after the game exits. Add a **Terminated** state: the host
|
showing "Attached" after the game exits. Add a **Terminated** state: the host
|
||||||
already knows the target pid and tracks a DLL heartbeat (`InjectionPanel`); on top
|
already knows the target pid and tracks a DLL heartbeat (`InjectionPanel`); on top
|
||||||
of that, hold the `OpenProcess` handle from injection (or re-open with
|
of that, hold the `OpenProcess` handle from injection (or re-open with
|
||||||
@@ -105,16 +87,14 @@ need a real game (and Remote Play) to fully validate come last.
|
|||||||
process isn't proof it's running — also flag a **stalled heartbeat** (no advance
|
process isn't proof it's running — also flag a **stalled heartbeat** (no advance
|
||||||
for ~2 s while the process still exists) as a distinct "hung / not responding"
|
for ~2 s while the process still exists) as a distinct "hung / not responding"
|
||||||
state, since games here can freeze without exiting.
|
state, since games here can freeze without exiting.
|
||||||
|
- **Re-attach to a relaunched target.** A killed-and-relaunched game gets a new pid,
|
||||||
3. **Re-attach to a relaunched target.** A killed-and-relaunched game gets a new pid,
|
but the UI still holds the stale one. In the Terminated state, remember the
|
||||||
but the UI still holds the stale one. In the Terminated state (task 2), remember
|
target's image name (the panel already keeps the selected exe name) and offer a
|
||||||
the target's image name (the panel already keeps the selected exe name) and offer a
|
|
||||||
**Re-attach** button that injects only if a live process with that *same name*
|
**Re-attach** button that injects only if a live process with that *same name*
|
||||||
exists, rebinding the IPC server to the new pid via the existing `inject_dll` path.
|
exists, rebinding the IPC server to the new pid via the existing `inject_dll` path.
|
||||||
If several live processes share that name, don't guess — surface the matches in the
|
If several live processes share that name, don't guess — surface the matches in the
|
||||||
picker (task 4) for a manual choice.
|
window/process picker for a manual choice.
|
||||||
|
- **Select targets by window, not just process.** A flat process list is fine as an
|
||||||
4. **Select targets by window, not just process.** A flat process list is fine as an
|
|
||||||
advanced/debug view, but the default should be a **window list** — there are far
|
advanced/debug view, but the default should be a **window list** — there are far
|
||||||
fewer top-level windows than processes, and a window directly yields the HWND the
|
fewer top-level windows than processes, and a window directly yields the HWND the
|
||||||
WGC capturer and focus spoof already want. *How:* add a window enumerator
|
WGC capturer and focus spoof already want. *How:* add a window enumerator
|
||||||
@@ -124,8 +104,7 @@ need a real game (and Remote Play) to fully validate come last.
|
|||||||
image name. Show **title + process name + pid** with a filter box like the process
|
image name. Show **title + process name + pid** with a filter box like the process
|
||||||
list; injecting by window injects into its pid and hands the HWND straight to
|
list; injecting by window injects into its pid and hands the HWND straight to
|
||||||
capture. Keep the process list behind "Debug details" as the advanced path.
|
capture. Keep the process list behind "Debug details" as the advanced path.
|
||||||
|
- **Auto-size and lay out the overlay windows so none need manual resizing.** Panels
|
||||||
5. **Auto-size and lay out the overlay windows so none need manual resizing.** Panels
|
|
||||||
currently `Begin` at default cascade positions, so they overlap and clip. Give each
|
currently `Begin` at default cascade positions, so they overlap and clip. Give each
|
||||||
`ImGuiWindowFlags_AlwaysAutoResize` and an initial position computed from
|
`ImGuiWindowFlags_AlwaysAutoResize` and an initial position computed from
|
||||||
`ImGui::GetMainViewport()->WorkPos/WorkSize`, applied with `ImGuiCond_FirstUseEver`
|
`ImGui::GetMainViewport()->WorkPos/WorkSize`, applied with `ImGuiCond_FirstUseEver`
|
||||||
@@ -136,23 +115,20 @@ need a real game (and Remote Play) to fully validate come last.
|
|||||||
stream). Auto-resize fits these because they're all control/debug panels — the live
|
stream). Auto-resize fits these because they're all control/debug panels — the live
|
||||||
mirror image is drawn to the whole host window *behind* the overlay, not inside a
|
mirror image is drawn to the whole host window *behind* the overlay, not inside a
|
||||||
panel.
|
panel.
|
||||||
|
- **Fix the Audio panel "live" column.** It overlays a green dot and grey "idle"
|
||||||
6. **Fix the Audio panel "live" column.** It overlays a green dot and grey "idle"
|
|
||||||
because liveness is recomputed each frame from the per-stream `frames_rendered`
|
because liveness is recomputed each frame from the per-stream `frames_rendered`
|
||||||
delta, which is zero on most frames (buffers release in bursts), so it flickers.
|
delta, which is zero on most frames (buffers release in bursts), so it flickers.
|
||||||
Replace it with a **debounced activity indicator**: keep a per-stream "last
|
Replace it with a **debounced activity indicator**: keep a per-stream "last
|
||||||
advanced" timestamp (the panel already stores the previous frame counts) and show
|
advanced" timestamp (the panel already stores the previous frame counts) and show
|
||||||
**live** if frames advanced within the last ~300–500 ms, else **idle** — optionally
|
**live** if frames advanced within the last ~300–500 ms, else **idle** — optionally
|
||||||
a small frames/s or activity bar so multi-stream games read clearly.
|
a small frames/s or activity bar so multi-stream games read clearly.
|
||||||
|
- **Move the synthetic-input toggle to the Controllers panel, under Debug details.**
|
||||||
7. **Move the synthetic-input toggle to the Controllers panel, under Debug details.**
|
|
||||||
The "Forward synthetic test input" checkbox is a controller-debugging aid, so move
|
The "Forward synthetic test input" checkbox is a controller-debugging aid, so move
|
||||||
it out of the Injection panel into `ControllersPanel` and gate it behind
|
it out of the Injection panel into `ControllersPanel` and gate it behind
|
||||||
`debug_details`. The publish path is unchanged (the Injection panel already
|
`debug_details`. The publish path is unchanged (the Injection panel already
|
||||||
substitutes the synthetic pattern in `publish`); the flag just moves with it (or is
|
substitutes the synthetic pattern in `publish`); the flag just moves with it (or is
|
||||||
passed from Controllers into the publish call).
|
passed from Controllers into the publish call).
|
||||||
|
- **Mouse & keyboard forwarding (messages + polling-state hooks).** Forward guest
|
||||||
8. **Mouse & keyboard forwarding (messages + polling-state hooks).** Forward guest
|
|
||||||
clicks and keystrokes into the unfocused game via a new **MKB hook subsystem** in
|
clicks and keystrokes into the unfocused game via a new **MKB hook subsystem** in
|
||||||
`coop_hook.dll` — the toggle *is* the hook (not installed → no forwarding).
|
`coop_hook.dll` — the toggle *is* the hook (not installed → no forwarding).
|
||||||
*Delivery:* `PostMessage` window-message input (`WM_KEYDOWN`/`WM_KEYUP`/`WM_CHAR`,
|
*Delivery:* `PostMessage` window-message input (`WM_KEYDOWN`/`WM_KEYUP`/`WM_CHAR`,
|
||||||
@@ -169,8 +145,7 @@ need a real game (and Remote Play) to fully validate come last.
|
|||||||
doesn't want the event (`ImGuiIO::WantCaptureMouse` / `WantCaptureKeyboard`), so
|
doesn't want the event (`ImGuiIO::WantCaptureMouse` / `WantCaptureKeyboard`), so
|
||||||
interacting with the overlay's own windows never leaks input into the game. The host
|
interacting with the overlay's own windows never leaks input into the game. The host
|
||||||
sends MKB events to the hook over a new (or extended) IPC region.
|
sends MKB events to the hook over a new (or extended) IPC region.
|
||||||
|
- **Rumble / haptics forwarding (both backends).** Currently unsupported — the XInput
|
||||||
9. **Rumble / haptics forwarding (both backends).** Currently unsupported — the XInput
|
|
||||||
hook swallows `XInputSetState`. Add a reverse path: the hook captures the game's
|
hook swallows `XInputSetState`. Add a reverse path: the hook captures the game's
|
||||||
`XInputSetState` (left/right motor) and publishes it over a hook→host channel (the
|
`XInputSetState` (left/right motor) and publishes it over a hook→host channel (the
|
||||||
back-channel already exists), and the host drives the guest's actuators per backend —
|
back-channel already exists), and the host drives the guest's actuators per backend —
|
||||||
@@ -178,8 +153,7 @@ need a real game (and Remote Play) to fully validate come last.
|
|||||||
whether Steam's RPT virtual pad accepts vibration and routes it to the guest);
|
whether Steam's RPT virtual pad accepts vibration and routes it to the guest);
|
||||||
**Steam Input:** `SteamInput()->TriggerVibration` / `Legacy_TriggerHapticPulse`.
|
**Steam Input:** `SteamInput()->TriggerVibration` / `Legacy_TriggerHapticPulse`.
|
||||||
Map each guest slot to the right actuator.
|
Map each guest slot to the right actuator.
|
||||||
|
- **Per-backend input debug visualization.** To separate "wrong input *into* the
|
||||||
10. **Per-backend input debug visualization.** To separate "wrong input *into* the
|
|
||||||
tool" from "wrong input *out to* the game", show three distinct views in the
|
tool" from "wrong input *out to* the game", show three distinct views in the
|
||||||
Controllers panel (under Debug details): (a) **received via XInput** (raw
|
Controllers panel (under Debug details): (a) **received via XInput** (raw
|
||||||
`XInputSource` state), (b) **received via Steam Input** (raw `SteamInputSource`
|
`XInputSource` state), (b) **received via Steam Input** (raw `SteamInputSource`
|
||||||
@@ -188,8 +162,7 @@ need a real game (and Remote Play) to fully validate come last.
|
|||||||
the game actually read back via the hook's per-slot channel). The hook already
|
the game actually read back via the hook's per-slot channel). The hook already
|
||||||
reports per-slot poll counts; extend it to echo the last state the game read so (c)
|
reports per-slot poll counts; extend it to echo the last state the game read so (c)
|
||||||
is a true round-trip.
|
is a true round-trip.
|
||||||
|
- **Release the mouse cursor for cursor-clipping games.** Games that confine the
|
||||||
11. **Release the mouse cursor for cursor-clipping games.** Games that confine the
|
|
||||||
cursor while focused (e.g. Trails through Daybreak via `ClipCursor` / per-frame
|
cursor while focused (e.g. Trails through Daybreak via `ClipCursor` / per-frame
|
||||||
`SetCursorPos` re-centering) trap the operator's mouse permanently, because the
|
`SetCursorPos` re-centering) trap the operator's mouse permanently, because the
|
||||||
focus spoof makes the game believe it's always focused — so the operator can't
|
focus spoof makes the game believe it's always focused — so the operator can't
|
||||||
@@ -198,8 +171,7 @@ need a real game (and Remote Play) to fully validate come last.
|
|||||||
re-centering `SetCursorPos`, gated by a new host→hook flag driven by a host
|
re-centering `SetCursorPos`, gated by a new host→hook flag driven by a host
|
||||||
toggle + hotkey. Defaults to released (the guest plays via the pad, so the game's
|
toggle + hotkey. Defaults to released (the guest plays via the pad, so the game's
|
||||||
own cursor clip is operator-only), with the option to re-enable clipping per game.
|
own cursor clip is operator-only), with the option to re-enable clipping per game.
|
||||||
|
- **Real capture metrics + latency stats.** The current FPS readout only measures
|
||||||
12. **Real capture metrics + latency stats.** The current FPS readout only measures
|
|
||||||
how fast the host renders its own window, which hides capture stutter. Add a
|
how fast the host renders its own window, which hides capture stutter. Add a
|
||||||
three-line frametime/FPS graph — **game present rate** (from `VideoShare` present
|
three-line frametime/FPS graph — **game present rate** (from `VideoShare` present
|
||||||
deltas), **capture rate** (generation deltas / WGC arrivals), and **tool render
|
deltas), **capture rate** (generation deltas / WGC arrivals), and **tool render
|
||||||
@@ -207,16 +179,14 @@ need a real game (and Remote Play) to fully validate come last.
|
|||||||
a `QueryPerformanceCounter` value in `VideoShare`, and the host reports
|
a `QueryPerformanceCounter` value in `VideoShare`, and the host reports
|
||||||
`host-present QPC − game-present QPC` (min/avg/max ms) for the matched frame. QPC
|
`host-present QPC − game-present QPC` (min/avg/max ms) for the matched frame. QPC
|
||||||
is system-wide, so the two processes' timestamps compare directly.
|
is system-wide, so the two processes' timestamps compare directly.
|
||||||
|
- **DX12 hooked capture (Spider-Man: Miles Morales).** Miles Morales is D3D12, so
|
||||||
13. **DX12 hooked capture (Spider-Man: Miles Morales).** Miles Morales is D3D12, so
|
|
||||||
the Present hook fires but `GetBuffer(0)` as `ID3D11Texture2D` fails (the
|
the Present hook fires but `GetBuffer(0)` as `ID3D11Texture2D` fails (the
|
||||||
backbuffer is an `ID3D12Resource`) and the hook idles; WGC works but stutters. Add
|
backbuffer is an `ID3D12Resource`) and the hook idles; WGC works but stutters. Add
|
||||||
a D3D12 path via a **D3D11On12 bridge**: capture the game's D3D12 command queue
|
a D3D12 path via a **D3D11On12 bridge**: capture the game's D3D12 command queue
|
||||||
(hook `ID3D12CommandQueue::ExecuteCommandLists`), create an `ID3D11On12Device`,
|
(hook `ID3D12CommandQueue::ExecuteCommandLists`), create an `ID3D11On12Device`,
|
||||||
`CreateWrappedResource` around the backbuffer, and `CopyResource` into the
|
`CreateWrappedResource` around the backbuffer, and `CopyResource` into the
|
||||||
*existing* D3D11 shared keyed-mutex texture — so the host side is unchanged.
|
*existing* D3D11 shared keyed-mutex texture — so the host side is unchanged.
|
||||||
|
- **Multi-stream audio capture + mixing, with per-stream format detection.** Games
|
||||||
14. **Multi-stream audio capture + mixing, with per-stream format detection.** Games
|
|
||||||
with several concurrent WASAPI render streams (e.g. Miles Morales) only get their
|
with several concurrent WASAPI render streams (e.g. Miles Morales) only get their
|
||||||
first ("primary") stream mirrored today; the rest keep playing locally and never
|
first ("primary") stream mirrored today; the rest keep playing locally and never
|
||||||
reach the guest. Capture every tracked render stream into its own shared ring,
|
reach the guest. Capture every tracked render stream into its own shared ring,
|
||||||
@@ -332,9 +302,11 @@ input layer sees a real state change. `disable_mask` (hex bits `0x1`=input
|
|||||||
Present-hook crash was isolated.
|
Present-hook crash was isolated.
|
||||||
|
|
||||||
Both auto-detect a 32-bit (WOW64) target and inject via `coop_inject_x86.exe` +
|
Both auto-detect a 32-bit (WOW64) target and inject via `coop_inject_x86.exe` +
|
||||||
`coop_hook_x86.dll`, exactly like the host. Run them from `bin/<config>/`.
|
`coop_hook_x86.dll`, exactly like the host. The probes build into
|
||||||
**Kill the game between runs** — the loaded DLL locks `coop_hook.dll` against the
|
`bin/<config>/tools/` (the deployable `bin/<config>/` root holds only shipping
|
||||||
next rebuild.
|
artifacts; tests build into `bin/<config>/tests/`) and resolve `coop_hook.dll` from
|
||||||
|
the root one level up, so run them from there. **Kill the game between runs** — the
|
||||||
|
loaded DLL locks `coop_hook.dll` against the next rebuild.
|
||||||
|
|
||||||
## Running the tool (manual, end-to-end)
|
## Running the tool (manual, end-to-end)
|
||||||
|
|
||||||
|
|||||||
52
common/include/coop/tool_paths.hpp
Normal file
52
common/include/coop/tool_paths.hpp
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// Resolve artifacts that ship in the deployable bin/<config>/ root even when the
|
||||||
|
// running executable lives in a sibling subfolder (e.g. dev tools staged under
|
||||||
|
// bin/<config>/tools/). The probes use this to find coop_hook.dll and the x86
|
||||||
|
// injector helper, which stay at the root while the probes themselves do not.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
namespace coop
|
||||||
|
{
|
||||||
|
|
||||||
|
// Directory of the current executable, with a trailing separator.
|
||||||
|
inline std::wstring exe_directory()
|
||||||
|
{
|
||||||
|
wchar_t buf[MAX_PATH] = {};
|
||||||
|
GetModuleFileNameW(nullptr, buf, MAX_PATH);
|
||||||
|
std::wstring path(buf);
|
||||||
|
const std::size_t slash = path.find_last_of(L"\\/");
|
||||||
|
return slash == std::wstring::npos ? std::wstring() : path.substr(0, slash + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full path to a deployed artifact `name`: prefer one next to the running exe, else
|
||||||
|
// one directory up (the deployable root, when the exe runs from a tools/ subfolder).
|
||||||
|
// Falls back to the next-to-exe path so callers can report a sensible "not found".
|
||||||
|
inline std::wstring deployed_artifact_path(const wchar_t* name)
|
||||||
|
{
|
||||||
|
const std::wstring here = exe_directory() + name;
|
||||||
|
if (GetFileAttributesW(here.c_str()) != INVALID_FILE_ATTRIBUTES)
|
||||||
|
{
|
||||||
|
return here;
|
||||||
|
}
|
||||||
|
std::wstring dir = exe_directory();
|
||||||
|
if (!dir.empty())
|
||||||
|
{
|
||||||
|
dir.pop_back(); // drop the trailing separator before going up a level
|
||||||
|
}
|
||||||
|
const std::size_t slash = dir.find_last_of(L"\\/");
|
||||||
|
if (slash != std::wstring::npos)
|
||||||
|
{
|
||||||
|
const std::wstring up = dir.substr(0, slash + 1) + name;
|
||||||
|
if (GetFileAttributesW(up.c_str()) != INVALID_FILE_ATTRIBUTES)
|
||||||
|
{
|
||||||
|
return up;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return here;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace coop
|
||||||
@@ -76,6 +76,8 @@ if(EXISTS "${COOP_STEAM_SDK_DIR}/public/steam/steam_api.h")
|
|||||||
coop_common
|
coop_common
|
||||||
xinput
|
xinput
|
||||||
"${COOP_STEAM_SDK_DIR}/redistributable_bin/win64/steam_api64.lib")
|
"${COOP_STEAM_SDK_DIR}/redistributable_bin/win64/steam_api64.lib")
|
||||||
|
# Dev tool -> bin/<config>/tools/ (its steam_api64.dll copy follows via TARGET_FILE_DIR).
|
||||||
|
coop_output_subdir(tools coop_steam_input_probe)
|
||||||
add_custom_command(TARGET coop_steam_input_probe POST_BUILD
|
add_custom_command(TARGET coop_steam_input_probe POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
"${COOP_STEAM_SDK_DIR}/redistributable_bin/win64/steam_api64.dll"
|
"${COOP_STEAM_SDK_DIR}/redistributable_bin/win64/steam_api64.dll"
|
||||||
|
|||||||
@@ -107,3 +107,15 @@ target_link_libraries(opengl_hook_test PRIVATE
|
|||||||
gdi32)
|
gdi32)
|
||||||
|
|
||||||
add_test(NAME opengl_hook_test COMMAND opengl_hook_test)
|
add_test(NAME opengl_hook_test COMMAND opengl_hook_test)
|
||||||
|
|
||||||
|
# Keep the bin/<config>/ root deployable: stage every test exe under tests/. coop_tone
|
||||||
|
# (the audio_loopback_test fixture) is staged there too, next to its consumer, so the
|
||||||
|
# test's "coop_tone.exe alongside me" lookup keeps working.
|
||||||
|
coop_output_subdir(tests
|
||||||
|
hook_selftest
|
||||||
|
audio_ring_test
|
||||||
|
audio_loopback_test
|
||||||
|
audio_hook_test
|
||||||
|
srgb_format_test
|
||||||
|
present_hook_test
|
||||||
|
opengl_hook_test)
|
||||||
|
|||||||
@@ -4,3 +4,4 @@
|
|||||||
add_executable(coop_audio_probe main.cpp)
|
add_executable(coop_audio_probe main.cpp)
|
||||||
target_link_libraries(coop_audio_probe PRIVATE coop_common)
|
target_link_libraries(coop_audio_probe PRIVATE coop_common)
|
||||||
set_target_properties(coop_audio_probe PROPERTIES OUTPUT_NAME "coop_audio_probe")
|
set_target_properties(coop_audio_probe PROPERTIES OUTPUT_NAME "coop_audio_probe")
|
||||||
|
coop_output_subdir(tools coop_audio_probe) # dev tool -> bin/<config>/tools/
|
||||||
|
|||||||
@@ -23,21 +23,16 @@
|
|||||||
#include "coop/log_ring.hpp"
|
#include "coop/log_ring.hpp"
|
||||||
#include "coop/protocol.hpp"
|
#include "coop/protocol.hpp"
|
||||||
#include "coop/shared_memory.hpp"
|
#include "coop/shared_memory.hpp"
|
||||||
|
#include "coop/tool_paths.hpp"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
|
// coop_hook.dll ships in the deployable bin/<config>/ root; this probe runs from
|
||||||
|
// bin/<config>/tools/, so resolve next-to-self first, then one level up.
|
||||||
std::wstring dll_path_next_to_self()
|
std::wstring dll_path_next_to_self()
|
||||||
{
|
{
|
||||||
wchar_t exe[MAX_PATH] = {};
|
return coop::deployed_artifact_path(L"coop_hook.dll");
|
||||||
GetModuleFileNameW(nullptr, exe, MAX_PATH);
|
|
||||||
std::wstring path(exe);
|
|
||||||
const size_t slash = path.find_last_of(L"\\/");
|
|
||||||
if (slash != std::wstring::npos)
|
|
||||||
{
|
|
||||||
path.resize(slash + 1);
|
|
||||||
}
|
|
||||||
return path + L"coop_hook.dll";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::wstring sibling_of(const std::wstring& path, const wchar_t* name)
|
std::wstring sibling_of(const std::wstring& path, const wchar_t* name)
|
||||||
|
|||||||
@@ -3,3 +3,7 @@
|
|||||||
add_executable(coop_tone main.cpp)
|
add_executable(coop_tone main.cpp)
|
||||||
target_link_libraries(coop_tone PRIVATE ole32)
|
target_link_libraries(coop_tone PRIVATE ole32)
|
||||||
set_target_properties(coop_tone PROPERTIES OUTPUT_NAME "coop_tone")
|
set_target_properties(coop_tone PROPERTIES OUTPUT_NAME "coop_tone")
|
||||||
|
|
||||||
|
# Fixture for audio_loopback_test -> stage it next to the tests, not in the
|
||||||
|
# deployable root. audio_loopback_test spawns coop_tone.exe from its own directory.
|
||||||
|
coop_output_subdir(tests coop_tone)
|
||||||
|
|||||||
@@ -5,3 +5,4 @@
|
|||||||
add_executable(coop_input_probe main.cpp)
|
add_executable(coop_input_probe main.cpp)
|
||||||
target_link_libraries(coop_input_probe PRIVATE coop_common)
|
target_link_libraries(coop_input_probe PRIVATE coop_common)
|
||||||
set_target_properties(coop_input_probe PROPERTIES OUTPUT_NAME "coop_input_probe")
|
set_target_properties(coop_input_probe PROPERTIES OUTPUT_NAME "coop_input_probe")
|
||||||
|
coop_output_subdir(tools coop_input_probe) # dev tool -> bin/<config>/tools/
|
||||||
|
|||||||
@@ -21,21 +21,16 @@
|
|||||||
#include "coop/log_ring.hpp"
|
#include "coop/log_ring.hpp"
|
||||||
#include "coop/protocol.hpp"
|
#include "coop/protocol.hpp"
|
||||||
#include "coop/shared_memory.hpp"
|
#include "coop/shared_memory.hpp"
|
||||||
|
#include "coop/tool_paths.hpp"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
|
// coop_hook.dll ships in the deployable bin/<config>/ root; this probe runs from
|
||||||
|
// bin/<config>/tools/, so resolve next-to-self first, then one level up.
|
||||||
std::wstring dll_path_next_to_self()
|
std::wstring dll_path_next_to_self()
|
||||||
{
|
{
|
||||||
wchar_t exe[MAX_PATH] = {};
|
return coop::deployed_artifact_path(L"coop_hook.dll");
|
||||||
GetModuleFileNameW(nullptr, exe, MAX_PATH);
|
|
||||||
std::wstring path(exe);
|
|
||||||
const size_t slash = path.find_last_of(L"\\/");
|
|
||||||
if (slash != std::wstring::npos)
|
|
||||||
{
|
|
||||||
path.resize(slash + 1);
|
|
||||||
}
|
|
||||||
return path + L"coop_hook.dll";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::wstring sibling_of(const std::wstring& path, const wchar_t* name)
|
std::wstring sibling_of(const std::wstring& path, const wchar_t* name)
|
||||||
|
|||||||
Reference in New Issue
Block a user