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