Per-subsystem hook control: install/remove input, focus, audio at runtime
Add a host->hook control channel (protocol v5 -> v6: HookControl in SharedBlock, per-subsystem "disabled" flags, 0 = install so the zero-filled default is unchanged). The worker now reconciles each subsystem every tick: install what's requested-and-missing, remove what's no longer wanted -- so the audio hooks re-attach the ring and republish format on a reinstall, and XInput/focus clear their stale status flags on removal. Injection panel: a checkbox per subsystem (input forwarding / focus spoof / audio render-hook) toggles it at runtime, showing the requested vs actual installed state from the registry, plus DLL heartbeat liveness. The hook-status section now keys off whether a DLL was injected (host-side) rather than the input-hook "attached" flag, so it stays visible with input unhooked. Guards for dependent features: the synthetic-input control is disabled when input forwarding is off, and the Audio panel explains that mirroring uses loopback (echo) when the render-hook is off. Verified against Phantom Brave via coop_audio_probe: starting with audio requested off installs only input+focus (8 hooks, no capture); re-enabling at runtime installs the audio hooks (13) and capture starts immediately. All four tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -92,9 +92,15 @@ void InjectionPanel::inject_selected()
|
||||
return;
|
||||
}
|
||||
|
||||
// Publish the desired subsystem state before the hook's first reconcile tick.
|
||||
server_.set_subsystem_enabled(HookSubsys_Input, want_input_);
|
||||
server_.set_subsystem_enabled(HookSubsys_Focus, want_focus_);
|
||||
server_.set_subsystem_enabled(HookSubsys_Audio, want_audio_);
|
||||
|
||||
const InjectResult result = inject_dll(selected_pid_, hook_dll_path());
|
||||
if (result.status == InjectStatus::Ok)
|
||||
{
|
||||
injected_ = true;
|
||||
status_ = "Injected into " + narrow(selected_name_) + " (pid " + std::to_string(selected_pid_) + ").";
|
||||
status_color_ = kGreen;
|
||||
}
|
||||
@@ -200,6 +206,62 @@ void InjectionPanel::draw_hook_list(const HookStatusView& status)
|
||||
}
|
||||
}
|
||||
|
||||
// True if any hook of `subsystem` is currently installed in the game.
|
||||
static bool subsystem_installed(const HookStatusView& status, std::uint32_t subsystem)
|
||||
{
|
||||
const std::uint32_t n = status.hook_entry_count < kMaxHookEntries ? status.hook_entry_count : kMaxHookEntries;
|
||||
for (std::uint32_t i = 0; i < n; ++i)
|
||||
{
|
||||
if (status.hook_entries[i].subsystem == subsystem && status.hook_entries[i].installed)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void InjectionPanel::draw_subsystem_controls(const HookStatusView& status)
|
||||
{
|
||||
ImGui::SeparatorText("Subsystems (hook / unhook)");
|
||||
|
||||
struct Row
|
||||
{
|
||||
const char* label;
|
||||
std::uint32_t subsystem;
|
||||
bool* want;
|
||||
const char* depends;
|
||||
};
|
||||
const Row rows[] = {
|
||||
{"Input forwarding (XInput)", HookSubsys_Input, &want_input_, "controller input reaches the game"},
|
||||
{"Focus spoof", HookSubsys_Focus, &want_focus_, "the game keeps running unfocused"},
|
||||
{"Audio render-hook", HookSubsys_Audio, &want_audio_, "audio mirror without echo"},
|
||||
};
|
||||
|
||||
for (const Row& r : rows)
|
||||
{
|
||||
ImGui::PushID(r.label);
|
||||
if (ImGui::Checkbox(r.label, r.want))
|
||||
{
|
||||
server_.set_subsystem_enabled(r.subsystem, *r.want);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
const bool on = subsystem_installed(status, r.subsystem);
|
||||
if (*r.want != on)
|
||||
{
|
||||
ImGui::TextColored(kGrey, "(%s...)", *r.want ? "installing" : "removing");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextColored(on ? kGreen : kGrey, on ? "installed" : "off");
|
||||
}
|
||||
if (!*r.want)
|
||||
{
|
||||
ImGui::TextDisabled(" off: %s won't work", r.depends);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
void InjectionPanel::draw_hook_status(bool debug_details)
|
||||
{
|
||||
if (!server_.running())
|
||||
@@ -209,16 +271,37 @@ void InjectionPanel::draw_hook_status(bool debug_details)
|
||||
|
||||
const HookStatusView status = server_.hook_status();
|
||||
|
||||
ImGui::SeparatorText("Hook status");
|
||||
if (!status.attached)
|
||||
// DLL liveness from the heartbeat (advances ~4x/s while the worker runs).
|
||||
const double now = ImGui::GetTime();
|
||||
if (status.heartbeat != last_heartbeat_)
|
||||
{
|
||||
ImGui::TextColored(kGrey, "Waiting for hook to attach in the game...");
|
||||
last_heartbeat_ = status.heartbeat;
|
||||
last_heartbeat_time_ = now;
|
||||
dll_alive_ = true;
|
||||
}
|
||||
else if (now - last_heartbeat_time_ > 1.5)
|
||||
{
|
||||
dll_alive_ = false;
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Hook status");
|
||||
if (!injected_)
|
||||
{
|
||||
ImGui::TextColored(kGrey, "Not injected.");
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::TextColored(kGreen, "Attached (game pid %u)", status.game_pid);
|
||||
ImGui::TextColored(status.focus_spoof ? kGreen : kGrey, "Focus spoof: %s",
|
||||
status.focus_spoof ? "active" : "inactive");
|
||||
if (dll_alive_)
|
||||
{
|
||||
ImGui::TextColored(kGreen, "Hook DLL loaded in pid %lu (heartbeat %u)", server_.target_pid(),
|
||||
status.heartbeat);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextColored(kRed, "Hook DLL not responding (no heartbeat).");
|
||||
}
|
||||
|
||||
draw_subsystem_controls(status);
|
||||
ImGui::TextDisabled("Controller poll rates are in the Controllers panel.");
|
||||
|
||||
draw_hook_list(status);
|
||||
@@ -263,10 +346,11 @@ void InjectionPanel::draw(bool debug_details)
|
||||
|
||||
if (server_.running())
|
||||
{
|
||||
ImGui::TextColored(kGreen, "Forwarding input to pid %lu", server_.target_pid());
|
||||
if (ImGui::Button("Stop forwarding"))
|
||||
ImGui::TextColored(kGreen, "Connected to pid %lu", server_.target_pid());
|
||||
if (ImGui::Button("Disconnect"))
|
||||
{
|
||||
server_.stop();
|
||||
injected_ = false;
|
||||
status_ = "Stopped.";
|
||||
status_color_ = kGrey;
|
||||
}
|
||||
@@ -315,7 +399,10 @@ void InjectionPanel::draw(bool debug_details)
|
||||
ImGui::TextColored(status_color_, "%s", status_.c_str());
|
||||
}
|
||||
|
||||
// Synthetic input only reaches the game if the XInput hook is installed.
|
||||
ImGui::BeginDisabled(injected_ && !want_input_);
|
||||
ImGui::Checkbox("Forward synthetic test input", &test_input_);
|
||||
ImGui::EndDisabled();
|
||||
if (test_input_)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
|
||||
Reference in New Issue
Block a user