// Hook-side view of the shared-memory IPC channel. The host creates the section // (named by the target game's pid); we open it from inside the game and read the // forwarded pad state the host publishes each frame. #pragma once #include #include #include #include "coop/protocol.hpp" #include "coop/shared_memory.hpp" namespace coop::hook { class IpcClient { public: // Tries to open the section a few times: the host may inject us slightly // before (or after) it creates the mapping. Returns true once connected. bool connect(int attempts, int delay_ms) { const std::wstring name = shared_memory_name(GetCurrentProcessId()); for (int i = 0; i < attempts; ++i) { if (shm_.open(name, sizeof(SharedBlock))) { auto* block = shm_.as(); if (block->magic == kProtocolMagic && block->version == kProtocolVersion) { block_ = block; return true; } shm_.reset(); // present but not a contract we understand; retry } Sleep(static_cast(delay_ms)); } return false; } [[nodiscard]] bool connected() const { return block_ != nullptr; } // Host-requested install state for a subsystem (default = install, since the // mapping is zero-filled and 0 means "disabled flag clear" = install). [[nodiscard]] bool subsystem_install_requested(std::uint32_t subsystem) const { if (block_ == nullptr || subsystem >= HookSubsys_Count) { return true; } return block_->control.subsystem_disabled[subsystem].load(std::memory_order_acquire) == 0; } // Whether the game is allowed to clip/position the cursor (false = release it, // the default). Drives the Focus subsystem's ClipCursor/SetCursorPos hooks. [[nodiscard]] bool cursor_clip_allowed() const { return block_ != nullptr && block_->control.allow_cursor_clip.load(std::memory_order_acquire) != 0; } // Copies a torn-free snapshot of all slots. Returns false only if the host // was mid-write for the whole spin window (caller should reuse its cache). bool snapshot(CoopPadState (&out)[kMaxPads], std::uint32_t& count) const { if (block_ == nullptr) { return false; } return read_pads(*block_, out, count); } // --- Status back-channel (hook -> host diagnostics) -------------------- // Record that the game queried a controller slot via XInputGetState/Ex. void note_state_query(std::uint32_t user_index) { if (block_ != nullptr && user_index < kMaxPads) { block_->status.get_state_calls[user_index].fetch_add(1, std::memory_order_relaxed); } } void note_caps_query(std::uint32_t user_index) { if (block_ != nullptr && user_index < kMaxPads) { block_->status.get_caps_calls[user_index].fetch_add(1, std::memory_order_relaxed); } } void note_focus_query(FocusApi which) { if (block_ != nullptr && which < FocusApi_Count) { block_->status.focus_query_calls[which].fetch_add(1, std::memory_order_relaxed); } } void mark_attached() { if (block_ != nullptr) { block_->status.game_pid = GetCurrentProcessId(); block_->status.attached = 1; } } // XInput hooks were removed (host unhooked input): clear the attached flag so // the Controllers panel stops showing stale poll rates. void mark_detached() { if (block_ != nullptr) { block_->status.attached = 0; } } void mark_focus_spoof(bool active, std::uint64_t game_hwnd) { if (block_ != nullptr) { block_->status.focus_spoof = active ? 1u : 0u; block_->status.game_hwnd = game_hwnd; } } void set_input_diagnostics(bool raw_registered, bool raw_gamepad, bool raw_gamepad_sink, bool dinput) { if (block_ != nullptr) { block_->status.raw_input_registered = raw_registered ? 1u : 0u; block_->status.raw_input_gamepad = raw_gamepad ? 1u : 0u; block_->status.raw_input_gamepad_sink = raw_gamepad_sink ? 1u : 0u; block_->status.dinput_loaded = dinput ? 1u : 0u; } } void heartbeat() { if (block_ != nullptr) { block_->status.heartbeat.fetch_add(1, std::memory_order_relaxed); } } // Record the rumble the game requested for a slot (so the host can forward it to // the guest's controller). Plain stores; the hook is the sole writer. void note_rumble(std::uint32_t slot, std::uint16_t left, std::uint16_t right) { if (block_ != nullptr && slot < kMaxPads) { block_->status.rumble_left[slot] = left; block_->status.rumble_right[slot] = right; } } // Record the state the hook just returned to the game for a slot (round-trip view). void note_read_state(std::uint32_t slot, const CoopPadState& state) { if (block_ != nullptr && slot < kMaxPads) { block_->status.read_state[slot] = state; } } // --- Audio render-hook diagnostics ------------------------------------- // Total distinct render streams the audio hook has observed. void set_audio_streams_seen(std::uint32_t count) { if (block_ != nullptr) { block_->status.audio_streams_seen = count; } } // Publish a tracked stream's format/role into its debug slot. void publish_audio_stream(std::uint32_t slot, const AudioStreamInfo& info) { if (block_ != nullptr && slot < kMaxAudioStreams) { block_->status.audio_streams[slot] = info; } } // Update a tracked stream's cumulative frame count (host derives live/idle). void note_audio_frames(std::uint32_t slot, std::uint64_t frames) { if (block_ != nullptr && slot < kMaxAudioStreams) { block_->status.audio_streams[slot].frames_rendered = frames; } } // --- Present-hook video channel ---------------------------------------- // Record that the game's Present() ran (diagnostic counter, hook is sole writer). void note_present() { if (block_ != nullptr) { block_->video.present_calls += 1; } } // Publish that a fresh backbuffer copy is in the shared texture (named // coop_video_) with these dimensions/format; bumps the generation the host // polls. The texture itself is shared out-of-band by name, not through here. void publish_video_frame(std::uint32_t width, std::uint32_t height, std::uint32_t format) { if (block_ != nullptr) { block_->video.width = width; block_->video.height = height; block_->video.format = format; block_->video.generation.fetch_add(1, std::memory_order_release); } } // --- Mouse + keyboard forwarding --------------------------------------- // The host's MKB event queue (nullptr if not connected). The MKB subsystem // drains it; the host is the sole producer. [[nodiscard]] MkbRing* mkb_ring() { return block_ != nullptr ? &block_->mkb : nullptr; } // --- Hook registry ----------------------------------------------------- // Publish the installed-hooks table (name / subsystem / installed / calls). void publish_hook_entries(const HookEntry* entries, std::uint32_t count) { if (block_ == nullptr) { return; } if (count > kMaxHookEntries) { count = kMaxHookEntries; } for (std::uint32_t i = 0; i < count; ++i) { block_->status.hook_entries[i] = entries[i]; } block_->status.hook_entry_count = count; } private: SharedMemory shm_; SharedBlock* block_ = nullptr; }; } // namespace coop::hook