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:
2026-06-21 00:59:03 +02:00
parent e9d1253b8f
commit 61049a620c
10 changed files with 223 additions and 174 deletions

View File

@@ -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()

View File

@@ -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)

View 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

View File

@@ -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"

View File

@@ -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)

View File

@@ -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/

View File

@@ -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)

View File

@@ -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)

View File

@@ -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/

View File

@@ -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)