From b00b516cdb9b2a1244fc178c4f85ec3224a6ea77 Mon Sep 17 00:00:00 2001 From: BlackMark Date: Mon, 22 Jun 2026 02:03:13 +0200 Subject: [PATCH] Injection: session-only auto re-attach on relaunch Add an "Auto re-attach this game on relaunch" checkbox (default off, never persisted, shown while attached or terminated). While on and the target has terminated, the host polls the process list for the same image name and re-injects the moment it reappears -- built on the existing re-attach-by-image-name path, so there's no target to pick. The kill+relaunch recovery for a wrong audio format: re-attaching early catches IAudioClient::Initialize and reads the exact format. Validated live (harness): inject coop_tone -> kill -> relaunch -> auto re-injected into the new pid, and early enough that the format came back as Exact. Trim README. Co-Authored-By: Claude Opus 4.8 --- README.md | 9 -------- host/src/injection_panel.cpp | 43 ++++++++++++++++++++++++++++++++++++ host/src/injection_panel.hpp | 11 +++++++++ host/src/main.cpp | 5 +++++ 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1057d74..06e17a3 100644 --- a/README.md +++ b/README.md @@ -92,15 +92,6 @@ and covers anything the hooked path doesn't (Vulkan, D3D9 — see Roadmap). ### Planned (next up) -- **Auto re-attach the same game on relaunch (session-only).** A checkbox on the - attached (or just-terminated) target, default off, *not* persisted, **always visible** - (it's the recommended recovery path, not a diagnostic). While on and the target has - terminated, the host watches the process list for the same image name and injects the - moment it reappears — built on the existing re-attach-by-image-name path, so there's no - target to type. Workflow: if audio came out wrong (late attach forced the guess), tick - the box, kill and relaunch the game, and the host re-attaches *early* — early enough to - catch `IAudioClient::Initialize` and read the exact format. A dependable fallback, not - the intended primary path (hardened first-launch detection should get it right anyway). - **Mock game + stress-test suite.** A bare-bones test game that renders an animated (non-static) pattern — so dropped/duplicated/torn frames are obvious — with a selectable graphics backend (DX11/DX12 now; structured to add OpenGL/Vulkan) and a diff --git a/host/src/injection_panel.cpp b/host/src/injection_panel.cpp index 5990601..3697e37 100644 --- a/host/src/injection_panel.cpp +++ b/host/src/injection_panel.cpp @@ -115,6 +115,37 @@ void InjectionPanel::close_target_handle() void InjectionPanel::tick() { update_liveness(); + auto_reattach_tick(); +} + +void InjectionPanel::auto_reattach_tick() +{ + if (!auto_reattach_ || target_state_ != TargetState::Terminated || selected_name_.empty()) + { + return; + } + // Poll the process list a couple of times a second (cheap, and we want to catch the + // relaunch early to read the exact audio format before the game creates its client). + const double now = ImGui::GetTime(); + if (now - last_auto_poll_ < 0.5) + { + return; + } + last_auto_poll_ = now; + refresh_processes(); + for (const ProcessEntry& e : processes_) + { + if (iequals_name(e.exe_name, selected_name_)) + { + // The same game relaunched -> tear down the stale channel and re-attach to it. + server_.stop(); + close_target_handle(); + injected_ = false; + selected_pid_ = e.pid; + inject_selected(); // logs success/failure in status_; retried next poll on failure + break; + } + } } void InjectionPanel::update_liveness() @@ -538,6 +569,18 @@ void InjectionPanel::draw(bool debug_details) ImGui::SameLine(); ImGui::TextDisabled("(relaunched %s)", narrow(selected_name_).c_str()); } + // Session-only auto re-attach: tick it (while attached or terminated), then kill + + // relaunch the game and it re-injects itself early -- the kill+relaunch fix for a + // wrong audio format, without picking a target again. + if (!selected_name_.empty()) + { + ImGui::Checkbox("Auto re-attach this game on relaunch", &auto_reattach_); + if (auto_reattach_ && target_state_ == TargetState::Terminated) + { + ImGui::SameLine(); + ImGui::TextColored(kGrey, "(watching for %s...)", narrow(selected_name_).c_str()); + } + } ImGui::Separator(); } diff --git a/host/src/injection_panel.hpp b/host/src/injection_panel.hpp index 0484ea3..6313684 100644 --- a/host/src/injection_panel.hpp +++ b/host/src/injection_panel.hpp @@ -49,6 +49,10 @@ public: // Test harness (debug builds only): inject into the first running process whose image // name matches. Returns the pid on success, 0 otherwise. Same path as the UI button. unsigned long dev_inject_by_name(const std::wstring& image_name); + void dev_set_auto_reattach(bool on) + { + auto_reattach_ = on; + } #endif // Forward the latest pad snapshot to the injected hook (if connected). When @@ -162,6 +166,7 @@ private: void refresh_processes(); void inject_selected(); void reattach(); // re-inject a relaunched same-name target (Terminated state) + void auto_reattach_tick(); // poll for the same game relaunching while auto-reattach is on void update_liveness(); // recompute target_state_ from process + heartbeat void close_target_handle(); // close target_process_ and reset liveness state void draw_subsystem_controls(const HookStatusView& status); @@ -193,6 +198,12 @@ private: bool release_cursor_ = true; // free the operator's mouse from the game's clip (default) bool injected_ = false; // a hook DLL is loaded in the target + // Session-only (never persisted): while on and the target has terminated, auto-inject + // the same image name the moment it relaunches, so a quick kill+relaunch re-attaches + // early (catching IAudioClient::Initialize) without the operator picking a target. + bool auto_reattach_ = false; + double last_auto_poll_ = 0.0; // throttle the process-list poll + // Heartbeat liveness tracking (is the injected DLL responding?). std::uint32_t last_heartbeat_ = 0; double last_heartbeat_time_ = 0.0; diff --git a/host/src/main.cpp b/host/src/main.cpp index 2b0a1d4..6b2feb8 100644 --- a/host/src/main.cpp +++ b/host/src/main.cpp @@ -144,6 +144,11 @@ std::string apply_test_command(const std::string& cmd, coop::UiState& ui, coop:: ui.debug_details = (arg(1) == "on"); return "ok"; } + if (v == "autoattach") + { + injection.dev_set_auto_reattach(arg(1) == "on"); + return "ok"; + } if (v == "remeasure") { audio.dev_request_op(num(1), coop::AudioRingOp_Remeasure, 0, 0, 0, 0);