Ctrl K
ComunidadGuías…

server/tools/playnite_launcher/launcher.cpp

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#include "tools/playnite_launcher/launcher.h"

#include "src/logging.h"
#include "src/platform/windows/ipc/misc_utils.h"
#include "src/platform/windows/playnite_ipc.h"
#include "src/platform/windows/playnite_protocol.h"
#include "tools/playnite_launcher/arguments.h"
#include "tools/playnite_launcher/cleanup.h"
#include "tools/playnite_launcher/focus_utils.h"
#include "tools/playnite_launcher/lossless_scaling.h"
#include "tools/playnite_launcher/playnite_process.h"

#include <algorithm>
#include <atomic>
#include <chrono>
#include <filesystem>
#include <limits>
#include <mutex>
#include <nlohmann/json.hpp>
#include <optional>
#include <ShlObj.h>
#include <string>
#include <string_view>
#include <thread>
#include <utility>
#include <vector>
#include <windows.h>

using namespace std::chrono_literals;

namespace playnite_launcher {
  namespace detail {

    std::filesystem::path resolve_log_path();
    void ensure_playnite_open();
    int64_t steady_to_millis(std::chrono::steady_clock::time_point tp);
    std::chrono::steady_clock::time_point millis_to_steady(int64_t ms);
    std::string normalize_game_id(std::string s);

    int run_cleanup_mode(const LauncherConfig &config, const lossless::lossless_scaling_options &lossless_options);
    int run_fullscreen_mode(LauncherConfig config, const lossless::lossless_scaling_options &lossless_options);
    int run_standard_mode(LauncherConfig config, const lossless::lossless_scaling_options &lossless_options);

  }  // namespace detail

  int launcher_run(int argc, char **argv) {
    auto parsed = parse_arguments(argc, argv);
    if (!parsed.success) {
      return parsed.exit_code;
    }
    LauncherConfig config = std::move(parsed.config);

    FreeConsole();

    auto log_path = detail::resolve_log_path();
    auto log_guard = logging::init(2, log_path.string());
    BOOST_LOG(info) << "Playnite launcher starting; pid=" << GetCurrentProcessId();

    auto lossless_options = lossless::read_lossless_scaling_options();

    // Adjust focus_attempts to account for lossless scaling focus attempt (1 additional)
    // This ensures the game still gets the configured number of focus attempts
    if (lossless_options.enabled && config.focus_attempts > 0) {
      config.focus_attempts += 1;
      BOOST_LOG(debug) << "Lossless Scaling enabled: adjusted focus_attempts by +1 to " << config.focus_attempts;
    }

    if (config.cleanup) {
      return detail::run_cleanup_mode(config, lossless_options);
    }
    if (config.fullscreen) {
      return detail::run_fullscreen_mode(config, lossless_options);
    }
    return detail::run_standard_mode(config, lossless_options);
  }

  namespace detail {

    std::filesystem::path resolve_log_path() {
      std::wstring appdataW;
      appdataW.resize(MAX_PATH);
      if (FAILED(SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr, SHGFP_TYPE_CURRENT, appdataW.data()))) {
        appdataW = L".";
      }
      appdataW.resize(wcslen(appdataW.c_str()));
      std::filesystem::path logdir = std::filesystem::path(appdataW) / L"Sunshine";
      std::error_code ec;
      std::filesystem::create_directories(logdir, ec);
      return logdir / L"sunshine_playnite_launcher.log";
    }

    void ensure_playnite_open() {
      if (!playnite::is_playnite_running()) {
        BOOST_LOG(info) << "Playnite not running; opening playnite:// URI in detached mode";
        if (!playnite::launch_uri_detached_parented(L"playnite://")) {
          BOOST_LOG(warning) << "Failed to launch playnite:// via detached CreateProcess";
        }
      }
    }

    int64_t steady_to_millis(std::chrono::steady_clock::time_point tp) {
      return std::chrono::duration_cast<std::chrono::milliseconds>(tp.time_since_epoch()).count();
    }

    std::chrono::steady_clock::time_point millis_to_steady(int64_t ms) {
      return std::chrono::steady_clock::time_point(std::chrono::milliseconds(ms));
    }

    std::string normalize_game_id(std::string s) {
      s.erase(std::remove_if(s.begin(), s.end(), [](char c) {
                return c == '{' || c == '}';
              }),
              s.end());
      std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) {
        return static_cast<char>(std::tolower(c));
      });
      return s;
    }

    nlohmann::json snapshot_process_environment() {
      nlohmann::json environment = nlohmann::json::object();
      LPWCH block = GetEnvironmentStringsW();
      if (!block) {
        BOOST_LOG(warning) << "Unable to capture environment for Playnite launch, error=" << GetLastError();
        return environment;
      }

      auto free_block = util::fail_guard([block]() {
        FreeEnvironmentStringsW(block);
      });

      for (const wchar_t *entry = block; *entry != L'\0'; entry += wcslen(entry) + 1) {
        std::wstring_view pair(entry);
        auto separator = pair.find(L'=');
        // Windows environment blocks can contain drive-current-directory entries
        // (for example, "=C:=C:\\working"). They are not regular variables and
        // cannot be set through the .NET process-environment API used by Playnite.
        if (separator == 0) {
          separator = pair.find(L'=', 1);
        }
        if (separator == std::wstring_view::npos || separator == 0) {
          continue;
        }

        try {
          std::wstring_view name = pair.substr(0, separator);
          std::wstring_view value = pair.substr(separator + 1);
          environment[platf::dxgi::wide_to_utf8(std::wstring(name))] =
            platf::dxgi::wide_to_utf8(std::wstring(value));
        } catch (...) {
          BOOST_LOG(warning) << "Unable to serialize an environment variable for Playnite launch";
        }
      }

      return environment;
    }

    int run_cleanup_mode(const LauncherConfig &config, const lossless::lossless_scaling_options &lossless_options) {
      BOOST_LOG(info) << "Cleanup mode: starting (installDir='" << config.install_dir << "' fullscreen=" << (config.fullscreen ? 1 : 0) << ")";
      bool parent_bound_watchdog = false;
      if (!config.wait_for_pid.empty()) {
        try {
          if (config.wait_for_pid.find_first_not_of("0123456789") != std::string::npos) {
            BOOST_LOG(warning) << "Cleanup mode: invalid --wait-for-pid value: '" << config.wait_for_pid << "'";
          } else {
            DWORD wpid = static_cast<DWORD>(std::stoul(config.wait_for_pid));
            if (wpid == 0 || wpid == GetCurrentProcessId()) {
              BOOST_LOG(warning) << "Cleanup mode: refusing zero/self --wait-for-pid value: " << wpid;
            } else {
              parent_bound_watchdog = true;
              HANDLE hp = OpenProcess(SYNCHRONIZE, FALSE, wpid);
              if (hp) {
                BOOST_LOG(info) << "Cleanup mode: waiting for PID=" << wpid << " to exit";
                DWORD wr = WaitForSingleObject(hp, INFINITE);
                CloseHandle(hp);
                BOOST_LOG(info) << "Cleanup mode: wait result=" << wr;
              } else {
                const auto error = GetLastError();
                BOOST_LOG(info) << "Cleanup mode: parent PID is no longer waitable; continuing to successor grace. PID="
                                << wpid << " error=" << error;
              }
            }
          }
        } catch (...) {
          BOOST_LOG(warning) << "Cleanup mode: invalid --wait-for-pid value: '" << config.wait_for_pid << "'";
        }
      }
      std::wstring install_dir_w;
      if (!config.install_dir.empty()) {
        install_dir_w = platf::dxgi::utf8_to_wide(config.install_dir);
      }
      if (parent_bound_watchdog && !config.install_dir.empty()) {
        // The previous launcher may have exited because a client disconnected
        // just as another client resumed the same game. Give the successor time
        // to claim the install directory before closing any of its processes.
        constexpr auto successor_grace = 10s;
        const auto deadline = std::chrono::steady_clock::now() + successor_grace;
        while (std::chrono::steady_clock::now() < deadline) {
          if (playnite::cleanup_lease_t::held_by_another_launcher(config.install_dir)) {
            BOOST_LOG(info) << "Cleanup mode: successor launcher claimed install directory; skipping cleanup";
            return 0;
          }
          std::this_thread::sleep_for(250ms);
        }
        if (playnite::cleanup_lease_t::held_by_another_launcher(config.install_dir)) {
          BOOST_LOG(info) << "Cleanup mode: successor launcher claimed install directory; skipping cleanup";
          return 0;
        }
      }
      if (!config.fullscreen && !install_dir_w.empty()) {
        cleanup::cleanup_graceful_then_forceful_in_dir(install_dir_w, config.exit_timeout_secs);
      }
      if (config.fullscreen) {
        cleanup::cleanup_fullscreen_via_desktop(std::max(3, config.exit_timeout_secs));
      }
      if (lossless_options.enabled) {
        auto runtime = lossless::capture_lossless_scaling_state();
        if (!runtime.running_pids.empty()) {
          lossless::lossless_scaling_stop_processes(runtime);
        }
      }
      BOOST_LOG(info) << "Cleanup mode: done";
      return 0;
    }

    int run_fullscreen_mode(LauncherConfig config, const lossless::lossless_scaling_options &lossless_options) {
      BOOST_LOG(info) << "Fullscreen mode: preparing IPC connection to Playnite plugin";

      platf::playnite::IpcClient client;
      const auto fullscreen_launch_environment = snapshot_process_environment();
      std::atomic<bool> pipe_connected {false};

      std::atomic<bool> game_start_signal {false};
      std::atomic<bool> game_stop_signal {false};
      std::atomic<bool> cleanup_spawn_signal {false};
      std::atomic<bool> active_game_flag {false};
      // Set when the plugin reports Playnite is shutting down with no game running
      // (i.e. the user purposely quit Playnite, rather than Playnite quitting to
      // launch a game). Used to end the session promptly instead of waiting out
      // the process-missing grace window.
      std::atomic<bool> playnite_user_exit {false};
      std::atomic<int64_t> grace_deadline_ms {steady_to_millis(std::chrono::steady_clock::now() + std::chrono::seconds(15))};

      std::mutex game_mutex;

      struct FullscreenGameState {
        std::string id_original;
        std::string id_norm;
        std::string install_dir;
        std::string exe_path;
        std::string cleanup_dir;
      } game_state;

      std::string fullscreen_install_dir_utf8;
      std::optional<playnite::cleanup_lease_t> fullscreen_cleanup_lease;
      std::string fullscreen_cleanup_lease_install_dir;
      std::optional<playnite::cleanup_lease_t> game_cleanup_lease;
      std::string game_cleanup_lease_install_dir;

      auto ensure_cleanup_leases = [&]() {
        std::string game_install_dir;
        {
          std::lock_guard<std::mutex> lk(game_mutex);
          game_install_dir = game_state.cleanup_dir;
        }

        auto ensure_lease = [&](const std::string &install_dir, auto &lease, std::string &lease_install_dir) {
          if (lease_install_dir != install_dir) {
            lease.reset();
            lease_install_dir = install_dir;
          }
          if (!install_dir.empty() && !lease) {
            if (auto acquired = playnite::cleanup_lease_t::acquire(install_dir)) {
              lease = std::move(*acquired);
              BOOST_LOG(debug) << "Fullscreen cleanup lease: acquired for install directory '" << install_dir << "'";
            }
          }
        };

        ensure_lease(
          fullscreen_install_dir_utf8,
          fullscreen_cleanup_lease,
          fullscreen_cleanup_lease_install_dir
        );
        ensure_lease(game_install_dir, game_cleanup_lease, game_cleanup_lease_install_dir);
      };

      std::mutex cleanup_mutex;
      std::string active_cleanup_dir;
      bool game_cleanup_spawned = false;
      std::atomic<bool> watcher_spawned {false};

      lossless::lossless_scaling_profile_backup fullscreen_lossless_backup {};
      bool fullscreen_lossless_applied = false;
      bool game_started_once = false;
      bool fullscreen_was_running = false;

      auto renew_grace_deadline = [&](const char *reason) {
        grace_deadline_ms.store(steady_to_millis(std::chrono::steady_clock::now() + std::chrono::seconds(15)));
        BOOST_LOG(debug) << "Fullscreen mode: extending handoff grace due to " << reason;
      };

      auto resolve_install_dir = [&](const std::string &install_dir, const std::string &exe_path) -> std::string {
        if (!install_dir.empty()) {
          return install_dir;
        }
        try {
          if (!exe_path.empty()) {
            std::wstring wexe = platf::dxgi::utf8_to_wide(exe_path);
            std::filesystem::path p(wexe);
            auto parent = p.parent_path();
            if (!parent.empty()) {
              return platf::dxgi::wide_to_utf8(parent.wstring());
            }
          }
        } catch (...) {
        }
        return std::string();
      };

      client.set_message_handler([&](std::span<const uint8_t> bytes) {
        auto msg = platf::playnite::parse(bytes);
        using MT = platf::playnite::MessageType;
        if (msg.type != MT::Status) {
          return;
        }
        auto norm_id = normalize_game_id(msg.status_game_id);
        if (msg.status_name == "gameStarted") {
          std::string install_for_ls;
          std::string exe_for_ls;
          {
            std::lock_guard<std::mutex> lk(game_mutex);
            game_state.id_original = msg.status_game_id;
            game_state.id_norm = norm_id;
            if (!msg.status_install_dir.empty()) {
              game_state.install_dir = msg.status_install_dir;
            }
            if (!msg.status_exe.empty()) {
              game_state.exe_path = msg.status_exe;
            }
            auto resolved = resolve_install_dir(game_state.install_dir, game_state.exe_path);
            if (!resolved.empty()) {
              game_state.install_dir = resolved;
              game_state.cleanup_dir = resolved;
            } else {
              game_state.cleanup_dir.clear();
            }
            install_for_ls = game_state.install_dir;
            exe_for_ls = game_state.exe_path;
          }
          game_started_once = true;
          active_game_flag.store(true);
          // A fresh launch overrides any prior "Playnite is exiting" intent.
          playnite_user_exit.store(false);
          game_start_signal.store(true);
          cleanup_spawn_signal.store(true);
          renew_grace_deadline("gameStarted");
          if (lossless_options.enabled && !fullscreen_lossless_applied) {
            if (lossless_options.launch_delay_seconds > 0) {
              BOOST_LOG(info) << "Lossless Scaling: delaying launch by " << lossless_options.launch_delay_seconds << " seconds after gameStarted";
              auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(lossless_options.launch_delay_seconds);
              while (std::chrono::steady_clock::now() < deadline) {
                std::this_thread::sleep_for(200ms);
              }
            }
            auto runtime = lossless::capture_lossless_scaling_state();
            if (!runtime.running_pids.empty()) {
              lossless::lossless_scaling_stop_processes(runtime);
            }
            lossless::lossless_scaling_profile_backup backup;
            bool changed = lossless::lossless_scaling_apply_global_profile(lossless_options, install_for_ls, exe_for_ls, backup);
            if (backup.valid) {
              fullscreen_lossless_backup = backup;
              fullscreen_lossless_applied = true;
            } else {
              fullscreen_lossless_backup = {};
            }
            DWORD focus_pid = 0;
            if (auto selected = lossless::lossless_scaling_select_focus_pid(install_for_ls, exe_for_ls, std::nullopt)) {
              focus_pid = *selected;
            }
            lossless::lossless_scaling_restart_foreground(
              runtime,
              changed,
              install_for_ls,
              exe_for_ls,
              focus_pid,
              lossless_options.legacy_auto_detect
            );
          }
        } else if (msg.status_name == "gameStopped") {
          bool matches = false;
          {
            std::lock_guard<std::mutex> lk(game_mutex);
            if (game_state.id_norm.empty() || norm_id.empty()) {
              matches = true;
            } else {
              matches = game_state.id_norm == norm_id;
            }
            if (matches) {
              game_state.id_original.clear();
              game_state.id_norm.clear();
            }
          }
          if (matches) {
            active_game_flag.store(false);
            game_stop_signal.store(true);
            renew_grace_deadline("gameStopped");
            if (fullscreen_lossless_applied) {
              auto runtime = lossless::capture_lossless_scaling_state();
              if (!runtime.running_pids.empty()) {
                lossless::lossless_scaling_stop_processes(runtime);
              }
              bool restored = lossless::lossless_scaling_restore_global_profile(fullscreen_lossless_backup);
              lossless::lossless_scaling_restart_foreground(runtime, restored);
              fullscreen_lossless_backup = {};
              fullscreen_lossless_applied = false;
            }
          }
        } else if (msg.status_name == "playniteExiting") {
          // Playnite is shutting down and the plugin determined no game is running,
          // so this is a deliberate user quit. Flag it; the main loop ends the
          // session once it confirms no game process is keeping the stream alive.
          BOOST_LOG(info) << "Fullscreen mode: Playnite reported user-initiated exit";
          playnite_user_exit.store(true);
        }
      });

      client.set_connected_handler([&]() {
        pipe_connected.store(true);
        try {
          nlohmann::json hello;
          hello["type"] = "hello";
          hello["role"] = "launcher";
          hello["pid"] = static_cast<uint32_t>(GetCurrentProcessId());
          hello["mode"] = "fullscreen";
          client.send_json_line(hello.dump());

          // Keep the session environment active while this fullscreen helper is
          // connected. This covers games and other child processes started from
          // Playnite's fullscreen UI, including manually selected games.
          nlohmann::json environment_command;
          environment_command["type"] = "command";
          environment_command["command"] = "set-environment";
          environment_command["env"] = fullscreen_launch_environment;
          if (!client.send_json_line(environment_command.dump())) {
            BOOST_LOG(warning) << "Fullscreen mode: failed to send persistent environment to Playnite";
          }
        } catch (...) {}
      });

      client.set_disconnected_handler([&]() {
        pipe_connected.store(false);
      });

      client.start();

      auto launch_playnite_fullscreen = [&]() -> bool {
        bool started = false;
        try {
          std::wstring assocExe = playnite::query_playnite_executable_from_assoc();
          if (!assocExe.empty()) {
            std::filesystem::path assoc_path(assocExe);
            std::filesystem::path base = assoc_path.parent_path();
            fullscreen_install_dir_utf8 = platf::dxgi::wide_to_utf8(base.wstring());
            ensure_cleanup_leases();
            std::filesystem::path desktopExe = base / L"Playnite.DesktopApp.exe";
            std::filesystem::path targetExe = desktopExe;
            if (!std::filesystem::exists(targetExe) && std::filesystem::exists(assoc_path)) {
              targetExe = assoc_path;
            }
            if (std::filesystem::exists(targetExe)) {
              BOOST_LOG(info) << "Launching Playnite Desktop with --startfullscreen from: " << platf::dxgi::wide_to_utf8(targetExe.wstring());
              started = playnite::launch_executable_detached_parented_with_args(targetExe.wstring(), L"--startfullscreen");
            }
            if (!started) {
              std::filesystem::path fullscreenExe = base / L"Playnite.FullscreenApp.exe";
              if (std::filesystem::exists(fullscreenExe)) {
                BOOST_LOG(info) << "Desktop launch failed; falling back to FullscreenApp from: " << platf::dxgi::wide_to_utf8(fullscreenExe.wstring());
                started = playnite::launch_executable_detached_parented(fullscreenExe.wstring());
              }
            }
          }
        } catch (...) {
        }
        if (!started) {
          BOOST_LOG(info) << "Playnite executable not resolved; falling back to playnite://";
          ensure_playnite_open();
        }
        return started;
      };

      BOOST_LOG(info) << "Fullscreen mode requested; attempting to start Playnite.DesktopApp.exe --startfullscreen";
      (void) launch_playnite_fullscreen();
      ensure_cleanup_leases();

      WCHAR selfPath[MAX_PATH] = {};
      GetModuleFileNameW(nullptr, selfPath, ARRAYSIZE(selfPath));
      DWORD launcher_pid = GetCurrentProcessId();
      if (playnite::spawn_cleanup_watchdog_process(selfPath, fullscreen_install_dir_utf8, config.exit_timeout_secs, true, launcher_pid)) {
        watcher_spawned.store(true);
      } else {
        BOOST_LOG(warning) << "Fullscreen mode: failed to spawn cleanup watchdog";
      }

      auto spawn_game_cleanup = [&](const std::string &dir_utf8) {
        if (dir_utf8.empty()) {
          return;
        }
        std::lock_guard<std::mutex> lk(cleanup_mutex);
        if (active_cleanup_dir != dir_utf8) {
          active_cleanup_dir = dir_utf8;
          game_cleanup_spawned = false;
        }
        if (game_cleanup_spawned) {
          return;
        }
        if (playnite::spawn_cleanup_watchdog_process(selfPath, dir_utf8, config.exit_timeout_secs, false, GetCurrentProcessId())) {
          game_cleanup_spawned = true;
        } else if (active_cleanup_dir == dir_utf8) {
          game_cleanup_spawned = false;
        }
      };

      auto wait_deadline = std::chrono::steady_clock::now() + 10s;
      while (std::chrono::steady_clock::now() < wait_deadline) {
        ensure_cleanup_leases();
        auto pids = platf::dxgi::find_process_ids_by_name(L"Playnite.FullscreenApp.exe");
        if (!pids.empty()) {
          break;
        }
        std::this_thread::sleep_for(300ms);
      }

      auto cancel_fullscreen_focus = [&]() {
        ensure_cleanup_leases();
        return active_game_flag.load();
      };
      ensure_cleanup_leases();
      bool focused = focus::focus_process_by_name_extended(L"Playnite.FullscreenApp.exe", config.focus_attempts, config.focus_timeout_secs, config.focus_exit_on_first, cancel_fullscreen_focus);
      ensure_cleanup_leases();
      BOOST_LOG(info) << (focused ? "Fullscreen focus applied" : "Fullscreen focus not confirmed");

      int fullscreen_successes_left = std::max(0, config.focus_attempts);
      bool fullscreen_focus_budget_active = fullscreen_successes_left > 0 && config.focus_timeout_secs > 0;
      auto fullscreen_focus_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(std::max(0, config.focus_timeout_secs));
      auto next_fullscreen_focus_check = std::chrono::steady_clock::now();

      int game_successes_left = 0;
      bool game_focus_budget_active = false;
      auto game_focus_deadline = std::chrono::steady_clock::now();
      auto next_game_focus_check = std::chrono::steady_clock::now();
      bool fullscreen_detected = false;

      int consecutive_missing = 0;
      auto connection_grace_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(std::max(30, config.timeout_sec));

      // Track the launched game's processes directly. Playnite can be configured to
      // quit when launching a game ("exit Playnite after launch"), in which case the
      // plugin dies and never delivers gameStopped over IPC. Watching the process tree
      // lets us keep the stream alive while the game runs and tear it down once it ends,
      // independent of any IPC signal.
      bool game_proc_seen = false;
      std::optional<std::chrono::steady_clock::time_point> game_proc_first_seen;
      std::optional<std::chrono::steady_clock::time_point> game_missing_since;
      // A long grace absorbs the early launch handoff (bootstrappers exiting and
      // respawning the real exe, loaders, multi-process launchers). Once a game has
      // actually been running for a while, any process disappearance is a genuine
      // exit, so a short grace is enough to confirm it.
      const auto game_missing_grace_long = std::chrono::seconds(15);
      const auto game_missing_grace_short = std::chrono::seconds(5);
      const auto long_session_threshold = std::chrono::seconds(60);

      auto game_processes_alive = [&]() -> bool {
        std::string install_dir;
        std::string exe_path;
        {
          std::lock_guard<std::mutex> lk(game_mutex);
          install_dir = game_state.install_dir;
          exe_path = game_state.exe_path;
        }
        if (!install_dir.empty()) {
          try {
            std::wstring wdir = platf::dxgi::utf8_to_wide(install_dir);
            if (!focus::find_pids_under_install_dir_sorted(wdir, false).empty()) {
              return true;
            }
          } catch (...) {
          }
        }
        if (!exe_path.empty()) {
          try {
            std::filesystem::path p(platf::dxgi::utf8_to_wide(exe_path));
            std::wstring base = p.filename().wstring();
            if (!base.empty() && !platf::dxgi::find_process_ids_by_name(base.c_str()).empty()) {
              return true;
            }
          } catch (...) {
          }
        }
        return false;
      };

      auto send_stop_command_if_needed = [&]() {
        if (!client.is_active()) {
          return;
        }
        std::string id_to_stop;
        {
          std::lock_guard<std::mutex> lk(game_mutex);
          id_to_stop = game_state.id_original;
        }
        if (id_to_stop.empty()) {
          return;
        }
        try {
          nlohmann::json stop;
          stop["type"] = "command";
          stop["command"] = "stop";
          stop["id"] = id_to_stop;
          client.send_json_line(stop.dump());
          BOOST_LOG(info) << "Fullscreen mode: stop command sent for id=" << id_to_stop;
        } catch (...) {
          BOOST_LOG(warning) << "Fullscreen mode: failed to send stop command";
        }
      };

      while (true) {
        ensure_cleanup_leases();
        bool fs_running = false;
        std::vector<DWORD> fs_pids;
        try {
          fs_pids = platf::dxgi::find_process_ids_by_name(L"Playnite.FullscreenApp.exe");
          fs_running = !fs_pids.empty();
        } catch (...) {
        }

        bool active_game_now = active_game_flag.load();

        auto now = std::chrono::steady_clock::now();

        // Watch the launched game's process tree so the session survives Playnite quitting
        // on launch (no gameStopped IPC) and ends once the game itself exits.
        bool game_proc_alive = game_processes_alive();
        if (game_proc_alive) {
          game_proc_seen = true;
          if (!game_proc_first_seen) {
            game_proc_first_seen = now;
          }
          game_missing_since.reset();
        }
        bool game_alive;
        if (game_proc_seen) {
          if (game_proc_alive) {
            game_alive = true;
          } else {
            if (!game_missing_since) {
              game_missing_since = now;
            }
            // Shorten the grace once the game has clearly been running for a while.
            bool long_session = game_proc_first_seen && (*game_missing_since - *game_proc_first_seen) >= long_session_threshold;
            auto grace = long_session ? game_missing_grace_short : game_missing_grace_long;
            game_alive = (now - *game_missing_since) < grace;
          }
        }
        else {
          // gameStarted signaled but the process has not been observed yet; trust IPC + grace.
          game_alive = active_game_now;
        }

        // Playnite fullscreen can exit before the plugin delivers gameStarted for the launched game.
        // Hold the session briefly on that transition so Sunshine does not tear down the stream first.
        if (!fs_running && fullscreen_was_running && fullscreen_detected && !game_alive) {
          renew_grace_deadline("fullscreen-handoff");
        }

        int64_t grace_ms = grace_deadline_ms.load();
        bool in_grace = grace_ms > 0 && now < millis_to_steady(grace_ms);
        bool waiting_for_pipe = !pipe_connected.load() && now < connection_grace_deadline;

        if (!fullscreen_detected && fs_running) {
          fullscreen_detected = true;
        }

        // The user purposely quit Playnite (plugin confirmed no game was running) and no
        // game process is keeping the stream alive: end the session right away.
        if (playnite_user_exit.load() && !fs_running && !game_alive) {
          BOOST_LOG(info) << "Fullscreen mode: ending session after user exit with no active game";
          break;
        }

        // A game we were watching has exited (its processes are gone past the grace window)
        // and Playnite fullscreen is not running. This is the "exit Playnite on launch"
        // case: Playnite quit to start the game, so the user expects to land back in the
        // fullscreen library rather than on the desktop with a dead stream. Relaunch
        // Playnite fullscreen and keep the session going. The session only ends when
        // Playnite is quit deliberately with no game (playnite_user_exit, handled above).
        if (game_proc_seen && !game_alive && !fs_running) {
          BOOST_LOG(info) << "Fullscreen mode: game exited and Playnite is closed; relaunching Playnite fullscreen";
          {
            std::lock_guard<std::mutex> lk(game_mutex);
            game_state.id_original.clear();
            game_state.id_norm.clear();
            game_state.install_dir.clear();
            game_state.exe_path.clear();
            game_state.cleanup_dir.clear();
          }
          active_game_flag.store(false);
          game_proc_seen = false;
          game_proc_first_seen.reset();
          game_missing_since.reset();
          fullscreen_detected = false;
          fullscreen_was_running = false;
          consecutive_missing = 0;
          (void) launch_playnite_fullscreen();
          // Give Playnite time to come back before any teardown is reconsidered, and
          // allow the plugin's IPC connection to re-establish.
          renew_grace_deadline("fullscreen-relaunch");
          connection_grace_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(std::max(30, config.timeout_sec));
          // Re-arm autofocus for the relaunched fullscreen UI.
          if (config.focus_attempts > 0 && config.focus_timeout_secs > 0) {
            fullscreen_successes_left = std::max(0, config.focus_attempts);
            fullscreen_focus_budget_active = true;
            fullscreen_focus_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(std::max(1, config.focus_timeout_secs));
            next_fullscreen_focus_check = std::chrono::steady_clock::now();
          }
          std::this_thread::sleep_for(500ms);
          continue;
        }

        if (fs_running || game_alive || in_grace || waiting_for_pipe) {
          consecutive_missing = 0;
        } else {
          consecutive_missing++;
        }

        if (consecutive_missing >= 12) {
          break;
        }

        fullscreen_was_running = fs_running;

        if (!watcher_spawned.load() && !fullscreen_install_dir_utf8.empty()) {
          bool expected = false;
          if (watcher_spawned.compare_exchange_strong(expected, true)) {
            if (!playnite::spawn_cleanup_watchdog_process(selfPath, fullscreen_install_dir_utf8, config.exit_timeout_secs, true, launcher_pid)) {
              watcher_spawned.store(false);
            }
          }
        }

        if (cleanup_spawn_signal.exchange(false)) {
          std::string dir;
          {
            std::lock_guard<std::mutex> lk(game_mutex);
            dir = game_state.cleanup_dir;
          }
          spawn_game_cleanup(dir);
        }

        if (game_start_signal.exchange(false)) {
          game_successes_left = std::max(0, config.focus_attempts);
          game_focus_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(std::max(1, config.focus_timeout_secs));
          game_focus_budget_active = config.focus_attempts > 0 && config.focus_timeout_secs > 0;
          next_game_focus_check = std::chrono::steady_clock::now();
          fullscreen_focus_budget_active = false;
          fullscreen_successes_left = std::max(0, config.focus_attempts);
        }

        if (game_stop_signal.exchange(false)) {
          game_focus_budget_active = false;
          game_successes_left = 0;
          if (config.focus_attempts > 0 && config.focus_timeout_secs > 0) {
            fullscreen_focus_budget_active = true;
            fullscreen_focus_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(std::max(1, config.focus_timeout_secs));
            next_fullscreen_focus_check = std::chrono::steady_clock::now();
          }
        }

        if (active_game_now && game_focus_budget_active) {
          auto now_focus = std::chrono::steady_clock::now();
          if (now_focus >= next_game_focus_check) {
            int remaining_secs = static_cast<int>(std::chrono::duration_cast<std::chrono::seconds>(game_focus_deadline - now_focus).count());
            if (remaining_secs <= 0) {
              game_focus_budget_active = false;
            } else {
              std::string install_dir;
              std::string exe_path;
              {
                std::lock_guard<std::mutex> lk(game_mutex);
                install_dir = game_state.install_dir;
                exe_path = game_state.exe_path;
              }
              bool applied = false;
              auto cancel = [&]() {
                return !active_game_flag.load();
              };
              int slice = std::clamp(remaining_secs, 1, 3);
              if (!install_dir.empty()) {
                try {
                  std::wstring wdir = platf::dxgi::utf8_to_wide(install_dir);
                  applied = focus::focus_by_install_dir_extended(wdir, 1, slice, true, cancel);
                } catch (...) {
                }
              }
              if (!applied && !exe_path.empty()) {
                try {
                  std::wstring wexe = platf::dxgi::utf8_to_wide(exe_path);
                  std::filesystem::path p(wexe);
                  std::wstring base = p.filename().wstring();
                  if (!base.empty()) {
                    applied = focus::focus_process_by_name_extended(base.c_str(), 1, slice, true, cancel);
                  }
                } catch (...) {
                }
              }
              if (applied) {
                if (game_successes_left > 0) {
                  game_successes_left--;
                }
                if (game_successes_left <= 0) {
                  game_focus_budget_active = false;
                }
              } else if (std::chrono::steady_clock::now() >= game_focus_deadline) {
                game_focus_budget_active = false;
              }
            }
            next_game_focus_check = std::chrono::steady_clock::now() + 1s;
          }
        }

        if (!active_game_now && fullscreen_focus_budget_active) {
          auto now_focus = std::chrono::steady_clock::now();
          if (now_focus >= next_fullscreen_focus_check) {
            bool already_fg = false;
            for (auto pid : fs_pids) {
              if (focus::confirm_foreground_pid(pid)) {
                already_fg = true;
                break;
              }
            }
            if (!already_fg) {
              int remaining_secs = static_cast<int>(std::chrono::duration_cast<std::chrono::seconds>(fullscreen_focus_deadline - now_focus).count());
              if (remaining_secs <= 0) {
                fullscreen_focus_budget_active = false;
              } else if (fullscreen_successes_left > 0) {
                bool ok = focus::focus_process_by_name_extended(L"Playnite.FullscreenApp.exe", 1, std::min(2, remaining_secs), true);
                if (ok) {
                  fullscreen_successes_left--;
                }
                if (fullscreen_successes_left <= 0 || std::chrono::steady_clock::now() >= fullscreen_focus_deadline) {
                  fullscreen_focus_budget_active = false;
                }
              } else {
                fullscreen_focus_budget_active = false;
              }
            }
            next_fullscreen_focus_check = now_focus + 2s;
          }
        }

        std::this_thread::sleep_for(500ms);
      }

      if (game_started_once) {
        send_stop_command_if_needed();
      }

      {
        std::string final_cleanup_dir;
        {
          std::lock_guard<std::mutex> lk(game_mutex);
          final_cleanup_dir = game_state.cleanup_dir;
        }
        if (!final_cleanup_dir.empty()) {
          spawn_game_cleanup(final_cleanup_dir);
        }
      }

      client.stop();
      if (fullscreen_lossless_applied) {
        auto runtime = lossless::capture_lossless_scaling_state();
        if (!runtime.running_pids.empty()) {
          lossless::lossless_scaling_stop_processes(runtime);
        }
        (void) lossless::lossless_scaling_restore_global_profile(fullscreen_lossless_backup);
      }
      return 0;
    }

    int run_standard_mode(LauncherConfig config, const lossless::lossless_scaling_options &lossless_options) {
      BOOST_LOG(info) << "Launcher mode: preparing IPC connection to Playnite plugin";

      platf::playnite::IpcClient client;
      const auto launch_environment = snapshot_process_environment();

      std::atomic<bool> should_exit {false};
      std::atomic<bool> got_started {false};
      std::atomic<bool> game_stop_pending {false};
      std::atomic<bool> explicit_stop_requested {false};
      std::atomic<bool> launch_command_sent {false};
      std::atomic<int> launch_retry_budget {2};
      std::atomic<bool> request_game_focus {false};
      std::atomic<bool> game_focus_confirmed {false};
      std::atomic<int> game_focus_successes_left {0};
      std::atomic<bool> lossless_refocus_pending {false};
      std::atomic<bool> had_focus_success {false};
      std::atomic<DWORD> last_confirmed_focus_pid {0};
      std::atomic<int64_t> focus_retry_deadline_ms {0};
      std::atomic<int64_t> next_focus_attempt_ms {std::numeric_limits<int64_t>::min()};
      std::mutex game_info_mutex;
      std::string last_install_dir;
      std::string last_game_exe;
      bool focus_exit_on_first_flag = config.focus_exit_on_first;
      int exit_timeout_secs = config.exit_timeout_secs;

      auto snapshot_game_info = [&]() {
        std::lock_guard<std::mutex> lock(game_info_mutex);
        return std::pair {last_install_dir, last_game_exe};
      };

      std::mutex lossless_backup_mutex;
      lossless::lossless_scaling_profile_backup active_lossless_backup {};
      std::atomic<bool> lossless_profiles_applied {false};

      // Restores the user's Lossless Scaling configuration and stops the processes
      // this launcher started. The IPC handler thread and the main loop both reach
      // this, so the applied flag is claimed with an exchange and the backup is
      // taken under the mutex; only the winning caller restores, which keeps a
      // deferred cleanup from double-restoring an already consumed backup.
      auto teardown_lossless_scaling = [&]() {
        if (!lossless_profiles_applied.exchange(false, std::memory_order_acq_rel)) {
          return;
        }
        lossless::lossless_scaling_profile_backup backup;
        {
          std::lock_guard<std::mutex> lock(lossless_backup_mutex);
          backup = active_lossless_backup;
          active_lossless_backup = {};
        }
        auto runtime = lossless::capture_lossless_scaling_state();
        if (!runtime.running_pids.empty()) {
          lossless::lossless_scaling_stop_processes(runtime);
        }
        (void) lossless::lossless_scaling_restore_global_profile(backup);
      };

      std::atomic<bool> watcher_spawned {false};
      std::optional<playnite::cleanup_lease_t> cleanup_lease;
      std::string cleanup_lease_install_dir;

      auto ensure_cleanup_lease = [&]() {
        const auto install_dir = snapshot_game_info().first;
        if (install_dir.empty()) {
          return;
        }
        if (cleanup_lease_install_dir != install_dir) {
          cleanup_lease.reset();
          cleanup_lease_install_dir = install_dir;
        }
        if (!cleanup_lease) {
          if (auto lease = playnite::cleanup_lease_t::acquire(install_dir)) {
            cleanup_lease = std::move(*lease);
            BOOST_LOG(debug) << "Cleanup lease: acquired for install directory '" << install_dir << "'";
          }
        }
      };

      auto send_launch_command = [&](std::string_view reason) -> bool {
        nlohmann::json j;
        j["type"] = "command";
        j["command"] = "launch";
        j["id"] = config.game_id;
        // Playnite is usually already running, so its children would otherwise
        // inherit Playnite's stale environment instead of the per-session
        // environment Sunshine gave this helper.
        j["env"] = launch_environment;
        launch_command_sent.store(true, std::memory_order_release);
        if (!client.send_json_line(j.dump())) {
          BOOST_LOG(error) << "Failed to send launch command for id=" << config.game_id << " reason=" << reason;
          return false;
        }
        BOOST_LOG(info) << "Launch command sent for id=" << config.game_id << " reason=" << reason;
        return true;
      };

      auto schedule_focus_retry = [&]() {
        if (!got_started.load(std::memory_order_acquire)) {
          return;
        }
        if (config.focus_attempts <= 0 || config.focus_timeout_secs <= 0) {
          return;
        }
        auto now = std::chrono::steady_clock::now();
        auto deadline = now + std::chrono::seconds(std::max(1, config.focus_timeout_secs));
        // If autofocus is already active, don't reset the attempt budget.
        // Playnite sends multiple status updates (installDir, exe, gameStarted)
        // in rapid succession for the same game launch, and each one was
        // resetting the budget — effectively making it unlimited.
        // Only extend the deadline and clear the throttle so the next attempt
        // uses the updated target info promptly.
        if (request_game_focus.load(std::memory_order_acquire)) {
          focus_retry_deadline_ms.store(steady_to_millis(deadline), std::memory_order_relaxed);
          next_focus_attempt_ms.store(std::numeric_limits<int64_t>::min(), std::memory_order_relaxed);
          BOOST_LOG(debug) << "Autofocus: extended deadline (budget already active, remaining="
                           << game_focus_successes_left.load(std::memory_order_acquire) << ')';
          return;
        }
        focus_retry_deadline_ms.store(steady_to_millis(deadline), std::memory_order_relaxed);
        next_focus_attempt_ms.store(std::numeric_limits<int64_t>::min(), std::memory_order_relaxed);
        int attempts = std::max(0, config.focus_attempts);
        if (focus_exit_on_first_flag && attempts > 1) {
          attempts = 1;
        }
        game_focus_successes_left.store(attempts, std::memory_order_release);
        game_focus_confirmed.store(false, std::memory_order_release);
        request_game_focus.store(true, std::memory_order_release);
      };

      client.set_message_handler([&](std::span<const uint8_t> bytes) {
        auto msg = platf::playnite::parse(bytes);
        using MT = platf::playnite::MessageType;
        if (msg.type != MT::Status) {
          return;
        }
        auto norm = [&](std::string s) {
          return normalize_game_id(std::move(s));
        };
        if (!msg.status_game_id.empty() && norm(msg.status_game_id) == norm(config.game_id)) {
          const bool launch_was_sent = launch_command_sent.load(std::memory_order_acquire);
          if (!launch_was_sent && msg.status_name != "gameStarted") {
            BOOST_LOG(debug) << "Ignoring pre-launch status '" << msg.status_name << "' for id=" << msg.status_game_id;
            return;
          }
          if (!launch_was_sent) {
            // The plugin synchronizes an already-running game as soon as the launcher
            // connects. That status can arrive before the launcher has sent its launch
            // command, which is the normal resume path rather than stale state.
            BOOST_LOG(info) << "Accepting active game sync before launch for id=" << msg.status_game_id;
          }
          if (!msg.status_install_dir.empty()) {
            bool changed;
            std::string install_dir;
            {
              std::lock_guard<std::mutex> lock(game_info_mutex);
              changed = last_install_dir != msg.status_install_dir;
              last_install_dir = msg.status_install_dir;
              install_dir = last_install_dir;
            }
            if (!watcher_spawned.load()) {
              bool expected = false;
              if (watcher_spawned.compare_exchange_strong(expected, true)) {
                WCHAR selfPath[MAX_PATH] = {};
                GetModuleFileNameW(nullptr, selfPath, ARRAYSIZE(selfPath));
                if (!playnite::spawn_cleanup_watchdog_process(selfPath, install_dir, exit_timeout_secs, false, GetCurrentProcessId())) {
                  watcher_spawned.store(false);
                }
              }
            }
            if (changed) {
              schedule_focus_retry();
            }
          }
          if (!msg.status_exe.empty()) {
            bool changed;
            {
              std::lock_guard<std::mutex> lock(game_info_mutex);
              changed = last_game_exe != msg.status_exe;
              last_game_exe = msg.status_exe;
            }
            if (changed) {
              schedule_focus_retry();
            }
          }
          if (msg.status_name == "gameStarted") {
            got_started.store(true);
            game_stop_pending.store(false, std::memory_order_release);
            launch_retry_budget.store(0, std::memory_order_release);
            schedule_focus_retry();
            // Wait for user to unlock if they launched the game while locked
            bool was_locked = false;
            while (platf::dxgi::is_secure_desktop_active()) {
              if (!was_locked) {
                BOOST_LOG(info) << "Secure desktop detected (user locked screen). Waiting for unlock before applying Lossless Scaling and autofocus...";
                was_locked = true;
              }
              if (should_exit.load()) {
                BOOST_LOG(info) << "Exit requested while waiting for unlock";
                return;
              }
              std::this_thread::sleep_for(500ms);
            }
            if (was_locked) {
              BOOST_LOG(info) << "User unlocked. Proceeding with Lossless Scaling and autofocus.";
            }
            if (lossless_options.enabled && !lossless_profiles_applied.load(std::memory_order_acquire)) {
              if (lossless_options.launch_delay_seconds > 0) {
                BOOST_LOG(info) << "Lossless Scaling: delaying launch by " << lossless_options.launch_delay_seconds << " seconds after gameStarted";
                auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(lossless_options.launch_delay_seconds);
                while (std::chrono::steady_clock::now() < deadline) {
                  if (should_exit.load()) {
                    return;
                  }
                  std::this_thread::sleep_for(200ms);
                }
              }
              auto runtime = lossless::capture_lossless_scaling_state();
              if (!runtime.running_pids.empty()) {
                lossless::lossless_scaling_stop_processes(runtime);
              }
              const auto [install_dir, game_exe] = snapshot_game_info();
              lossless::lossless_scaling_profile_backup backup;
              bool changed = lossless::lossless_scaling_apply_global_profile(lossless_options, install_dir, game_exe, backup);
              {
                std::lock_guard<std::mutex> lock(lossless_backup_mutex);
                if (backup.valid) {
                  active_lossless_backup = backup;
                } else {
                  active_lossless_backup = {};
                }
              }
              lossless_profiles_applied.store(backup.valid, std::memory_order_release);
              DWORD focus_pid = last_confirmed_focus_pid.load(std::memory_order_acquire);
              if (!focus_pid) {
                if (auto selected = lossless::lossless_scaling_select_focus_pid(install_dir, game_exe, std::nullopt)) {
                  focus_pid = *selected;
                }
              }
              lossless::lossless_scaling_restart_foreground(
                runtime,
                changed,
                install_dir,
                game_exe,
                focus_pid,
                lossless_options.legacy_auto_detect
              );
            }
          }
          if (msg.status_name == "stopRequested") {
            BOOST_LOG(info) << "Explicit stop requested for id=" << config.game_id;
            explicit_stop_requested.store(true, std::memory_order_release);
            should_exit.store(true, std::memory_order_release);
            request_game_focus.store(false, std::memory_order_release);
            game_focus_confirmed.store(false, std::memory_order_release);
            game_focus_successes_left.store(0, std::memory_order_release);
            lossless_refocus_pending.store(false, std::memory_order_release);
            focus_retry_deadline_ms.store(0, std::memory_order_relaxed);
            next_focus_attempt_ms.store(std::numeric_limits<int64_t>::min(), std::memory_order_relaxed);
            teardown_lossless_scaling();
            return;
          }
          if (msg.status_name == "gameStopped") {
            if (!got_started.load(std::memory_order_acquire)) {
              int retries_left = launch_retry_budget.load(std::memory_order_acquire);
              while (retries_left > 0) {
                if (launch_retry_budget.compare_exchange_weak(retries_left, retries_left - 1, std::memory_order_acq_rel)) {
                  BOOST_LOG(info) << "Received gameStopped before gameStarted; retrying launch for id=" << config.game_id
                                  << " retries_left=" << (retries_left - 1);
                  if (!send_launch_command("retry-after-prestart-stop")) {
                    should_exit.store(true);
                  }
                  return;
                }
              }
              BOOST_LOG(debug) << "Ignoring gameStopped before fresh gameStarted for id=" << msg.status_game_id
                               << " (retry budget exhausted)";
              return;
            }
            // Treat the plugin notification as a request to verify that the
            // launched process is actually gone. Some launchers emit a
            // transient stop while handing off to the real game process.
            game_stop_pending.store(true, std::memory_order_release);
            request_game_focus.store(false, std::memory_order_release);
            game_focus_confirmed.store(false, std::memory_order_release);
            game_focus_successes_left.store(0, std::memory_order_release);
            lossless_refocus_pending.store(false, std::memory_order_release);
            focus_retry_deadline_ms.store(0, std::memory_order_relaxed);
            next_focus_attempt_ms.store(std::numeric_limits<int64_t>::min(), std::memory_order_relaxed);
            // Process verification is deferred, but the Lossless Scaling overrides are
            // not: Sunshine force-kills this process group at exit_timeout (10s by
            // default), which is shorter than the verification grace, so a deferred
            // restore would never run and would leave the user's Settings.xml mutated
            // and LosslessScaling.exe alive. Restore now; a resumed game re-applies on
            // the next gameStarted.
            teardown_lossless_scaling();
          }
        }
      });

      client.set_connected_handler([&]() {
        try {
          nlohmann::json hello;
          hello["type"] = "hello";
          hello["role"] = "launcher";
          hello["pid"] = static_cast<uint32_t>(GetCurrentProcessId());
          hello["mode"] = "standard";
          if (!config.game_id.empty()) {
            hello["gameId"] = config.game_id;
          }
          client.send_json_line(hello.dump());
        } catch (...) {}
      });

      client.start();

      if (!config.game_id.empty()) {
        ensure_playnite_open();
      }

      auto start_deadline = std::chrono::steady_clock::now() + std::chrono::minutes(2);
      while (!client.is_active() && std::chrono::steady_clock::now() < start_deadline) {
        std::this_thread::sleep_for(50ms);
      }
      if (!client.is_active()) {
        BOOST_LOG(error) << "IPC did not become active; exiting";
        client.stop();
        return 3;
      }

      if (!send_launch_command("initial")) {
        client.stop();
        return 3;
      }

      // Wait for user to unlock if they launched the game while locked
      bool was_initially_locked = false;
      while (platf::dxgi::is_secure_desktop_active()) {
        ensure_cleanup_lease();
        if (!was_initially_locked) {
          BOOST_LOG(info) << "Secure desktop detected at launch (user locked screen). Waiting for unlock before proceeding...";
          was_initially_locked = true;
        }
        if (should_exit.load()) {
          BOOST_LOG(info) << "Exit requested while waiting for unlock at launch";
          client.stop();
          return 0;
        }
        std::this_thread::sleep_for(500ms);
      }
      if (was_initially_locked) {
        BOOST_LOG(info) << "User unlocked. Proceeding with game launch.";
      }

      if (config.focus_attempts > 0 && config.focus_timeout_secs > 0) {
        if (got_started.load()) {
          schedule_focus_retry();
          BOOST_LOG(info) << "Autofocus scheduled after launch";
        } else {
          BOOST_LOG(info) << "Autofocus deferred until gameStarted signal";
        }
      }

      auto restart_lossless_after_refocus = [&]() {
        if (!lossless_options.enabled || !lossless_profiles_applied.load(std::memory_order_acquire)) {
          return;
        }
        const auto [install_dir, game_exe] = snapshot_game_info();
        auto runtime = lossless::capture_lossless_scaling_state();
        if (!runtime.running_pids.empty()) {
          lossless::lossless_scaling_stop_processes(runtime);
        }
        DWORD focus_pid = last_confirmed_focus_pid.load(std::memory_order_acquire);
        if (!focus_pid) {
          if (auto selected = lossless::lossless_scaling_select_focus_pid(install_dir, game_exe, std::nullopt)) {
            focus_pid = *selected;
          }
        }
        BOOST_LOG(info) << "Autofocus: refocus detected; restarting Lossless Scaling";
        lossless::lossless_scaling_restart_foreground(
          runtime,
          true,
          install_dir,
          game_exe,
          focus_pid,
          lossless_options.legacy_auto_detect
        );
      };

      auto normalize_path = [](std::wstring value) {
        for (auto &ch : value) {
          if (ch == L'/') {
            ch = L'\\';
          }
          ch = static_cast<wchar_t>(std::towlower(ch));
        }
        return value;
      };

      auto path_equals = [&](const std::wstring &lhs, const std::wstring &rhs) -> bool {
        if (lhs.empty() || rhs.empty()) {
          return false;
        }
        return normalize_path(lhs) == normalize_path(rhs);
      };

      auto path_starts_with_dir = [&](const std::wstring &path, const std::wstring &dir) -> bool {
        if (path.empty() || dir.empty()) {
          return false;
        }
        auto normalized_path = normalize_path(path);
        auto normalized_dir = normalize_path(dir);
        if (!normalized_dir.empty() && normalized_dir.back() != L'\\') {
          normalized_dir.push_back(L'\\');
        }
        if (normalized_path.size() < normalized_dir.size()) {
          return false;
        }
        return normalized_path.compare(0, normalized_dir.size(), normalized_dir) == 0;
      };

      auto process_path_matches_game = [&](const std::wstring &image_path, const std::string &install_dir, const std::string &game_exe) {
        if (image_path.empty()) {
          return false;
        }
        if (!game_exe.empty()) {
          try {
            if (path_equals(image_path, platf::dxgi::utf8_to_wide(game_exe))) {
              return true;
            }
          } catch (...) {
          }
        }
        if (!install_dir.empty()) {
          try {
            if (path_starts_with_dir(image_path, platf::dxgi::utf8_to_wide(install_dir))) {
              return true;
            }
          } catch (...) {
          }
        }
        return false;
      };

      auto process_matches_game = [&](DWORD pid, const std::string &install_dir, const std::string &game_exe) {
        if (!pid) {
          return false;
        }
        std::wstring image_path;
        return focus::get_process_image_path(pid, image_path) &&
               process_path_matches_game(image_path, install_dir, game_exe);
      };

      auto foreground_pid = [&]() -> DWORD {
        HWND fg = GetForegroundWindow();
        if (!fg || !IsWindowVisible(fg) || IsIconic(fg) || GetWindow(fg, GW_OWNER) != nullptr) {
          return 0;
        }
        DWORD pid = 0;
        GetWindowThreadProcessId(fg, &pid);
        return pid;
      };

      auto is_game_foreground = [&]() -> bool {
        DWORD pid = foreground_pid();
        if (!pid) {
          return false;
        }
        const auto [install_dir, game_exe] = snapshot_game_info();
        return process_matches_game(pid, install_dir, game_exe);
      };

      auto focus_game_after_start = [&]() {
        if (!request_game_focus.load(std::memory_order_acquire)) {
          return;
        }
        if (config.focus_attempts <= 0 || config.focus_timeout_secs <= 0) {
          request_game_focus.store(false, std::memory_order_release);
          return;
        }
        auto now = std::chrono::steady_clock::now();
        int attempts_left = game_focus_successes_left.load(std::memory_order_acquire);
        if (attempts_left <= 0) {
          request_game_focus.store(false, std::memory_order_release);
          return;
        }
        int64_t deadline_ms = focus_retry_deadline_ms.load(std::memory_order_relaxed);
        if (deadline_ms != 0) {
          auto deadline = millis_to_steady(deadline_ms);
          if (now >= deadline) {
            request_game_focus.store(false, std::memory_order_release);
            return;
          }
        }
        int64_t next_ms = next_focus_attempt_ms.load(std::memory_order_relaxed);
        if (next_ms != std::numeric_limits<int64_t>::min()) {
          auto next_time = millis_to_steady(next_ms);
          if (now < next_time) {
            return;
          }
        }
        auto consume_focus_confirmation = [&](const char *source, DWORD confirmed_pid) {
          bool first_confirmation = !game_focus_confirmed.exchange(true, std::memory_order_acq_rel);
          had_focus_success.store(true, std::memory_order_release);
          if (confirmed_pid) {
            last_confirmed_focus_pid.store(confirmed_pid, std::memory_order_release);
          }
          if (lossless_refocus_pending.exchange(false, std::memory_order_acq_rel)) {
            restart_lossless_after_refocus();
          }
          if (!first_confirmation) {
            next_focus_attempt_ms.store(steady_to_millis(now + 1s), std::memory_order_relaxed);
            return;
          }
          if (focus_exit_on_first_flag) {
            BOOST_LOG(info) << "Autofocus: focus confirmed for launched game via " << source << " (exit-on-first)";
            request_game_focus.store(false, std::memory_order_release);
            return;
          }
          int left = game_focus_successes_left.fetch_sub(1, std::memory_order_acq_rel) - 1;
          BOOST_LOG(info) << "Autofocus: focus confirmed for launched game via " << source << " (remaining=" << left << ')';
          if (left <= 0) {
            request_game_focus.store(false, std::memory_order_release);
          }
          next_focus_attempt_ms.store(steady_to_millis(now + 1s), std::memory_order_relaxed);
        };
        bool visible = is_game_foreground();
        if (visible) {
          consume_focus_confirmation("foreground", foreground_pid());
          return;
        }
        game_focus_confirmed.store(false, std::memory_order_release);
        if (had_focus_success.load(std::memory_order_acquire)) {
          lossless_refocus_pending.store(true, std::memory_order_release);
        }
        int remaining = config.focus_timeout_secs;
        if (deadline_ms != 0) {
          auto deadline = millis_to_steady(deadline_ms);
          remaining = static_cast<int>(std::chrono::duration_cast<std::chrono::seconds>(deadline - now).count());
        }
        if (remaining <= 0) {
          request_game_focus.store(false, std::memory_order_release);
          return;
        }
        int slice = std::clamp(remaining, 1, 3);
        bool applied = false;
        DWORD confirmed_focus_pid = 0;
        auto cancel = [&]() {
          return should_exit.load();
        };
        const auto [install_dir, game_exe] = snapshot_game_info();
        if (!applied && !game_exe.empty()) {
          try {
            std::wstring wexe = platf::dxgi::utf8_to_wide(game_exe);
            std::filesystem::path p = wexe;
            std::wstring base = p.filename().wstring();
            if (!base.empty()) {
              applied = focus::focus_process_by_name_extended(base.c_str(), 1, slice, true, cancel, &confirmed_focus_pid);
            }
          } catch (...) {
          }
        }
        if (!applied && !install_dir.empty()) {
          try {
            std::wstring wdir = platf::dxgi::utf8_to_wide(install_dir);
            applied = focus::focus_by_install_dir_extended(wdir, 1, slice, true, cancel, &confirmed_focus_pid);
          } catch (...) {
          }
        }
        DWORD confirmed_pid = applied ? confirmed_focus_pid : 0;
        bool confirmed = confirmed_pid != 0;
        if (confirmed) {
          consume_focus_confirmation("focus-attempt", confirmed_pid);
        } else {
          game_focus_confirmed.store(false, std::memory_order_release);
          if (applied) {
            BOOST_LOG(debug) << "Autofocus: focus helper returned success without a confirmed PID";
          }
        }
        next_focus_attempt_ms.store(steady_to_millis(now + 1s), std::memory_order_relaxed);
      };

      auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(config.timeout_sec);
      // While the plugin connection is active, gameStarted is authoritative.
      // A gameStopped notification starts local process verification rather
      // than tearing the session down immediately, because launch handoffs can
      // emit a transient stop while the actual game remains alive.
      std::optional<std::chrono::steady_clock::time_point> game_missing_since;
      std::optional<std::chrono::steady_clock::time_point> game_verification_deadline;
      const auto game_missing_grace = std::chrono::seconds(15);
      const auto game_verification_limit = std::chrono::seconds(std::max(30, config.timeout_sec));
      while (!should_exit.load()) {
        ensure_cleanup_lease();
        focus_game_after_start();
        if (!got_started.load() && std::chrono::steady_clock::now() >= deadline) {
          break;
        }
        if (got_started.load()) {
          const bool verify_game_processes =
            !client.is_active() || game_stop_pending.load(std::memory_order_acquire);
          if (!verify_game_processes) {
            game_missing_since.reset();
            game_verification_deadline.reset();
            std::this_thread::sleep_for(250ms);
            continue;
          }
          const auto now = std::chrono::steady_clock::now();
          const auto [install_dir, game_exe] = snapshot_game_info();
          bool any_game_proc = false;
          const auto confirmed_focus_pid = last_confirmed_focus_pid.load(std::memory_order_acquire);
          if (confirmed_focus_pid && process_matches_game(confirmed_focus_pid, install_dir, game_exe)) {
            any_game_proc = true;
          }
          if (!install_dir.empty()) {
            try {
              std::wstring wdir = platf::dxgi::utf8_to_wide(install_dir);
              if (!focus::find_pids_under_install_dir_sorted(wdir, false).empty()) {
                any_game_proc = true;
              }
            } catch (...) {
            }
          }
          if (!any_game_proc && !game_exe.empty()) {
            try {
              std::wstring wexe = platf::dxgi::utf8_to_wide(game_exe);
              std::filesystem::path p(wexe);
              std::wstring base = p.filename().wstring();
              if (!base.empty()) {
                for (const auto pid : platf::dxgi::find_process_ids_by_name(base.c_str())) {
                  if (process_matches_game(pid, install_dir, game_exe)) {
                    any_game_proc = true;
                    break;
                  }
                }
              }
            } catch (...) {
            }
          }
          if (any_game_proc) {
            game_missing_since.reset();
            game_verification_deadline = now + game_verification_limit;
          } else {
            if (!game_verification_deadline) {
              game_verification_deadline = now + game_verification_limit;
            } else if (now >= *game_verification_deadline) {
              BOOST_LOG(warning) << "Game process verification deadline reached; proceeding to cleanup";
              should_exit.store(true);
              break;
            }
            if (!game_missing_since) {
              game_missing_since = now;
            } else if (now - *game_missing_since >= game_missing_grace) {
              BOOST_LOG(info) << "Game processes have exited; proceeding to cleanup";
              should_exit.store(true);
              break;
            }
          }
        }
        std::this_thread::sleep_for(250ms);
      }

      if (!should_exit.load()) {
        BOOST_LOG(warning) << (got_started.load() ? "Timeout after start unexpectedly; exiting" : "Timeout waiting for game start; exiting");
      }

      request_game_focus.store(false, std::memory_order_release);
      game_focus_confirmed.store(false, std::memory_order_release);
      game_focus_successes_left.store(0, std::memory_order_release);
      lossless_refocus_pending.store(false, std::memory_order_release);
      had_focus_success.store(false, std::memory_order_release);
      last_confirmed_focus_pid.store(0, std::memory_order_release);
      focus_retry_deadline_ms.store(0, std::memory_order_relaxed);
      next_focus_attempt_ms.store(std::numeric_limits<int64_t>::min(), std::memory_order_relaxed);

      const auto install_dir = snapshot_game_info().first;
      if (explicit_stop_requested.load(std::memory_order_acquire)) {
        if (!install_dir.empty()) {
          try {
            BOOST_LOG(info) << "Explicit stop requested; beginning graceful cleanup immediately";
            cleanup::cleanup_graceful_then_forceful_in_dir(
              platf::dxgi::utf8_to_wide(install_dir),
              exit_timeout_secs
            );
          } catch (...) {
            BOOST_LOG(warning) << "Explicit stop requested; immediate graceful cleanup failed";
          }
        } else {
          BOOST_LOG(warning) << "Explicit stop requested without an install directory; falling back to cleanup watcher";
        }
      }

      BOOST_LOG(info) << "Playnite launcher is scheduling fallback cleanup and exiting";
      if (!install_dir.empty() && !watcher_spawned.load()) {
        WCHAR selfPath[MAX_PATH] = {};
        GetModuleFileNameW(nullptr, selfPath, ARRAYSIZE(selfPath));
        static_cast<void>(playnite::spawn_cleanup_watchdog_process(selfPath, install_dir, exit_timeout_secs, false, GetCurrentProcessId()));
      }
      teardown_lossless_scaling();
      int exit_code = should_exit.load() ? 0 : 4;
      client.stop();
      return exit_code;
    }

  }  // namespace detail
}  // namespace playnite_launcher