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 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 01:08:41 +02:00
parent b3e8a1adc6
commit b0f78312c6
3 changed files with 75 additions and 7 deletions

View File

@@ -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<unsigned char>(x[i])) != ::tolower(static_cast<unsigned char>(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<unsigned long> 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<PadInfo, kMaxPads>& 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();
}