#!/usr/bin/python3
import hashlib
import json
import os
import re
import runpy
import signal
import subprocess
import sys
import tempfile
from pathlib import Path

GAME_TU_DEBUG = "DROIDDECK_GAME_TU_DEBUG"
CHROMIUM_FILES = {"chrome-sandbox", "chrome_100_percent.pak", "v8_context_snapshot.bin"}
SYNC_KEYS = ("BL_SYNC", "BL_SYNC_FALLBACK", "BL_FSYNC_FIRST", "BL_SYNC_MIN_NOFILE", "PROTON_NO_ESYNC", "PROTON_NO_FSYNC", "PROTON_NO_NTSYNC", "WINEESYNC")
SEED_REDISTS = "/usr/local/bin/droiddeck-seed-redists"
PACK_KEYS = ("WINESERVER", "BL_SYNC_PACK", "WINEESYNC", "PROTON_NO_ESYNC", "PROTON_NO_FSYNC")


# Prefixes several games share; the game itself is the Steam app being launched.
SHARED_PREFIXES = {"2929927072"}


def app_number(value):
    if not re.fullmatch(r"-?[0-9]+", value or ""):
        return None
    number = int(value)
    if not -(2**31) <= number < 2**32 or number == 0:
        return None
    return str(number % 2**32)


def game_id(env):
    prefix = env.get("STEAM_COMPAT_DATA_PATH", "").rstrip("/")
    if not prefix:
        return None
    found = app_number(prefix.rsplit("/", 1)[-1])
    if found in SHARED_PREFIXES:
        return app_number(env.get("SteamAppId", "")) or shortcut_number(env.get("SteamGameId", "")) or found
    return found


def shortcut_number(value):
    """The shortcut appid inside Steam's 64-bit shortcut game id `(appid << 32) | 0x02000000`."""
    if not re.fullmatch(r"[0-9]{1,20}", value or ""):
        return None
    number = int(value)
    if number >= 2**64 or number & 0xFFFFFFFF != 0x02000000:
        return None
    return app_number(str(number >> 32))


DOTNET_FEX = {
    "FEX_TSOENABLED": "1",
    "FEX_VECTORTSOENABLED": "1",
    "FEX_MEMCPYSETTSOENABLED": "1",
    "FEX_HALFBARRIERTSOENABLED": "1",
    "FEX_X87REDUCEDPRECISION": "0",
}


def game_files(exe):
    names = set()
    folder = os.path.dirname(exe)
    try:
        with os.scandir(folder) as top:
            for entry in top:
                names.add(entry.name.lower())
                if entry.is_dir(follow_symlinks=False):
                    try:
                        with os.scandir(entry.path) as inner:
                            names.update(e.name.lower() for e in inner)
                    except OSError:
                        pass
    except OSError:
        pass
    return names


class Game:

    def __init__(self, exe, args):
        self.exe = exe
        self.args = args
        self.stem = os.path.splitext(os.path.basename(exe))[0].lower()
        self.files = game_files(exe)

    def has_arg(self, name):
        return any(a == name or a.startswith(name + "=") for a in self.args)


FIXES = []


def fix(function):
    FIXES.append(function)
    return function


def is_godot(game):
    return game.stem + ".pck" in game.files or "godotsharp.dll" in game.files


@fix
def dotnet_memory_ordering(game):
    if "coreclr.dll" in game.files:
        return DOTNET_FEX, [], ".NET runtime: FEX with full TSO"


@fix
def unity_memory_ordering(game):
    if {"unityplayer.so", "gameassembly.so", "unityplayer.dll"} & game.files:
        return {"FEX_TSOENABLED": "1"}, [], "Unity: FEX with TSO"


@fix
def godot_vulkan(game):
    if is_godot(game) and not game.has_arg("--rendering-driver"):
        return {}, ["--rendering-driver", "vulkan"], "Godot: Vulkan renderer"


@fix
def godot_sentry(game):
    sentry = sorted(n[:-4] for n in game.files if n.startswith("libsentry.windows.") and n.endswith(".dll"))
    if is_godot(game) and sentry and ("coreclr.dll" in game.files or "godotsharp.dll" in game.files):
        return {"WINEDLLOVERRIDES": ";".join(n + "=d" for n in sentry)}, [], "Godot: Sentry crash handler off"


@fix
def d3dgear_hook(game):
    if "d3dgear64.dll" in game.files:
        return {"WINEDLLOVERRIDES": "d3dgear64=d"}, [], "D3DGear capture hook off (it stops the game drawing)"


@fix
def supergiant_dotnet_reporter(game):
    if "basecampbugreporter.native.dll" in game.files:
        return {"WINEDLLOVERRIDES": "mscoree=n,b"}, [], "Supergiant: allow the builtin mscoree"


@fix
def deep_rock_gdk_raytracing(game):
    if game.stem == "fsd-wingdk-shipping" and not any(game.has_arg(arg) for arg in ("-raytracing", "-noraytracing")):
        return {}, ["-noraytracing", "-ini:Engine:[/Script/Engine.RendererSettings]:r.RayTracing=False"], "Deep Rock Galactic GDK: ray tracing off (D3D12 pipeline creation fails on Thor)"


@fix
def arkham_city_x87(game):
    if "batmanac.exe" in game.files:
        return {"FEX_X87REDUCEDPRECISION": "0"}, [], "Batman: Arkham City: FEX with full x87 precision"



@fix
def doom_2016_swapchain_images(game):
    if "doomx64vk.exe" in game.files and game.stem in ("doomx64", "doomx64vk"):
        images = {
            "GAMESCOPE_WSI_MIN_IMAGE_COUNT": None,
            "GAMESCOPE_WSI_ENSURE_MIN_IMAGE_COUNT": None,
            "vk_x11_strict_image_count": "true",
        }
        return images, [], "DOOM (2016): the swapchain keeps the image count the game asks for"


@fix
def teardown_vsync(game):
    if game.stem == "teardown":
        return {"vblank_mode": "2"}, [], "Teardown: OpenGL keeps the game's own vsync"


@fix
def protonfixes_off(game):
    if game.stem in ("blackops3", "darksoulsremastered"):
        return {"PROTONFIXES_DISABLE": "1"}, [], "umu-protonfixes off (its ALSA audio and VC++ installer steps fail here)"


@fix
def little_nightmares_threads(game):
    if game.stem == "littlenightmares":
        return {"PROTONFIXES_DISABLE": "1"}, [], "Little Nightmares: umu-protonfixes off (its -onethread puts the game and renderer on one thread)"


def skip_red_launcher(command):
    if len(command) > 2 and os.path.basename(command[2]).lower() == "redprelauncher.exe":
        game = os.path.join(os.path.dirname(command[2]), "bin", "x64", "Cyberpunk2077.exe")
        if os.path.isfile(game):
            print("droiddeck-game-env: REDlauncher skipped", file=sys.stderr)
            return command[:2] + [game] + command[3:]
    return command


UBISOFT_LAUNCHER = "drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher"
UBISOFT_SETTINGS = "drive_c/users/steamuser/AppData/Local/Ubisoft Game Launcher/settings.yaml"


def overlay_off(text):
    lines = text.splitlines()
    try:
        start = next(i for i, line in enumerate(lines) if re.fullmatch(r"overlay:\s*(#.*)?", line))
    except StopIteration:
        if lines and not lines[-1].strip():
            lines.pop()
        return "\n".join(lines + ["overlay:", "  enabled: false", "  warning_enabled: false"]) + "\n"
    end = start + 1
    for index in range(start + 1, len(lines)):
        if lines[index].strip() and not lines[index].startswith("  "):
            break
        if lines[index].strip():
            end = index + 1
    block = lines[start + 1:end]
    for key in ("enabled", "warning_enabled"):
        entry = "  %s: false" % key
        found = [i for i, line in enumerate(block) if line.split(":", 1)[0] == "  " + key]
        if found:
            block[found[0]] = entry
        else:
            block.append(entry)
    return "\n".join(lines[:start + 1] + block + lines[end:]) + "\n"


def ubisoft_overlay_off(env):
    prefix = Path(env.get("STEAM_COMPAT_DATA_PATH", "/nonexistent")) / "pfx"
    if not (prefix / UBISOFT_LAUNCHER).is_dir():
        return
    settings = prefix / UBISOFT_SETTINGS
    try:
        text = settings.read_text(encoding="utf-8") if settings.exists() else ""
        updated = overlay_off(text)
        if updated == text:
            return
        settings.parent.mkdir(parents=True, exist_ok=True)
        handle, staged = tempfile.mkstemp(prefix=".settings-", dir=settings.parent)
        try:
            with os.fdopen(handle, "w", encoding="utf-8") as target:
                target.write(updated)
            if settings.exists():
                os.chmod(staged, settings.stat().st_mode & 0o7777)
            os.replace(staged, settings)
        except BaseException:
            os.unlink(staged)
            raise
        print("droiddeck-game-env: Ubisoft Connect overlay turned off", file=sys.stderr)
    except (OSError, UnicodeDecodeError) as error:
        print("droiddeck-game-env: Ubisoft Connect overlay left as is (%s)" % error, file=sys.stderr)


def ubisoft_composition_off(proton, env):
    prefix = Path(env.get("STEAM_COMPAT_DATA_PATH", "/nonexistent")) / "pfx"
    if not (prefix / UBISOFT_LAUNCHER).is_dir() or not (prefix / "user.reg").is_file():
        return
    try:
        for program in ("upc.exe", "UplayWebCore.exe"):
            key = r"HKCU\Software\Wine\AppDefaults\%s\DllOverrides" % program
            subprocess.run([proton, "runinprefix", "reg.exe", "add", key,
                            "/v", "dcomp", "/t", "REG_SZ", "/d", "", "/f"], env=env, check=True)
        print("droiddeck-game-env: Ubisoft Connect uses CEF without DirectComposition", file=sys.stderr)
    except (OSError, ValueError, subprocess.CalledProcessError) as error:
        print("droiddeck-game-env: Ubisoft Connect composition configuration failed (%s)" % error, file=sys.stderr)


PZ_JVM_FLAGS = ("-XX:+UseG1GC", "-XX:TieredStopAtLevel=1")


def zomboid_vm_args(args):
    updated = [arg for arg in args if not re.fullmatch(r"-XX:\+Use\w*GC", arg) or arg == "-XX:+UseG1GC"]
    for flag in PZ_JVM_FLAGS:
        if flag not in updated:
            updated.append(flag)
    return updated


def zomboid_rewrite(node):
    changed = False
    if isinstance(node, dict):
        for key, value in node.items():
            if key == "vmArgs" and isinstance(value, list) and all(isinstance(arg, str) for arg in value):
                updated = zomboid_vm_args(value)
                if updated != value:
                    node[key] = updated
                    changed = True
            elif isinstance(value, dict):
                changed |= zomboid_rewrite(value)
    return changed


def project_zomboid_jvm(command):
    if len(command) <= 2:
        return
    exe = Path(command[2])
    if not exe.stem.lower().startswith("projectzomboid"):
        return
    settings = exe.with_suffix(".json")
    try:
        with settings.open(encoding="utf-8-sig") as source:
            data = json.load(source)
        if not zomboid_rewrite(data):
            return
        handle, staged = tempfile.mkstemp(prefix=".droiddeck-", dir=settings.parent)
        try:
            with os.fdopen(handle, "w", encoding="utf-8") as target:
                json.dump(data, target, indent=4)
                target.write("\n")
            os.chmod(staged, settings.stat().st_mode & 0o7777)
            os.replace(staged, settings)
        except BaseException:
            os.unlink(staged)
            raise
        print("droiddeck-game-env: Project Zomboid: G1 garbage collector and C1 JIT only (ZGC overheats and stalls here)",
              file=sys.stderr)
    except (OSError, ValueError, AttributeError) as error:
        print("droiddeck-game-env: Project Zomboid JVM settings left as is (%s)" % error, file=sys.stderr)


def offline_without_anticheat(command, env):
    if (env.get("DROIDDECK_ANTICHEAT_OFFLINE") != "1" or len(command) <= 2
            or command[1] != "waitforexitandrun"
            or not re.fullmatch(r"start_protected_game(?:_?x64|_?64)?\.exe",
                                os.path.basename(command[2]), re.IGNORECASE)):
        return command
    folder = os.path.dirname(command[2])
    try:
        with open(os.path.join(folder, "EasyAntiCheat", "settings.json"), encoding="utf-8-sig") as source:
            executable = json.load(source).get("executable")
    except (OSError, ValueError, AttributeError):
        return command
    if not isinstance(executable, str) or not executable:
        return command
    relative = os.path.normpath(executable.replace("\\", "/"))
    game = os.path.join(folder, relative)
    if os.path.isabs(relative) or relative.startswith("..") or not os.path.isfile(game):
        return command
    root = os.path.realpath(folder)
    if os.path.commonpath((root, os.path.realpath(game))) != root:
        return command
    if os.path.normcase(os.path.abspath(game)) == os.path.normcase(os.path.abspath(command[2])):
        return command
    print("droiddeck-game-env: started without Easy Anti-Cheat (offline only)", file=sys.stderr)
    return command[:2] + [game] + command[3:]


def engine_fixes(exe, args):
    game = Game(exe, args)
    env, extra, notes = {}, [], []
    for check in FIXES:
        found = check(game)
        if found:
            for name, value in found[0].items():
                if name == "WINEDLLOVERRIDES" and env.get(name):
                    value = env[name] + ";" + value
                env[name] = value
            extra += found[1]
            notes.append(found[2])
    return env, extra, notes


def win_components(compat, app_id, proton=None, env=None):
    if not compat:
        return ""
    try:
        module = runpy.run_path(str(Path(__file__).resolve().parent / "droiddeck-wincomponents"))
        names, notes = module["apply"](compat.rstrip("/"), app_id, proton=proton, env=env or os.environ)
        for note in notes:
            print("droiddeck-game-env: " + note, file=sys.stderr)
        return module["overrides_value"](names)
    except Exception as error:
        print("droiddeck-game-env: Windows components not applied (%s)" % error, file=sys.stderr)
        return ""


def chromium_app(folder):
    root = os.path.realpath(folder)
    for top, dirs, files in os.walk(root):
        if CHROMIUM_FILES.intersection(files) or CHROMIUM_FILES.intersection(dirs):
            return True
        if os.path.relpath(top, root).count(os.sep) >= 2:
            dirs[:] = []
    return False


def game_gpu(env, keep=False):
    if GAME_TU_DEBUG not in env:
        return env
    result = env.copy()
    value = result.pop(GAME_TU_DEBUG)
    if keep:
        return result
    if value:
        result["TU_DEBUG"] = value
    else:
        result.pop("TU_DEBUG", None)
    return result


def vram_cap(env):
    value = env.get("BL_VRAM_MB", "")
    if not re.fullmatch(r"[1-9][0-9]{0,6}", value):
        return env
    result = env.copy()
    cap = "dxgi.maxDeviceMemory = %d" % int(value)
    own = result.get("DXVK_CONFIG", "").strip().strip(";").strip()
    result["DXVK_CONFIG"] = cap + "; " + own if own else cap
    result["DROIDDECK_VRAM_CAP"] = "1"
    result["DROIDDECK_VRAM_CAP_MB"] = value
    return result


def parse_cores(text):
    cores = set()
    for part in text.replace(" ", "").split(","):
        match = re.fullmatch(r"(\d{1,3})(?:-(\d{1,3}))?", part)
        if not match:
            return None
        first, last = int(match.group(1)), int(match.group(2) or match.group(1))
        if last < first:
            return None
        cores.update(range(first, last + 1))
    return cores or None


def online_cores():
    try:
        with open("/sys/devices/system/cpu/online") as f:
            cores = parse_cores(f.read().strip())
    except OSError:
        cores = None
    return cores or os.sched_getaffinity(0) | set(range(os.cpu_count() or 0))


def game_cores(env):
    text = env.get("DROIDDECK_GAME_CORES", "").strip()
    if not text:
        return
    cores = parse_cores(text)
    chosen = cores & online_cores() if cores else None
    if not chosen:
        print("droiddeck-game-env: ignoring game cores %r" % text, file=sys.stderr)
        return
    try:
        os.sched_setaffinity(0, chosen)
        print("droiddeck-game-env: game cores %s" % ",".join(map(str, sorted(chosen))), file=sys.stderr)
    except OSError as error:
        print("droiddeck-game-env: could not use game cores %r (%s)" % (text, error), file=sys.stderr)


def core_speeds(cores, root="/sys/devices/system/cpu"):
    for name in ("cpu_capacity", "cpufreq/cpuinfo_max_freq"):
        speeds = {}
        for core in cores:
            try:
                with open("%s/cpu%d/%s" % (root, core, name)) as f:
                    speeds[core] = int(f.read().strip())
            except (OSError, ValueError):
                break
        else:
            return speeds
    return None


def fastest_first(cores):
    cores = sorted(cores)
    speeds = core_speeds(cores)
    return sorted(cores, key=lambda core: -speeds[core]) if speeds else cores


def proton_cpu_limit(command, env):
    app_id = env.get("SteamGameId", "")
    if not command or not app_id.isdigit():
        return None
    try:
        if os.path.getsize(command[0]) > 4 * 1024 * 1024:
            return None
        with open(command[0], encoding="utf-8", errors="replace") as f:
            text = f.read()
    except OSError:
        return None
    table = re.search(r"^default_cpu_limit = \{(.*?)^\}", text, re.M | re.S)
    match = table and re.search(r'^\s*"%s"\s*:\s*"?(\w+)' % app_id, table.group(1), re.M)
    if not match:
        return None
    value = match.group(1)
    return int(value) if value.isdigit() else value


def cpu_topology(env, command=()):
    if "PROTON_CPU_TOPOLOGY" in env:
        return
    if "WINE_CPU_TOPOLOGY" in env:
        if env["WINE_CPU_TOPOLOGY"]:
            env["PROTON_CPU_TOPOLOGY"] = env["WINE_CPU_TOPOLOGY"]
        return
    picked = bool(env.get("DROIDDECK_GAME_CORES", "").strip() or env.get("BL_GAME_CPUS", "").strip())
    fastest = env.get("DROIDDECK_FASTEST_CORES", "").strip() != "0" and game_id(env) is not None
    if not (picked or fastest):
        return
    try:
        cores = sorted(os.sched_getaffinity(0))
    except OSError:
        return
    online = online_cores()
    if not cores or cores[-1] >= len(online):
        return
    picked = picked and not set(cores) >= online
    if fastest:
        limit = None if picked else proton_cpu_limit(command, env)
        if isinstance(limit, str):
            return
        cores = fastest_first(cores)[:limit or None]
    elif set(cores) >= online:
        return
    if cores == sorted(online):
        return
    env["WINE_CPU_TOPOLOGY"] = env["PROTON_CPU_TOPOLOGY"] = "%d:%s" % (len(cores), ",".join(map(str, cores)))
    print("droiddeck-game-env: Windows sees %d cores (%s)" % (len(cores), env["WINE_CPU_TOPOLOGY"]), file=sys.stderr)


def gl_vsync(env):
    if "vblank_mode" in env or game_id(env) is None:
        return False
    env["vblank_mode"] = "0"
    return True


def game_images(env):
    if game_id(env) is None:
        return False
    env.setdefault("GAMESCOPE_WSI_MIN_IMAGE_COUNT", "8")
    env.setdefault("GAMESCOPE_WSI_ENSURE_MIN_IMAGE_COUNT", "1")
    return True


def apply_config(env, config, app_id, fixes=None):
    if config.get("version") != 1:
        raise ValueError("unsupported environment configuration")
    result = env.copy()
    entries = dict(config.get("shared", {}))
    for name, value in (fixes or {}).items():
        if name == "WINEDLLOVERRIDES" and entries.get(name, env.get(name)):
            value = entries.get(name, env.get(name)) + ";" + value
        entries[name] = value
    entries.update(config.get("games", {}).get(app_id, {}))
    for name, value in entries.items():
        if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name):
            raise ValueError("invalid environment name")
        if value is None:
            result.pop(name, None)
        elif isinstance(value, str) and "\0" not in value and len(value) <= 8192:
            result[name] = value
        else:
            raise ValueError("invalid environment value")
    game_dxvk = config.get("dxvkConfigGames", {})
    if not isinstance(game_dxvk, dict):
        raise ValueError("invalid game dxvk configuration")
    dxvk = game_dxvk.get(app_id, config.get("dxvkConfig"))
    if dxvk is not None:
        if not isinstance(dxvk, str) or "\0" in dxvk or len(dxvk) > 8192:
            raise ValueError("invalid dxvk configuration")
        if dxvk:
            own = result.get("DXVK_CONFIG", "").strip().rstrip(";").strip()
            result["DXVK_CONFIG"] = own + "; " + dxvk if own else dxvk
    # An open driver pair (PanVK, RADV) on a GPU that is not an Adreno: vkd3d-proton reports the
    # driver's own feature level and shader model. The app-wide 12_2 / 6_9 over-promised what PanVK
    # has, and an Unreal DX12 game took paths that crashed. A game's own setting still wins.
    if env.get("BL_OPEN_DRIVER") == "1":
        game = config.get("games", {}).get(app_id, {}) or {}
        for name in ("VKD3D_FEATURE_LEVEL", "VKD3D_SHADER_MODEL"):
            if name not in game and name in result:
                result.pop(name)
                result["DROIDDECK_VKD3D_CAPS"] = "driver"
    return result


def render_path_defaults(command, env):
    if len(command) < 3 or command[1] != "waitforexitandrun" or Path(command[2]).name.lower() != "spintires.exe":
        return None
    own = env.get("DXVK_CONFIG", "")
    if re.search(r"(?:^|;)\s*dxvk\.tilerMode\s*=", own) or env.get("DXVK_CONFIG_FILE"):
        return None
    if Path("dxvk.conf").is_file() or Path(command[2]).with_name("dxvk.conf").is_file():
        return None
    driver = env.get("VK_DRIVER_FILES", env.get("VK_ICD_FILENAMES", ""))
    if Path(driver).parent.name != "DD-Turnip_v0.4.0":
        return None
    try:
        if Path("/sys/class/kgsl/kgsl-3d0/gpu_model").read_text().strip() != "Adreno722":
            return None
        with open(driver, encoding="utf-8") as source:
            library = json.load(source)["ICD"]["library_path"]
        if Path(library).name != "libvulkan_freedreno.so":
            return None
    except (OSError, ValueError, KeyError, TypeError):
        return None
    own = own.strip().rstrip(";").strip()
    env["DXVK_CONFIG"] = own + "; dxvk.tilerMode = False" if own else "dxvk.tilerMode = False"
    return "Spintires: DXVK tiler heuristics off on Adreno 722"


def bridge_feature_level(env):
    if env.get("BL_VKBRIDGE") != "1" or env.get("VKD3D_FEATURE_LEVEL") not in ("12_1", "12_2"):
        return None
    was = env["VKD3D_FEATURE_LEVEL"]
    env["VKD3D_FEATURE_LEVEL"] = "12_0"
    return "Mali bridge: VKD3D_FEATURE_LEVEL %s capped at 12_0 (no ray tracing or mesh shaders)" % was


GAME_DEBUG_CHANNELS = (
    "timestamp", "pid", "tid", "seh", "unwind", "threadname", "debugstr", "loaddll", "mscoree",
)
WINE_DEBUG_SELECTOR = re.compile(r"(?:(trace|warn|err|fixme))?([+-])([A-Za-z_][A-Za-z0-9_]*)")


def game_debug_channels(*values):
    selectors = {}
    for value in values:
        if not isinstance(value, str) or "\0" in value or len(value) > 8192:
            continue
        for part in value.split(","):
            part = part.strip()
            match = WINE_DEBUG_SELECTOR.fullmatch(part)
            if match is None:
                continue
            key = (match[1] or "", match[3])
            selectors.pop(key, None)
            selectors[key] = part
    for channel in GAME_DEBUG_CHANNELS:
        key = ("", channel)
        selectors.pop(key, None)
        selectors[key] = "+" + channel
    formatted = ",".join(selectors.values())
    if len(formatted) > 8192:
        formatted = ",".join("+" + channel for channel in GAME_DEBUG_CHANNELS)
    return formatted


def apply_game_debug_logs(env, config, debug_dir):
    if not isinstance(config, dict) or config.get("version") != 1 or config.get("gameDebugLogs") is not True:
        return env
    if (not isinstance(debug_dir, str) or "\0" in debug_dir or len(debug_dir) > 8192
            or not os.path.isabs(debug_dir) or not os.path.isdir(debug_dir)):
        print("droiddeck-game-env: game debug logs skipped; session log directory unavailable", file=sys.stderr)
        return env
    result = dict(env)
    result.update({
        "PROTON_LOG": "1",
        "PROTON_LOG_DIR": debug_dir,
        "PROTON_CRASH_REPORT_DIR": debug_dir,
        "DXVK_LOG_PATH": debug_dir,
        "DXVK_LOG_LEVEL": "debug",
        "VKD3D_DEBUG": "info",
        "WINEDEBUG": game_debug_channels(env.get("WINEDEBUG"), env.get("PROTON_LOG")),
    })
    return result


def sync_config(config, app_id):
    games = config.get("games", {})
    return {
        "version": config.get("version"),
        "shared": {name: value for name, value in config.get("shared", {}).items() if name in SYNC_KEYS},
        "games": {app_id: {name: value for name, value in games.get(app_id, {}).items() if name in SYNC_KEYS}},
    }


def load_sync():
    try:
        return runpy.run_path(str(Path(__file__).resolve().parent / "droiddeck-esync"))
    except Exception as error:
        print("droiddeck-game-env: sync selection failed (%s); stock launch" % error, file=sys.stderr)
        return None


def select_sync(command, env, sync=None):
    try:
        sync = sync if sync is not None else load_sync()
        if sync is None:
            return command, env
        chosen, chosen_env = sync["select"](command, env)
        if chosen and all(isinstance(value, str) for value in chosen) and isinstance(chosen_env, dict):
            return list(chosen), dict(chosen_env)
    except Exception as error:
        print("droiddeck-game-env: sync selection failed (%s); stock launch" % error, file=sys.stderr)
    return command, env


def record_stock(sync, command, env):
    try:
        if sync is not None and "record" in sync:
            sync["record"](command, env)
    except Exception as error:
        print("droiddeck-game-env: stock launch not recorded (%s)" % error, file=sys.stderr)


def without_pack(chosen_env, env):
    result = dict(chosen_env)
    for name in PACK_KEYS:
        if name in env:
            result[name] = env[name]
        else:
            result.pop(name, None)
    return result


SSBS_LIB = "/usr/local/lib/libssbs.so"


def force_ssbs(command, env):
    if env.get("DROIDDECK_FORCE_SSBS", "").lower() not in ("1", "on", "true", "yes"):
        return env
    proton = os.path.dirname(command[0]) if command else ""
    if not os.path.isdir(os.path.join(proton, "files/lib/wine/aarch64-unix")):
        print("droiddeck-game-env: force SSBS asked for, but this Proton is not an ARM64 one; left off", file=sys.stderr)
        return env
    if not os.path.isfile(SSBS_LIB):
        print("droiddeck-game-env: force SSBS asked for, but %s is missing; left off" % SSBS_LIB, file=sys.stderr)
        return env
    parts = [p for p in re.split(r"[:\s]+", env.get("LD_PRELOAD", "")) if p]
    if SSBS_LIB not in parts:
        env = dict(env, LD_PRELOAD=":".join(parts + [SSBS_LIB]))
        print("droiddeck-game-env: force SSBS on (libssbs.so preloaded)", file=sys.stderr)
    return env


EVALUATOR_SCRIPT = re.compile(r"(?:^|[\\/])evaluatorscript_[0-9]+\.vdf$", re.IGNORECASE)


def mark_install_scripts(command, env):
    if not any(arg.lower().replace("\\", "/").endswith("iscriptevaluator.exe") for arg in command):
        return []
    root = env.get("BL_X64_STEAM_ROOT") or env.get("STEAM_COMPAT_CLIENT_INSTALL_PATH") or str(Path.home() / ".local/share/Steam")
    scripts = [str(Path(root) / arg.replace("\\", "/")) for arg in command if EVALUATOR_SCRIPT.search(arg)]
    scripts = [path for path in scripts if os.path.isfile(path)]
    if not scripts:
        return []
    try:
        subprocess.run([SEED_REDISTS, root], env=dict(env, DROIDDECK_SEED_SCRIPTS="\n".join(scripts)),
                       stdout=sys.stderr, timeout=60, check=False)
    except (OSError, subprocess.SubprocessError) as error:
        print("droiddeck-game-env: install scripts not marked (%s)" % error, file=sys.stderr)
    return scripts


INSTALL_STAMP = ".droiddeck-install-scripts"
VDF_TOKEN = re.compile(r'"((?:[^"\\]|\\.)*)"|([{}])')


def read_vdf(text):
    tokens = iter(VDF_TOKEN.findall(text))

    def block():
        items = []
        for word, brace in tokens:
            if brace == "}":
                return items
            if brace:
                raise ValueError("a block without a name")
            value, opens = next(tokens, ("", "}"))
            if opens == "}":
                raise ValueError("a name without a value")
            items.append((re.sub(r"\\(.)", r"\1", word), block() if opens else re.sub(r"\\(.)", r"\1", value)))
        return items

    return block()


def run_keys(items, appid=""):
    appid = next((value for name, value in items if name.lower() == "appid" and isinstance(value, str)), appid)
    found = set()
    for name, value in items:
        if not isinstance(value, list):
            continue
        if any(isinstance(v, str) and re.fullmatch(r"process [0-9]+", n.lower()) for n, v in value):
            key = next((v for n, v in value if n.lower() == "hasrunkey" and isinstance(v, str)),
                       "HKEY_LOCAL_MACHINE\\Software\\Valve\\Steam\\Apps\\" + appid)
            found.add((key.lower(), name.lower()))
        found |= run_keys(value, appid)
    return found


def registry_values(path):
    found, section = set(), None
    with open(path, encoding="utf-8", errors="replace") as reg:
        for line in reg:
            if line.startswith("["):
                section = line[1:line.find("]")].replace("\\\\", "\\").lower() if "]" in line else None
            elif section and line.startswith('"') and '"=' in line:
                found.add((section, line[1:line.index('"=')].replace("\\\\", "\\").lower()))
    return found


def has_run(key, value, values):
    path = key[len("hkey_local_machine\\"):] if key.startswith("hkey_local_machine\\") else None
    if path is None:
        return False
    views = [path]
    if path.startswith("software\\"):
        views.append("software\\wow6432node\\" + path[len("software\\"):])
    return any((view, value) in values for view in views)


def scripts_digest(scripts):
    digest = hashlib.sha256()
    for path in sorted(scripts):
        digest.update(os.path.basename(path).encode() + b"\0")
        digest.update(Path(path).read_bytes())
    return digest.hexdigest()


def install_scripts_done(scripts, env):
    prefix = Path(env.get("STEAM_COMPAT_DATA_PATH", "/nonexistent")) / "pfx"
    try:
        if (prefix / INSTALL_STAMP).read_text().strip() != scripts_digest(scripts):
            return False
        keys = set()
        for path in scripts:
            keys |= run_keys(read_vdf(Path(path).read_text(encoding="utf-8", errors="replace")))
        values = registry_values(prefix / "system.reg")
    except (OSError, ValueError):
        return False
    return bool(keys) and all(has_run(key, value, values) for key, value in keys)


def record_install_scripts(digest, env):
    prefix = Path(env.get("STEAM_COMPAT_DATA_PATH", "/nonexistent")) / "pfx"
    try:
        if prefix.is_dir():
            staged = prefix / (INSTALL_STAMP + ".tmp")
            staged.write_text(digest + "\n")
            staged.replace(prefix / INSTALL_STAMP)
    except OSError as error:
        print("droiddeck-game-env: install scripts not recorded (%s)" % error, file=sys.stderr)


def directaudio_settings(env):
    if "directaudio" not in env.get("WINE_AUDIO_DRIVER", "").split(","):
        return env
    for name, value in list(env.items()):
        if name.startswith("AUDIO_DIRECT_"):
            env["BANNER_" + name] = value
    return env


DIRT_GAMES = {"dirt3_game": "dirt3", "showdown": "dirtshowdown", "showdown_avx": "dirtshowdown"}
DIRT_WS2 = "/usr/local/lib/droiddeck-dirt/ws2_32.dll"
DIRT_MARKER = b"droiddeck-dirt-ws2_32"
DIRT_FIXED = "showdown_avx.exe".encode("utf-16-le")
DIRT_SAVED = ".droiddeck-original"


def dirt_telemetry_on(prefix, game):
    try:
        folders = [f for f in (Path(prefix) / "pfx/drive_c/users/steamuser/Documents/My Games").iterdir()
                   if f.name.lower().replace(" ", "") == game]
    except OSError:
        return False
    for folder in folders:
        try:
            text = (folder / "hardwaresettings/hardware_settings_config.xml").read_text(encoding="latin-1")
        except OSError:
            continue
        for tag in ("udp", "motion"):
            element = re.search(r"<%s\s[^>]*>" % tag, text)
            if element:
                attrs = dict(re.findall(r'\s(\w+)="([^"]*)"', element.group(0)))
                return attrs.get("enabled") == "true" and attrs.get("ip", "").lower() not in ("", "dbox")
    return False


def proton_ws2(proton):
    return Path(proton).parent / "files/lib/wine/i386-windows/ws2_32.dll"


def proton_has_dirt_fix(proton):
    try:
        return DIRT_FIXED in proton_ws2(proton).read_bytes()
    except OSError:
        return False


WINE_DLL_DIRS = ("/lib/wine/i386-windows", "/lib/wine/x86_64-windows", "/lib/wine/aarch64-windows", "/lib/wine",
                 "/lib/wine/fakedlls", "/lib64/wine", "/lib64/wine/fakedlls", "/lib64/wine/x86_64-windows")


FOREIGN_SUFFIX = ".foreign-arch"
PE = runpy.run_path(str(Path(__file__).resolve().parent / "droiddeck-wine-pe"))
pe_machine = PE["pe_machine"]


def repair_foreign_system_dlls(prefix, proton, env=None):
    dist = Path(proton).parent / "files/lib/wine"
    windows = Path(prefix) / "pfx/drive_c/windows"
    machines_by_folder = PE["proton_pe_machines"](proton, env or {})
    if machines_by_folder is None:
        return []
    repaired = []
    for folder, (libdir, machines) in machines_by_folder.items():
        builtins = dist / libdir
        try:
            builtin_names = {name.lower(): name for name in os.listdir(builtins)}
        except OSError:
            continue
        try:
            names = sorted(os.listdir(windows / folder))
        except OSError:
            continue
        for name in names:
            path = windows / folder / name
            builtin_name = builtin_names.get(name.lower())
            source = builtins / builtin_name if builtin_name else builtins / name
            if path.is_symlink() or not name.lower().endswith(".dll") or not source.is_file() or not path.is_file():
                continue
            machine = pe_machine(path)
            if machine is None or PE["machine_compatible"](path, machines, machine):
                continue
            link = windows / folder / (name + ".bl-new")
            backup = windows / folder / (name + FOREIGN_SUFFIX)
            serial = 1
            while os.path.lexists(backup):
                backup = windows / folder / (name + FOREIGN_SUFFIX + ".%d" % serial)
                serial += 1
            try:
                os.symlink(os.path.relpath(source, path.parent), link)
                os.replace(path, backup)
                try:
                    os.replace(link, path)
                except OSError:
                    if not os.path.lexists(path) and os.path.lexists(backup):
                        os.replace(backup, path)
                    raise
            except OSError:
                if link.is_symlink():
                    link.unlink()
                continue
            repaired.append("%s/%s (machine 0x%x)" % (folder, name, machine))
    return repaired


def is_wine_dll_link(path):
    try:
        return os.path.islink(path) and os.path.dirname(os.readlink(path)).endswith(WINE_DLL_DIRS)
    except OSError:
        return False


def prefix_ws2(prefix):
    return Path(prefix) / "pfx/drive_c/windows/syswow64/ws2_32.dll"


def is_dirt_stand_in(path):
    try:
        return not path.is_symlink() and DIRT_MARKER in path.read_bytes()
    except OSError:
        return False


def dirt_ws2_wanted(command, env):
    prefix = env.get("STEAM_COMPAT_DATA_PATH", "").rstrip("/")
    if len(command) < 3 or not prefix or not os.path.isfile(DIRT_WS2):
        return None
    game = DIRT_GAMES.get(os.path.splitext(os.path.basename(command[2]))[0].lower())
    if not game or not dirt_telemetry_on(prefix, game) or proton_has_dirt_fix(command[0]):
        return None
    return prefix if os.path.lexists(prefix_ws2(prefix)) else None


def swap_dirt_ws2(prefix):
    target = prefix_ws2(prefix)
    saved = target.with_name(target.name + DIRT_SAVED)
    data = Path(DIRT_WS2).read_bytes()
    if not is_dirt_stand_in(target):
        if os.path.lexists(saved):
            os.unlink(saved)
        os.replace(target, saved)
    staged = target.with_name(target.name + ".droiddeck-tmp")
    staged.write_bytes(data)
    os.replace(staged, target)


def restore_dirt_ws2(prefix, proton):
    target = prefix_ws2(prefix)
    saved = target.with_name(target.name + DIRT_SAVED)
    own = proton_ws2(proton)
    had_saved = os.path.lexists(saved)
    if had_saved:
        if os.path.lexists(target) and not is_dirt_stand_in(target):
            os.unlink(saved)
            return False
        if not own.is_file() or not is_wine_dll_link(saved) or os.path.realpath(saved) == os.path.realpath(own):
            os.replace(saved, target)
            return True
        os.unlink(saved)
    if own.is_file() and (is_dirt_stand_in(target) or (had_saved and not os.path.lexists(target))):
        link = target.with_name(target.name + ".droiddeck-tmp")
        if os.path.lexists(link):
            os.unlink(link)
        os.symlink(own, link)
        os.replace(link, target)
        return True
    return False


EGO_RESTRICTIONS = "system/hardware_settings_restrictions.xml"
EGO_CORE_RULE = re.compile(r'[ \t]*<restrict_if name="cores" op="(int_[a-z]+)" value="(\d+)">.*?</restrict_if>[ \t]*\r?\n', re.S)
EGO_OPS = {"int_eq": int.__eq__, "int_ne": int.__ne__, "int_ge": int.__ge__, "int_gt": int.__gt__,
           "int_le": int.__le__, "int_lt": int.__lt__}


def windows_core_count(env):
    count = (env.get("WINE_CPU_TOPOLOGY") or "").split(":", 1)[0]
    return int(count) if count.isascii() and count.isdigit() and int(count) > 0 else None


def ego_core_plan(command, env):
    count = windows_core_count(env)
    if not count or len(command) < 3:
        return None
    folder = Path(command[2]).parent
    path = folder / EGO_RESTRICTIONS
    try:
        raw = path.read_bytes()
    except OSError:
        return None
    text = raw.decode("latin-1")
    start, end = text.find("<cpu>"), text.find("</cpu>")
    if start < 0 or end < start:
        return None
    rules = list(EGO_CORE_RULE.finditer(text, start, end))
    if not rules or any(EGO_OPS.get(rule.group(1), lambda a, b: True)(count, int(rule.group(2))) for rule in rules):
        return None
    smaller = [rule for rule in rules if rule.group(1) == "int_eq" and int(rule.group(2)) < count]
    if not smaller:
        return None
    source = max(smaller, key=lambda rule: int(rule.group(2)))
    worker_map = re.search(r'workerMapFile="([^"]+)"', source.group(0))
    if worker_map and not (folder / worker_map.group(1)).is_file():
        return None
    plan = source.group(0).replace('value="%s"' % source.group(2), 'value="%d"' % count, 1)
    saved = path.with_name(path.name + DIRT_SAVED)
    if not os.path.lexists(saved):
        saved.write_bytes(raw)
    handle, staged = tempfile.mkstemp(prefix=path.name + ".", suffix=".droiddeck-tmp", dir=str(path.parent))
    try:
        with os.fdopen(handle, "wb") as out:
            out.write((text[:source.end()] + plan + text[source.end():]).encode("latin-1"))
        os.replace(staged, path)
    except BaseException:
        if os.path.lexists(staged):
            os.unlink(staged)
        raise
    return "%s had no thread plan for %d cores; added a copy of its %s-core plan" % (EGO_RESTRICTIONS, count, source.group(2))


def wait_forwarding_signals_raw(process):
    def forward(signum, frame):
        try:
            process.send_signal(signum)
        except OSError:
            pass

    previous = {signum: signal.signal(signum, forward) for signum in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP)}
    try:
        code = process.wait()
    finally:
        for signum, handler in previous.items():
            signal.signal(signum, handler)
    return code


def wait_forwarding_signals(process):
    code = wait_forwarding_signals_raw(process)
    return 128 - code if code < 0 else code


def diagnostic_game_event(event, app_id, values=None):
    fields = ["event=" + event, "app_id=" + app_id]
    fields.extend("%s=%s" % item for item in (values or {}).items())
    print("DROIDDECK_DIAGNOSTICS " + " ".join(fields), file=sys.stderr, flush=True)


def diagnostic_game_exit(app_id, raw_code, source=None, wrapper_status=None):
    normalized = 128 - raw_code if raw_code < 0 else raw_code
    values = {
        "raw_status": raw_code,
        "normalized_status": normalized,
        "wrapper_status": normalized if wrapper_status is None else wrapper_status,
        "signal": -raw_code if raw_code < 0 else 0,
    }
    if source is not None:
        values["source"] = source
    diagnostic_game_event("game_exit", app_id, values)


def start(launch, launch_env, digest, after=None, diagnostic_app_id=None):
    if len(launch) > 1 and launch[1] == "waitforexitandrun":
        ubisoft_composition_off(launch[0], launch_env)
    launch, launch_env = system_graphics_launch(launch, launch_env)
    if after or diagnostic_app_id is not None:
        process = subprocess.Popen(launch, env=launch_env)
        if diagnostic_app_id is not None:
            diagnostic_game_event("game_start", diagnostic_app_id)
        try:
            raw_code = wait_forwarding_signals_raw(process)
        finally:
            if after:
                try:
                    after()
                except OSError as error:
                    print("droiddeck-game-env: DiRT telemetry fix: prefix ws2_32 not put back (%s); the next launch does it" % error,
                          file=sys.stderr)
        code = 128 - raw_code if raw_code < 0 else raw_code
        if digest and after is None and code == 0:
            record_install_scripts(digest, launch_env)
        if diagnostic_app_id is not None:
            diagnostic_game_exit(diagnostic_app_id, raw_code)
        return code
    if digest:
        code = subprocess.run(launch, env=launch_env, check=False).returncode
        if code == 0:
            record_install_scripts(digest, launch_env)
        return code
    return os.execvpe(launch[0], launch, launch_env)


class SystemGraphicsError(ValueError):
    pass


def system_graphics_launch(command, env):
    """Use the APK's PowerVR compatibility files only for actual Proton game launches."""
    runtime = env.get("DROIDDECK_POWERVR_RUNTIME")
    if not runtime or not game_id(env) or len(command) < 3 or command[1] != "waitforexitandrun":
        return command, env
    directory = Path(runtime)
    if not directory.is_absolute():
        raise SystemGraphicsError("system Vulkan runtime path is not absolute")
    # run-proton.py picks the same payload by name; Xclipse needs a DXVK within 128 bytes of push data.
    payload = env.get("DROIDDECK_HYBRIS_DXVK") or "dxvk"
    if not payload.startswith("dxvk") or "/" in payload:
        raise SystemGraphicsError("invalid DXVK payload name: %s" % payload)
    required = (
        "run-proton.py", "wsi/libVkLayer_window_system_integration.so",
        "wsi/VkLayer_window_system_integration.json", "lib/libVkLayer_hybris_compat.so",
        *("%s/%s/%s.dll" % (payload, arch, name) for arch in ("x32", "x64")
          for name in ("d3d8", "d3d9", "d3d10core", "d3d11", "dxgi")),
    )
    try:
        for name in required:
            path = directory / name
            if not path.is_file() or not path.stat().st_size:
                raise SystemGraphicsError("missing system Vulkan component: %s" % name)
        wrapper = directory / "run-proton.py"
        if not os.access(wrapper, os.X_OK):
            raise SystemGraphicsError("system Vulkan Proton wrapper is not executable")
    except OSError as error:
        raise SystemGraphicsError("system Vulkan component is unreadable: %s" % error) from error
    result = dict(env)
    layers = "VK_LAYER_window_system_integration:" + env.get("DROIDDECK_HYBRIS_LAYERS", "VK_LAYER_HYBRIS_compat")
    result.update({
        "HYBRIS_BC_TEXTURES": "dxvk",
        "VK_LAYER_PATH": ":".join(filter(None, (str(directory / "wsi"), str(directory / "lib"),
                                                env.get("DROIDDECK_HYBRIS_LAYER_PATH")))),
        "VK_INSTANCE_LAYERS": layers,
        "ENABLE_GAMESCOPE_WSI": "0",
        "VK_LOADER_LAYERS_DISABLE": "*gamescope*",
        "DISABLE_VK_LAYER_VALVE_steam_fossilize_1": "1",
    })
    for name in ("DISABLE_WSI_LAYER", "PROTON_USE_WINED3D", "ENABLE_VK_LAYER_VALVE_steam_fossilize_1"):
        result.pop(name, None)
    return [str(wrapper), *command], result


def game_logs(command, env, app_id, now=None):
    """Detailed graphics logs for one game launch, in the session's log folder: Proton's own log,
    DXVK's and vkd3d-proton's, and the Vulkan driver's startup messages. The app turns this on
    (BL_GAME_LOGS=1) on a GPU that is not an Adreno, or with Setup's "Detailed graphics logs"."""
    if env.get("BL_GAME_LOGS") != "1" or not env.get("BL_DEBUG_DIR") or len(command) < 3 \
            or command[1] != "waitforexitandrun":
        return env
    import time
    stamp = time.strftime("%Y%m%d-%H%M%S", time.localtime(now))
    name = app_id or re.sub(r"[^A-Za-z0-9._-]+", "_", Path(command[2]).stem)[:40] or "game"
    parent = Path(env["BL_DEBUG_DIR"]) / "games"
    try:
        parent.mkdir(parents=True, exist_ok=True)
        folder = Path(tempfile.mkdtemp(prefix=stamp + "-", suffix="-" + name, dir=parent))
    except OSError as error:
        print("droiddeck-game-env: no game log folder (%s)" % error, file=sys.stderr)
        return env
    result = dict(env)
    result.update({
        "PROTON_LOG": "1",
        "PROTON_LOG_DIR": str(folder),
        "DXVK_LOG_PATH": str(folder),
        "DXVK_LOG_LEVEL": env.get("DXVK_LOG_LEVEL", "info"),
        "VKD3D_LOG_FILE": str(folder / "vkd3d.log"),
        "VKD3D_DEBUG": env.get("VKD3D_DEBUG", "warn"),
        "VKD3D_SHADER_DEBUG": env.get("VKD3D_SHADER_DEBUG", "none"),
        "MESA_LOG_FILE": str(folder / "mesa.log"),
        "PANVK_DEBUG": env.get("PANVK_DEBUG", "startup"),
    })
    try:
        (folder / "launch.txt").write_text("\n".join([
            "app: %s" % (app_id or "-"),
            "exe: %s" % command[2],
            "proton: %s" % command[0],
            "started: %s" % time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(now)),
        ] + ["%s=%s" % (key, env[key]) for key in sorted(env)
             if key.startswith(("VK_", "BL_VK", "BL_DRM", "BL_OPEN", "MESA_", "PANVK", "DXVK_", "VKD3D", "ZINK", "TU_", "DROIDDECK_VKD3D"))]) + "\n")
    except OSError:
        pass
    # Whether the game's sound reaches PulseAudio: the streams 20 s and 60 s in, without waiting.
    try:
        subprocess.Popen(["sh", "-c", 'for t in 20 40; do sleep $t; { echo "== $(date +%T)"; '
                          'pactl list sink-inputs 2>&1 | head -80; } >> "$1/audio-streams.txt"; done', "audio", str(folder)],
                         stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                         start_new_session=True)
    except OSError:
        pass
    return result


def exec_proton(command, env):
    # Synchronization selection has already picked the actual Proton executable.
    command, env = system_graphics_launch(command, env)
    os.execvpe(command[0], command, env)


def main():
    command = sys.argv[1:]
    if not command:
        return 2
    env = game_gpu(os.environ.copy())
    mic_enabled = env.get("BL_MIC_ENABLED")
    debug_dir = env.get("BL_DEBUG_DIR", "")
    prefix_path = env.get("STEAM_COMPAT_DATA_PATH", "").rstrip("/")
    app_id = game_id(env)
    components = ""
    waiting = bool(app_id and len(command) > 1 and command[1] == "waitforexitandrun")
    config = {}
    launch_config = None
    launch_config_valid = True
    effective_env = env
    if waiting:
        path = Path.home() / ".config/droiddeck/game-environment.json"
        try:
            with path.open(encoding="utf-8") as source:
                config = json.load(source)
            launch_config = config
        except FileNotFoundError:
            launch_config = {"version": 1}
        except (OSError, ValueError, TypeError, AttributeError):
            launch_config_valid = False
        if launch_config_valid:
            try:
                effective_env = apply_config(env, launch_config, app_id)
            except (OSError, ValueError, TypeError, AttributeError):
                launch_config_valid = False
        if not launch_config_valid:
            print("droiddeck-game-env: invalid configuration; using inherited environment", file=sys.stderr)
    if waiting:
        components = win_components(prefix_path, app_id, command[0], effective_env)
    if prefix_path and command:
        try:
            fixed = repair_foreign_system_dlls(prefix_path, command[0], effective_env)
        except OSError as error:
            fixed = []
            print("droiddeck-game-env: prefix system DLL check skipped (%s)" % error, file=sys.stderr)
        if fixed:
            print("droiddeck-game-env: replaced foreign-architecture system DLLs with this Proton's own: %s" % ", ".join(fixed),
                  file=sys.stderr)
    scripts = mark_install_scripts(command, env)
    if scripts and install_scripts_done(scripts, env):
        print("droiddeck-game-env: install scripts already run in this prefix; evaluator skipped", file=sys.stderr)
        return 0
    try:
        digest = scripts_digest(scripts) if scripts else None
    except OSError:
        digest = None
    diagnostic_app_id = app_id if waiting and os.environ.get("DROIDDECK_ENHANCED_DIAGNOSTICS") == "1" else None
    dirt_prefix = None
    if app_id:
        gl_default = gl_vsync(env)
        if waiting:
            game_images(env)
        fixes = {}
        if waiting:
            command = skip_red_launcher(command)
            ubisoft_overlay_off(env)
            project_zomboid_jvm(command)
            fixes, extra, notes = engine_fixes(command[2], command[3:]) if len(command) > 2 else ({}, [], [])
            if not gl_default:
                fixes.pop("vblank_mode", None)
            for note in notes:
                print("droiddeck-game-env: " + note, file=sys.stderr)
            command += extra
            if components:
                own = fixes.get("WINEDLLOVERRIDES")
                fixes["WINEDLLOVERRIDES"] = components + ";" + own if own else components
            dirt_prefix = dirt_ws2_wanted(command, env)
            if dirt_prefix:
                own = fixes.get("WINEDLLOVERRIDES")
                fixes["WINEDLLOVERRIDES"] = own + ";ws2_32=n,b" if own else "ws2_32=n,b"
        if waiting:
            if launch_config_valid:
                if "vblank_mode" in config.get("shared", {}):
                    fixes.pop("vblank_mode", None)
                env = apply_config(env, launch_config, app_id, fixes)
        else:
            path = Path.home() / ".config/droiddeck/game-environment.json"
            try:
                with path.open(encoding="utf-8") as source:
                    config = json.load(source)
                env = apply_config(env, sync_config(config, app_id), app_id)
            except FileNotFoundError:
                pass
            except (OSError, ValueError, TypeError, AttributeError):
                print("droiddeck-game-env: invalid configuration; using inherited environment", file=sys.stderr)
        if waiting:
            before_debug = env
            env = apply_game_debug_logs(env, config, debug_dir)
            if env is not before_debug:
                print("droiddeck-game-env: game debug logs enabled for this launch", file=sys.stderr)
        if gl_default and env.get("vblank_mode") == "0":
            print("droiddeck-game-env: OpenGL does not wait for the screen refresh (vblank_mode=0)", file=sys.stderr)
    if app_id and len(command) > 1 and command[1] == "waitforexitandrun":
        command = offline_without_anticheat(command, env)
    env = game_logs(command, env, app_id)
    if mic_enabled == "0":
        env["AUDIO_DIRECT_MIC"] = "0"
    env = directaudio_settings(env)
    env = vram_cap(env)
    note = render_path_defaults(command, env) if app_id else None
    if note:
        print("droiddeck-game-env: " + note, file=sys.stderr)
    note = bridge_feature_level(env)
    if note:
        print("droiddeck-game-env: " + note, file=sys.stderr)
    env = force_ssbs(command, env)
    game_cores(env)
    cpu_topology(env, command)
    if app_id and len(command) > 2 and command[1] == "waitforexitandrun":
        try:
            note = ego_core_plan(command, env)
        except OSError as error:
            note = "no thread plan added for this core count (%s)" % error
        if note:
            print("droiddeck-game-env: " + note, file=sys.stderr)
    try:
        xbox = runpy.run_path(str(Path(__file__).with_name("droiddeck-xbox")))
    except Exception as error:
        print("droiddeck-game-env: Xbox support unavailable (%s); stock launch" % error, file=sys.stderr)
        xbox = {"launch": lambda command, env: None}
    try:
        xbox_result = xbox["launch"](command, env)
    except (OSError, ValueError, KeyError, RuntimeError) as error:
        print("droiddeck-game-env: Xbox launch could not start (%s)" % error, file=sys.stderr)
        return 1
    if xbox_result is not None:
        if diagnostic_app_id is not None and isinstance(xbox_result, int):
            diagnostic_game_exit(diagnostic_app_id, xbox_result, "xbox", xbox_result % 256)
        return xbox_result
    after = None
    prefix = env.get("STEAM_COMPAT_DATA_PATH", "").rstrip("/")
    if app_id and prefix and len(command) > 1 and command[1] == "waitforexitandrun":
        try:
            if dirt_prefix:
                after = lambda: restore_dirt_ws2(dirt_prefix, command[0])
                swap_dirt_ws2(dirt_prefix)
                print("droiddeck-game-env: DiRT telemetry fix: ws2_32 stand-in in the prefix until the game exits",
                      file=sys.stderr)
            elif restore_dirt_ws2(prefix, command[0]):
                print("droiddeck-game-env: DiRT telemetry fix: put the prefix's own ws2_32 back", file=sys.stderr)
        except OSError as error:
            print("droiddeck-game-env: DiRT telemetry fix not applied (%s)" % error, file=sys.stderr)
            if after:
                after = None
                try:
                    restore_dirt_ws2(dirt_prefix, command[0])
                except OSError as undo:
                    print("droiddeck-game-env: DiRT telemetry fix: prefix ws2_32 not put back (%s); Wine uses its own copy "
                          "and the next launch puts the file back" % undo, file=sys.stderr)
    sync = load_sync()
    launch, launch_env = select_sync(command, env, sync)
    if launch[0] != command[0]:
        try:
            return start(launch, launch_env, digest, after, diagnostic_app_id)
        except OSError as error:
            print("droiddeck-game-env: %s could not start (%s); stock launch" % (launch[0], error), file=sys.stderr)
            launch, launch_env = command, without_pack(launch_env, env)
            record_stock(sync, launch, launch_env)
    try:
        return start(launch, launch_env, digest, after, diagnostic_app_id)
    except OSError:
        if after:
            try:
                after()
            except OSError as error:
                print("droiddeck-game-env: DiRT telemetry fix: prefix ws2_32 not put back (%s); the next launch does it" % error,
                      file=sys.stderr)
        raise


if __name__ == "__main__":
    try:
        sys.exit(main())
    except SystemGraphicsError as error:
        print("droiddeck-game-env: PowerVR graphics runtime unavailable (%s)" % error, file=sys.stderr)
        sys.exit(1)
