From b0f78312c67c894192a3224e145320bd2b7e99af Mon Sep 17 00:00:00 2001 From: BlackMark Date: Sun, 21 Jun 2026 01:08:41 +0200 Subject: [PATCH] Add Re-attach for a relaunched same-name target In the Terminated state the Injection panel now offers a Re-attach button: it refreshes the process list and finds live processes whose image name matches the original target's (case-insensitive). Exactly one match -> tear down the stale IPC channel and inject into the new pid via the normal path; several matches -> don't guess, filter the picker to the name and prompt the operator to pick one; none -> report it. Saves hunting for a relaunched game's new pid in the list. Verified: x64 build + ctest 7/7 green. Re-attach is a Terminated-state button flow, so its end-to-end behavior is best confirmed live (ImGui clicks can't be scripted); logic reviewed. Co-Authored-By: Claude Opus 4.8 --- README.md | 7 ---- host/src/injection_panel.cpp | 74 ++++++++++++++++++++++++++++++++++++ host/src/injection_panel.hpp | 1 + 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a530a70..6d640e2 100644 --- a/README.md +++ b/README.md @@ -75,13 +75,6 @@ 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 need a real game (and Remote Play) to fully validate come last. -- **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 - 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* - 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 - window/process picker for a manual choice. - **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 fewer top-level windows than processes, and a window directly yields the HWND the diff --git a/host/src/injection_panel.cpp b/host/src/injection_panel.cpp index 6a0e706..4533ad5 100644 --- a/host/src/injection_panel.cpp +++ b/host/src/injection_panel.cpp @@ -28,6 +28,24 @@ std::string narrow(const std::wstring& w) return out; } +// Case-insensitive equality of two image names (e.g. "snb.exe"). +bool iequals_name(const std::wstring& a, const std::wstring& b) +{ + const std::string x = narrow(a), y = narrow(b); + if (x.size() != y.size()) + { + return false; + } + for (std::size_t i = 0; i < x.size(); ++i) + { + if (::tolower(static_cast(x[i])) != ::tolower(static_cast(y[i]))) + { + return false; + } + } + return true; +} + bool contains_ci(const std::wstring& haystack, const char* needle_utf8) { if (needle_utf8 == nullptr || needle_utf8[0] == '\0') @@ -183,6 +201,50 @@ void InjectionPanel::inject_selected() } } +void InjectionPanel::reattach() +{ + if (selected_name_.empty()) + { + return; + } + + // Find live processes that share the original target's image name. + refresh_processes(); + std::vector matches; + for (const ProcessEntry& e : processes_) + { + if (iequals_name(e.exe_name, selected_name_)) + { + matches.push_back(e.pid); + } + } + + const std::string name = narrow(selected_name_); + if (matches.empty()) + { + status_ = "No running \"" + name + "\" to re-attach to."; + status_color_ = kRed; + return; + } + if (matches.size() > 1) + { + // Don't guess which instance: filter the picker to the matches so the operator + // chooses, then injects via the normal button. + snprintf(filter_, sizeof(filter_), "%s", name.c_str()); + selected_pid_ = 0; + status_ = "Multiple \"" + name + "\" running -- pick one below, then Inject & Connect."; + status_color_ = kGrey; + return; + } + + // Exactly one: tear down the stale channel and inject into the new pid. + server_.stop(); + close_target_handle(); + injected_ = false; + selected_pid_ = matches.front(); + inject_selected(); +} + void InjectionPanel::publish(const std::array& pads) { if (!test_input_) @@ -430,6 +492,18 @@ void InjectionPanel::draw(bool debug_details) status_ = "Stopped."; status_color_ = kGrey; } + // A relaunched game has a new pid; re-attach by image name without hunting for + // it in the list. Only offered once the old target is gone. + if (target_state_ == TargetState::Terminated && !selected_name_.empty()) + { + ImGui::SameLine(); + if (ImGui::Button("Re-attach")) + { + reattach(); + } + ImGui::SameLine(); + ImGui::TextDisabled("(relaunched %s)", narrow(selected_name_).c_str()); + } ImGui::Separator(); } diff --git a/host/src/injection_panel.hpp b/host/src/injection_panel.hpp index 93a025e..d154b1c 100644 --- a/host/src/injection_panel.hpp +++ b/host/src/injection_panel.hpp @@ -108,6 +108,7 @@ public: private: void refresh_processes(); void inject_selected(); + void reattach(); // re-inject a relaunched same-name target (Terminated state) 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);