"""Wine runner"""
# pylint: disable=too-many-lines
import os
import shlex
from gettext import gettext as _
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
from lutris import runtime, settings
from lutris.api import format_runner_version, normalize_version_architecture
from lutris.config import LutrisConfig
from lutris.database.games import get_game_by_field
from lutris.exceptions import (
EsyncLimitError,
FsyncUnsupportedError,
MisconfigurationError,
MissingExecutableError,
MissingGameExecutableError,
SymlinkNotUsableError,
UnspecifiedVersionError,
)
from lutris.game import Game
from lutris.gui.dialogs import FileDialog
from lutris.runners.commands.wine import ( # noqa: F401 pylint: disable=unused-import
create_prefix,
delete_registry_key,
eject_disc,
install_cab_component,
open_wine_terminal,
set_regedit,
set_regedit_file,
winecfg,
wineexec,
winekill,
winetricks,
)
from lutris.runners.runner import Runner
from lutris.util import system
from lutris.util.display import DISPLAY_MANAGER, get_default_dpi, is_display_x11
from lutris.util.graphics import drivers, vkquery
from lutris.util.linux import LINUX_SYSTEM
from lutris.util.log import logger
from lutris.util.process import Process
from lutris.util.strings import split_arguments
from lutris.util.wine import proton
from lutris.util.wine.d3d_extras import D3DExtrasManager
from lutris.util.wine.dgvoodoo2 import dgvoodoo2Manager
from lutris.util.wine.dxvk import REQUIRED_VULKAN_API_VERSION, DXVKManager
from lutris.util.wine.dxvk_nvapi import DXVKNVAPIManager
from lutris.util.wine.extract_icon import PEFILE_AVAILABLE, IconExtractor
from lutris.util.wine.prefix import DEFAULT_DLL_OVERRIDES, WinePrefixManager, find_prefix
from lutris.util.wine.vkd3d import VKD3DManager
from lutris.util.wine.wine import (
GE_PROTON_LATEST,
WINE_DEFAULT_ARCH,
WINE_PATHS,
detect_arch,
get_default_wine_version,
get_installed_wine_versions,
get_overrides_env,
get_real_executable,
get_runner_files_dir_for_version,
get_system_wine_version,
get_wine_path_for_version,
is_esync_limit_set,
is_fsync_supported,
is_gstreamer_build,
is_winewayland_available,
)
def _is_proton_config(config: LutrisConfig) -> bool:
version = config.runner_config.get("version")
return version == GE_PROTON_LATEST or proton.is_proton_version(version)
def _is_pre_proton(_option_key: str, config: LutrisConfig) -> bool:
return not _is_proton_config(config)
def _is_proton_hdr_available(_option_key: str, config: LutrisConfig) -> bool:
if _is_proton_config(config):
version = config.runner_config.get("version")
return bool(version and is_winewayland_available(version) and config.runner_config.get("Graphics") == "wayland")
return False
def _get_version_warning(_option_key: str, config: LutrisConfig) -> Optional[str]:
arch = config.game_config.get("arch")
if arch == "win32" and _is_proton_config(config):
return _("Proton is not compatible with 32-bit prefixes.")
return None
def _get_prefix_warning(_option_key: str, config: LutrisConfig) -> Optional[str]:
game_config = config.game_config
if game_config.get("prefix"):
return None
exe = game_config.get("exe")
if exe and find_prefix(exe):
return None
return _("Warning Some Wine configuration options cannot be applied, if no prefix can be found.")
def _get_exe_warning(_option_key: str, config: LutrisConfig) -> Optional[str]:
exe = config.game_config.get("exe") or ""
stripped_exe = exe.strip()
if not stripped_exe:
return _("Warning No executable path specified")
if exe != stripped_exe:
return _("Warning Executable path has extra whitespace at the beginning or end")
if not os.path.isabs(exe):
# The working dir is a bit dicey- Lutris doe snot use the prefix as a working dir,
# but it seems like Wine does, so we'll be conservative here. Only works with an explicit
# prefix- if we derive it from "exe" we obviosly can't use that.
working_dir = config.game_config.get("working_dir") or config.game_config.get("prefix") or ""
exe = os.path.join(os.path.expanduser(working_dir), os.path.expanduser(exe))
exe = system.fix_path_case(exe)
if not os.path.isfile(exe):
return _("Warning Executable file does not exist")
return None
def _get_dxvk_warning() -> Optional[str]:
if drivers.is_outdated():
driver_info = drivers.get_nvidia_driver_info()
return _(
"Warning Your NVIDIA driver is outdated.\n"
"You are currently running driver %s which does not "
"fully support all features for Vulkan and DXVK games."
) % (driver_info["version"],)
return None
def _get_simple_vulkan_support_error(option_key: str, config: LutrisConfig, feature: str) -> Optional[str]:
if os.environ.get("LUTRIS_NO_VKQUERY"):
return None
if config.runner_config.get(option_key) and not LINUX_SYSTEM.is_vulkan_supported():
return (
_("Error Vulkan is not installed or is not supported by your system, %s is not available.") % feature
)
return None
def _get_dxvk_version_warning(_option_key: str, config: LutrisConfig) -> Optional[str]:
if os.environ.get("LUTRIS_NO_VKQUERY"):
return None
runner_config = config.runner_config
if runner_config.get("dxvk") and LINUX_SYSTEM.is_vulkan_supported():
version = runner_config.get("dxvk_version")
if version and not version.startswith("v1."):
library_api_version = vkquery.get_vulkan_api_version()
if library_api_version and library_api_version < REQUIRED_VULKAN_API_VERSION:
return _(
"Warning Lutris has detected that Vulkan API version %s is installed, "
"but to use the latest DXVK version, %s is required."
) % (vkquery.format_version(library_api_version), vkquery.format_version(REQUIRED_VULKAN_API_VERSION))
devices = vkquery.get_device_info()
if devices and devices[0].api_version < REQUIRED_VULKAN_API_VERSION:
return _(
"Warning Lutris has detected that the best device available ('%s') supports Vulkan API %s, "
"but to use the latest DXVK version, %s is required."
) % (
devices[0].name,
vkquery.format_version(devices[0].api_version),
vkquery.format_version(REQUIRED_VULKAN_API_VERSION),
)
return None
def _get_esync_warning(_option_key: str, config: LutrisConfig) -> Optional[str]:
if config.runner_config.get("esync"):
limits_set = is_esync_limit_set()
if not limits_set:
return _(
"Warning Your limits are not set correctly. Please increase them as described here:\n"
""
"How-to-Esync (https://github.com/lutris/docs/blob/master/HowToEsync.md)"
)
return ""
def _get_fsync_warning(_option_key: str, config: LutrisConfig) -> Optional[str]:
if config.runner_config.get("fsync"):
fsync_supported = is_fsync_supported()
if not fsync_supported:
return _("Warning Your kernel is not patched for fsync.")
return None
def _get_virtual_desktop_warning(_option_key: str, config: LutrisConfig) -> Optional[str]:
message = _("Wine virtual desktop is no longer supported")
runner_config = config.runner_config
if runner_config.get("Desktop"):
version = str(runner_config.get("version")).casefold()
if "-ge-" in version or "proton" in version:
message += "\n"
message += _("Virtual desktops cannot be enabled in Proton or GE Wine versions.")
return message
def _get_wine_wayland_warning(_option_key: str, config: LutrisConfig) -> Optional[str]:
runner_config = config.runner_config
if runner_config.get("Graphics") == "wayland":
runner_version = runner_config.get("version")
if not runner_version:
return None
if is_display_x11():
return _("You cannot use winewayland driver when using an X11-based session")
if not is_winewayland_available(runner_version):
if _is_proton_config(config):
return _("Your Proton version does not support winewayland graphics driver")
else:
return _("Your Wine version does not support winewayland graphics driver")
return None
def _get_wine_version_choices():
version_choices = [(_("Custom (select executable below)"), "custom")]
system_wine_labels = {
"winehq-devel": _("WineHQ Devel ({})"),
"winehq-staging": _("WineHQ Staging ({})"),
"wine-development": _("Wine Development ({})"),
"system": _("System ({})"),
}
versions = get_installed_wine_versions()
for version in versions:
if version == GE_PROTON_LATEST:
label = _("GE-Proton (Latest)")
elif version in system_wine_labels:
version_number = get_system_wine_version(WINE_PATHS[version])
label = system_wine_labels[version].format(version_number)
else:
label = version
version_choices.append((label, version))
return version_choices
class wine(Runner):
description: str = _("Runs Windows games")
human_name = _("Wine")
platforms = [_("Windows")]
multiple_versions = True
entry_point_option = "exe"
game_options = [
{
"option": "exe",
"type": "file",
"label": _("Executable"),
"help": _("The game's main EXE file"),
"warning": _get_exe_warning,
},
{
"option": "args",
"type": "string",
"label": _("Arguments"),
"help": _("Windows command line arguments used when launching the game"),
"validator": shlex.split,
},
{
"option": "working_dir",
"type": "directory",
"label": _("Working directory"),
"help": _(
"The location where the game is run from.\nBy default, Lutris uses the directory of the executable."
),
},
{
"option": "prefix",
"type": "directory",
"label": _("Wine prefix"),
"warning": _get_prefix_warning,
"help": _(
"The prefix used by Wine.\n"
"It's a directory containing a set of files and "
"folders making up a confined Windows environment."
),
},
{
"option": "arch",
"type": "choice",
"label": _("Prefix architecture"),
"visible": _is_pre_proton,
"choices": [(_("Auto"), "auto"), (_("32-bit"), "win32"), (_("64-bit"), "win64")],
"default": "auto",
"help": _("The architecture of the Windows environment"),
},
{
"option": "desktop_integration",
"type": "bool",
"label": _("Integrate system files in the prefix"),
"default": False,
"advanced": True,
"help": _(
"Place 'Documents', 'Pictures', and similar files in your home folder, instead of "
"keeping them in the game's prefix. This includes some saved games."
),
},
]
runner_options = [
{
"option": "version",
"label": _("Wine version"),
"type": "choice",
"choices": _get_wine_version_choices,
"default": get_default_wine_version,
"warning": _get_version_warning,
"help": _(
"The version of Wine used to launch the game.\n"
"Using the last version is generally recommended, "
"but some games work better on older versions."
),
},
{
"option": "custom_wine_path",
"label": _("Custom Wine executable"),
"type": "file",
"advanced": True,
"help": _('The Wine executable to be used if you have selected "Custom" as the Wine version.'),
},
{
"option": "system_winetricks",
"label": _("Use system winetricks"),
"type": "bool",
"default": False,
"advanced": True,
"help": _("Switch on to use /usr/bin/winetricks for winetricks."),
},
{
"option": "dxvk",
"section": _("Graphics"),
"label": _("Enable DXVK"),
"type": "bool",
"default": True,
"warning": _get_dxvk_warning,
"error": lambda k, c: _get_simple_vulkan_support_error(k, c, _("DXVK")),
"active": True,
"help": _(
"Use DXVK to "
"increase compatibility and performance in Direct3D 11, 10 "
"and 9 applications by translating their calls to Vulkan."
),
},
{
"option": "dxvk_version",
"section": _("Graphics"),
"label": _("DXVK version"),
"advanced": True,
"type": "choice_with_entry",
"visible": _is_pre_proton,
"condition": LINUX_SYSTEM.is_vulkan_supported(),
"conditional_on": "dxvk",
"choices": lambda: DXVKManager().version_choices,
"default": lambda: DXVKManager().version,
"warning": _get_dxvk_version_warning,
},
{
"option": "vkd3d",
"section": _("Graphics"),
"label": _("Enable VKD3D"),
"type": "bool",
"visible": _is_pre_proton,
"error": lambda k, c: _get_simple_vulkan_support_error(k, c, _("VKD3D")),
"default": True,
"active": True,
"help": _("Use VKD3D to enable support for Direct3D 12 applications by translating their calls to Vulkan."),
},
{
"option": "vkd3d_version",
"section": _("Graphics"),
"label": _("VKD3D version"),
"advanced": True,
"type": "choice_with_entry",
"visible": _is_pre_proton,
"condition": LINUX_SYSTEM.is_vulkan_supported(),
"conditional_on": "vkd3d",
"choices": lambda: VKD3DManager().version_choices,
"default": lambda: VKD3DManager().version,
},
{
"option": "d3d_extras",
"section": _("Graphics"),
"label": _("Enable D3D Extras"),
"type": "bool",
"default": True,
"advanced": True,
"help": _(
"Replace Wine's D3DX and D3DCOMPILER libraries with alternative ones. "
"Needed for proper functionality of DXVK with some games."
),
},
{
"option": "d3d_extras_version",
"section": _("Graphics"),
"label": _("D3D Extras version"),
"advanced": True,
"conditional_on": "d3d_extras",
"type": "choice_with_entry",
"choices": lambda: D3DExtrasManager().version_choices,
"default": lambda: D3DExtrasManager().version,
},
{
"option": "dxvk_nvapi",
"section": _("Graphics"),
"label": _("Enable DXVK-NVAPI / DLSS"),
"type": "bool",
"error": lambda k, c: _get_simple_vulkan_support_error(k, c, _("DXVK-NVAPI / DLSS")),
"default": True,
"advanced": True,
"visible": _is_pre_proton,
"help": _("Enable emulation of Nvidia's NVAPI and add DLSS support, if available."),
},
{
"option": "dxvk_nvapi_version",
"section": _("Graphics"),
"label": _("DXVK NVAPI version"),
"advanced": True,
"conditional_on": "dxvk_nvapi",
"visible": _is_pre_proton,
"type": "choice_with_entry",
"choices": lambda: DXVKNVAPIManager().version_choices,
"default": lambda: DXVKNVAPIManager().version,
},
{
"option": "proton_hdr",
"section": _("Graphics"),
"label": _("Enable HDR (Experimental)"),
"type": "bool",
"default": False,
"advanced": True,
"visible": _is_proton_hdr_available,
"help": _(
"Enable Proton's support for High Dynamic Range graphics. "
"Requires Wayland selected as Graphics backend."
),
},
{
"option": "esync",
"label": _("Enable Esync"),
"type": "bool",
"warning": _get_esync_warning,
"active": True,
"default": True,
"help": _(
"Enable eventfd-based synchronization (esync). "
"This will increase performance in applications "
"that take advantage of multi-core processors."
),
},
{
"option": "fsync",
"label": _("Enable Fsync"),
"type": "bool",
"default": is_fsync_supported(),
"warning": _get_fsync_warning,
"active": True,
"help": _(
"Enable futex-based synchronization (fsync). "
"This will increase performance in applications "
"that take advantage of multi-core processors. "
"Requires kernel 5.16 or above."
),
},
{
"option": "fsr",
"label": _("Enable AMD FidelityFX Super Resolution (FSR)"),
"type": "bool",
"default": True,
"help": _(
"Use FSR to upscale the game window to native resolution.\n"
"Requires Lutris Wine FShack >= 6.13 and setting the game to a lower resolution.\n"
"Does not work with games running in borderless window mode or that perform their own upscaling."
),
},
{
"option": "battleye",
"label": _("Enable BattlEye Anti-Cheat"),
"type": "bool",
"default": True,
"help": _(
"Enable support for BattlEye Anti-Cheat in supported games\n"
"Requires Lutris Wine 6.21-2 and newer or any other compatible Wine build.\n"
),
},
{
"option": "eac",
"label": _("Enable Easy Anti-Cheat"),
"type": "bool",
"default": True,
"help": _(
"Enable support for Easy Anti-Cheat in supported games\n"
"Requires Lutris Wine 7.2 and newer or any other compatible Wine build.\n"
),
},
{
"option": "Desktop",
"section": _("Virtual Desktop"),
"label": _("Windowed (virtual desktop)"),
"type": "bool",
"advanced": True,
"visible": _is_pre_proton,
"warning": _get_virtual_desktop_warning,
"default": False,
"help": _(
"Run the whole Windows desktop in a window.\n"
"Otherwise, run it fullscreen.\n"
"This corresponds to Wine's Virtual Desktop option."
),
},
{
"option": "WineDesktop",
"section": _("Virtual Desktop"),
"label": _("Virtual desktop resolution"),
"type": "choice_with_entry",
"visible": _is_pre_proton,
"conditional_on": "Desktop",
"advanced": True,
"choices": DISPLAY_MANAGER.get_resolutions,
"help": _("The size of the virtual desktop in pixels."),
},
{
"option": "Dpi",
"section": _("DPI"),
"label": _("Enable DPI Scaling"),
"type": "bool",
"advanced": True,
"default": False,
"help": _(
"Enables the Windows application's DPI scaling.\n"
"Otherwise, the Screen Resolution option in 'Wine configuration' controls this."
),
},
{
"option": "ExplicitDpi",
"section": _("DPI"),
"label": _("DPI"),
"type": "string",
"conditional_on": "Dpi",
"advanced": True,
"default": str(get_default_dpi()),
"help": _("The DPI to be used if 'Enable DPI Scaling' is turned on."),
},
{
"option": "MouseWarpOverride",
"label": _("Mouse Warp Override"),
"type": "choice",
"choices": [
(_("Enable"), "enable"),
(_("Disable"), "disable"),
(_("Force"), "force"),
],
"default": "enable",
"advanced": True,
"help": _(
"Override the default mouse pointer warping behavior\n"
"Enable: (Wine default) warp the pointer when the "
"mouse is exclusively acquired \n"
"Disable: never warp the mouse pointer \n"
"Force: always warp the pointer"
),
},
{
"option": "Audio",
"label": _("Audio driver"),
"type": "choice",
"advanced": True,
"choices": [
(_("Auto"), "auto"),
("ALSA", "alsa"),
("PulseAudio", "pulse"),
("OSS", "oss"),
],
"default": "auto",
"help": _(
"Which audio backend to use.\nBy default, Wine automatically picks the right one for your system."
),
},
{
"option": "Graphics",
"label": _("Graphics driver"),
"type": "choice",
"advanced": True,
"choices": [
(_("Auto"), "auto"),
("Wayland", "wayland"),
("X11", "x11"),
],
"default": "auto",
"warning": _get_wine_wayland_warning,
"help": _(
"Which graphics backend to use.\nBy default, Wine automatically picks the right one for your system."
),
},
{
"option": "overrides",
"type": "mapping",
"label": _("DLL overrides"),
"help": _("Sets WINEDLLOVERRIDES when launching the game."),
},
{
"option": "show_debug",
"label": _("Output debugging info"),
"type": "choice",
"choices": [
(_("Disabled"), "-all"),
(_("Enabled"), ""),
(_("Inherit from environment"), "inherit"),
(_("Show FPS"), "+fps"),
(_("Full (CAUTION: Will cause MASSIVE slowdown)"), "+all"),
],
"default": "-all",
"help": _("Output debugging information in the game log (might affect performance)"),
},
{
"option": "ShowCrashDialog",
"label": _("Show crash dialogs"),
"type": "bool",
"default": False,
"advanced": True,
},
{
"option": "autoconf_joypad",
"type": "bool",
"label": _("Autoconfigure joypads"),
"advanced": True,
"default": False,
"help": _("Automatically disables one of Wine's detected joypad to avoid having 2 controllers detected"),
},
]
reg_prefix = "HKEY_CURRENT_USER/Software/Wine"
reg_keys = {
"Audio": r"%s/Drivers" % reg_prefix,
"Graphics": r"%s/Drivers" % reg_prefix,
"MouseWarpOverride": r"%s/DirectInput" % reg_prefix,
"Desktop": "MANAGED",
"WineDesktop": "MANAGED",
"ShowCrashDialog": "MANAGED",
}
core_processes = (
"services.exe",
"winedevice.exe",
"plugplay.exe",
"explorer.exe",
"rpcss.exe",
"rundll32.exe",
"wineboot.exe",
)
def __init__(self, config=None, prefix=None, working_dir=None, wine_arch=None): # noqa: C901
super().__init__(config)
self._prefix = prefix
self._working_dir = working_dir
self._wine_arch = wine_arch
self.dll_overrides = DEFAULT_DLL_OVERRIDES.copy() # we'll modify this, so we better copy it
@property
def context_menu_entries(self):
"""Return the contexual menu entries for wine"""
return [
("wineexec", _("Run EXE inside Wine prefix"), self.run_wineexec),
("wineshell", _("Open Bash terminal"), self.run_wine_terminal),
("wineconsole", _("Open Wine console"), self.run_wineconsole),
(None, "-", None),
("winecfg", _("Wine configuration"), self.run_winecfg),
("wine-regedit", _("Wine registry"), self.run_regedit),
("winecpl", _("Wine Control Panel"), self.run_winecpl),
("winetaskmgr", _("Wine Task Manager"), self.run_taskmgr),
(None, "-", None),
("winetricks", _("Winetricks"), self.run_winetricks),
]
@property
def prefix_path(self):
"""Return the absolute path of the Wine prefix. Falls back to default WINE prefix."""
_prefix_path = self._prefix or self.game_config.get("prefix") or os.environ.get("WINEPREFIX")
if not _prefix_path and self.game_config.get("exe"):
# Find prefix from game if we have one
_prefix_path = find_prefix(self.game_exe)
if _prefix_path:
_prefix_path = os.path.expanduser(_prefix_path) # just in case!
return _prefix_path
@property
def game_exe(self):
"""Return the game's executable's path, which may not exist. None
if there is no exe path defined."""
exe = self.game_config.get("exe")
if not exe:
logger.error("The game doesn't have an executable")
return None
exe = os.path.expanduser(exe) # just in case!
if os.path.isabs(exe):
return system.fix_path_case(exe)
if not self.game_path:
logger.warning("The game has an executable, but not a game path")
return None
return system.fix_path_case(os.path.join(self.game_path, exe))
@property
def working_dir(self):
"""Return the working directory to use when running the game."""
_working_dir = self._working_dir or self.game_config.get("working_dir")
if _working_dir:
return os.path.expanduser(_working_dir)
if self.game_exe:
game_dir = os.path.dirname(self.game_exe)
if os.path.isdir(game_dir):
return game_dir
return super().working_dir
@property
def nvidia_shader_cache_path(self):
"""WINE should give each game its own shader cache if possible."""
return self.game_path or self.shader_cache_dir
@property
def wine_arch(self):
"""Return the wine architecture.
Get it from the config or detect it from the prefix"""
arch = self._wine_arch or self.game_config.get("arch") or "auto"
if arch not in ("win32", "win64"):
prefix_path = self.prefix_path
if prefix_path:
arch = detect_arch(prefix_path, self.get_executable())
else:
arch = WINE_DEFAULT_ARCH
return arch
def get_runner_version(self, version: Optional[str] = None) -> Optional[Dict[str, str]]:
if version in WINE_PATHS:
return {"version": version}
return super().get_runner_version(version)
def read_version_from_config(self, default: Optional[str] = None) -> str:
"""Return the Wine version to use. use_default can be set to false to
force the installation of a specific wine version. If no version is configured,
we return the default supplied, or the4 global Wine default if none is."""
# We must use the config levels to avoid getting a default if the setting
# is not set; we'll fall back to get_default_version()
for level in [self.config.game_level, self.config.runner_level]:
if "wine" in level:
runner_version = level["wine"].get("version")
# Treat 'ge-proton' as if no version is set
if runner_version and runner_version != GE_PROTON_LATEST:
return runner_version
if default:
return default
return get_default_wine_version()
def get_path_for_version(self, version: str) -> str:
"""Return the absolute path of a wine executable for a given version"""
return get_wine_path_for_version(version, config=self.runner_config)
def resolve_config_path(self, path, relative_to=None):
# Resolve paths with tolerance for Windows-isms;
# first try to fix mismatched casing, and then if that
# finds no file or directory, try again after swapping in
# slashes for backslashes.
resolved = super().resolve_config_path(path, relative_to)
resolved = system.fix_path_case(resolved)
if not os.path.exists(resolved) and "\\" in path:
fixed = path.replace("\\", "/")
fixed_resolved = super().resolve_config_path(fixed, relative_to)
fixed_resolved = system.fix_path_case(fixed_resolved)
return fixed_resolved
return resolved
def get_launch_config_exe(self, launch_config):
"""Resolve launch config exe relative to game_path, like game_exe does."""
exe = launch_config.get("exe")
if not exe:
return None
exe = os.path.expanduser(exe)
if os.path.isabs(exe):
return system.fix_path_case(exe)
if self.game_path:
return system.fix_path_case(os.path.join(self.game_path, exe))
return exe
def get_executable(self, version: str = "", fallback: bool = True) -> str:
"""Return the path to the Wine executable.
A specific version can be specified if needed.
"""
if not version:
version = self.read_version_from_config()
if version == GE_PROTON_LATEST:
return proton.get_umu_path()
if proton.is_proton_version(version):
return proton.get_proton_wine_path(version)
try:
wine_path = self.get_path_for_version(version)
if system.path_exists(wine_path):
return wine_path
except MissingExecutableError:
if not fallback:
raise
if not fallback:
raise MissingExecutableError(_("The Wine executable at '%s' is missing.") % wine_path)
# Fallback to default version
default_version = get_default_wine_version()
wine_path = self.get_path_for_version(default_version)
if not system.path_exists(wine_path):
raise MissingExecutableError(_("The Wine executable at '%s' is missing.") % wine_path)
# Update the version in the config
if version == self.runner_config.get("version"):
self.runner_config["version"] = default_version
# TODO: runner_config is a dict so we have to instanciate a
# LutrisConfig object to save it.
# XXX: The version key could be either in the game specific
# config or the runner specific config. We need to know
# which one to get the correct LutrisConfig object.
return wine_path
def get_command(self) -> List[str]:
command = super().get_command()
if command:
if proton.is_proton_path(command[0]) and not proton.is_umu_path(command[0]):
command[0] = proton.get_umu_path()
if proton.is_umu_path(command[0]) and self.wine_arch == "win32":
raise RuntimeError(_("Proton is not compatible with 32-bit prefixes."))
return command
def is_installed(self, flatpak_allowed: bool = True, version: str = None, fallback: bool = True) -> bool:
"""Check if Wine is installed.
If no version is passed, checks if any version of wine is available
"""
try:
if version:
# We don't care where Wine is, but only if it was found at all.
self.get_executable(version, fallback)
return True
return bool(get_installed_wine_versions())
except MisconfigurationError:
return False
def is_installed_for(self, interpreter):
try:
version = self.get_installer_runner_version(interpreter.installer, use_api=True)
return self.is_installed(version=version, fallback=False)
except MisconfigurationError:
return False
def get_installer_runner_version(
self, installer, use_runner_config: bool = True, use_api: bool = False
) -> Optional[str]:
# If a version is specified in the script choose this one
version = None
if installer.script.get(installer.runner):
version = installer.script[installer.runner].get("version")
version = normalize_version_architecture(version)
# If the installer is an extension, use the wine version from the base game
elif installer.requires:
db_game = get_game_by_field(installer.requires, field="installer_slug")
if not db_game:
db_game = get_game_by_field(installer.requires, field="slug")
if not db_game:
raise MisconfigurationError(_("The required game '%s' could not be found.") % installer.requires)
game = Game(db_game["id"])
if game.config:
version = game.config.runner_config["version"]
else:
version = None
if not version and use_runner_config:
# Try to read the version from the saved runner config for Wine.
try:
return wine.get_runner_version_and_config()[0]
except UnspecifiedVersionError:
pass # fall back to the API in this case
if not version and use_api:
# Try to obtain the default wine version from the Lutris API.
default_version_info = self.get_runner_version()
if default_version_info and "version" in default_version_info:
logger.debug("Default wine version is %s", default_version_info["version"])
version = format_runner_version(default_version_info)
return version
def adjust_installer_runner_config(self, installer_runner_config: Dict[str, Any]) -> None:
version = installer_runner_config.get("version")
if version:
installer_runner_config["version"] = normalize_version_architecture(version)
@classmethod
def get_runner_version_and_config(cls) -> Tuple[str, LutrisConfig]:
runner_config = LutrisConfig(runner_slug="wine")
if "wine" in runner_config.runner_level:
config_version = runner_config.runner_level["wine"].get("version")
if config_version:
return config_version, runner_config
raise UnspecifiedVersionError(_("The runner configuration does not specify a Wine version."))
@classmethod
def msi_exec(
cls,
msi_file,
quiet=False,
prefix=None,
wine_path=None,
working_dir=None,
blocking=False,
):
msi_args = "/i %s" % msi_file
if quiet:
msi_args += " /q"
return wineexec(
"msiexec",
args=msi_args,
prefix=prefix,
wine_path=wine_path,
working_dir=working_dir,
blocking=blocking,
)
def _run_executable(self, executable):
"""Runs a Windows executable using this game's configuration"""
wineexec(
executable,
wine_path=self.get_executable(),
prefix=self.prefix_path,
working_dir=self.prefix_path,
config=self,
env=self.get_env(os_env=True),
runner=self,
)
def run_wineexec(self, *args):
"""Ask the user for an arbitrary exe file to run in the game's prefix"""
dlg = FileDialog(_("Select an EXE or MSI file"), default_path=self.game_path)
filename = dlg.filename
if not filename:
return
self.prelaunch()
self._run_executable(filename)
def run_wineconsole(self, *args):
"""Runs wineconsole inside wine prefix."""
self.prelaunch()
self._run_executable("wineconsole")
def run_winecfg(self, *args):
"""Run winecfg in the current context"""
self.prelaunch()
winecfg(
wine_path=self.get_executable(),
prefix=self.prefix_path,
arch=self.wine_arch,
config=self,
env=self.get_env(os_env=True),
runner=self,
)
def run_regedit(self, *args) -> None:
"""Run regedit in the current context"""
self.prelaunch()
self._run_executable("regedit")
def run_wine_terminal(self, *args) -> None:
terminal = self.system_config.get("terminal_app")
system_winetricks: bool = self.runner_config.get("system_winetricks", False)
open_wine_terminal(
terminal=terminal,
wine_path=self.get_executable(),
prefix=self.prefix_path,
env=self.get_env(os_env=True),
system_winetricks=system_winetricks,
)
def run_winetricks(self, *args):
"""Run winetricks in the current context"""
self.prelaunch()
disable_runtime = not self.use_runtime()
system_winetricks = self.runner_config.get("system_winetricks")
if system_winetricks:
# Don't run the system winetricks with the runtime; let the
# system be the system
disable_runtime = True
winetricks(
"",
prefix=self.prefix_path,
wine_path=self.get_executable(),
config=self,
disable_runtime=disable_runtime,
system_winetricks=system_winetricks,
env=self.get_env(os_env=True, disable_runtime=disable_runtime),
runner=self,
)
def run_winecpl(self, *args):
"""Execute Wine control panel."""
self.prelaunch()
self._run_executable("control")
def run_taskmgr(self, *args):
"""Execute Wine task manager"""
self.prelaunch()
self._run_executable("taskmgr")
def run_winekill(self, *args):
"""Runs wineserver -k."""
winekill(
self.prefix_path,
arch=self.wine_arch,
wine_path=self.get_executable(),
env=self.get_env(),
initial_pids=self.get_wine_executable_pids(),
)
return True
def set_regedit_keys(self):
"""Reset regedit keys according to config."""
prefix_manager = WinePrefixManager(self.prefix_path)
# Those options are directly changed with the prefix manager and skip
# any calls to regedit.
managed_keys = {
"ShowCrashDialog": prefix_manager.set_crash_dialogs,
"Desktop": prefix_manager.set_virtual_desktop,
"WineDesktop": prefix_manager.set_desktop_size,
}
for key, path in self.reg_keys.items():
value = self.runner_config.get(key) or "auto"
if not value or (value == "auto" and key not in managed_keys):
prefix_manager.clear_registry_subkeys(path, key)
elif key in self.runner_config:
if value and key == "Graphics" and value == "wayland":
if not is_winewayland_available(self.read_version_from_config()):
logger.warning("Your Wine version does not support winewayland graphics driver")
continue
if proton.is_proton_path(self.get_executable()):
continue
if key in managed_keys:
# Do not pass fallback 'auto' value to managed keys
if value == "auto":
value = None
if (
value
and key in ("Desktop", "WineDesktop")
and (
"wine-ge" in self.get_executable().casefold()
or proton.is_proton_path(self.get_executable())
)
):
logger.warning("Wine Virtual Desktop can't be used with Wine-GE and Proton")
value = None
managed_keys[key](value)
continue
# Convert numeric strings to integers so they are saved as dword
if value.isdigit():
value = int(value)
prefix_manager.set_registry_key(path, key, value)
# We always configure the DPI, because if the user turns off DPI scaling, but it
# had been on the only way to implement that is to save 96 DPI into the registry.
prefix_manager.set_dpi(self.get_dpi())
def get_dpi(self) -> int:
"""Return the DPI to be used by Wine; returns None to allow Wine's own
setting to govern."""
if bool(self.runner_config.get("Dpi")):
try:
return int(self.runner_config.get("ExplicitDpi", get_default_dpi()))
except:
return get_default_dpi()
return get_default_dpi()
def prelaunch(self):
prefix_path = self.prefix_path
if prefix_path:
if os.path.islink(prefix_path) and not os.path.exists(prefix_path):
raise SymlinkNotUsableError(
message=f"Link {prefix_path} couldn't be used, please check permissions or broken link.",
link=prefix_path,
)
if not system.path_exists(os.path.join(prefix_path, "user.reg")):
logger.warning("No valid prefix detected in %s, creating one...", prefix_path)
create_prefix(prefix_path, wine_path=self.get_executable(), arch=self.wine_arch, runner=self)
prefix_manager = WinePrefixManager(prefix_path)
prefix_manager.cleanup_broken_symlinks()
if self.runner_config.get("autoconf_joypad", False):
prefix_manager.configure_joypads()
prefix_manager.create_user_symlinks()
self.configure_desktop_integration(prefix_manager)
self.set_regedit_keys()
for manager, enabled in self.get_dll_managers().items():
manager.setup(enabled)
def get_dll_managers(self, enabled_only=False):
"""Returns the DLL managers in a dict; the keys are the managers themselves,
and the values are the enabled flags for them. If 'enabled_only' is true,
only enabled managers are returned, so disabled managers are not created."""
manager_classes = [
(DXVKManager, "dxvk", "dxvk_version"),
(VKD3DManager, "vkd3d", "vkd3d_version"),
(DXVKNVAPIManager, "dxvk_nvapi", "dxvk_nvapi_version"),
(D3DExtrasManager, "d3d_extras", "d3d_extras_version"),
(dgvoodoo2Manager, "dgvoodoo2", "dgvoodoo2_version"),
]
managers = {}
is_proton = proton.is_proton_path(self.get_executable())
for manager_class, enabled_option, version_option in manager_classes:
enabled = bool(self.runner_config.get(enabled_option))
version = self.runner_config.get(version_option)
if enabled or not enabled_only:
manager = manager_class(self.prefix_path, arch=self.wine_arch, version=version)
if not manager.can_enable():
enabled = False
if not manager.proton_compatible and is_proton:
enabled = False
if enabled or not enabled_only:
managers[manager] = enabled
return managers
def get_dll_overrides(self):
"""Return the DLLs overriden at runtime"""
try:
overrides = self.runner_config["overrides"]
except KeyError:
overrides = {}
if not isinstance(overrides, dict):
logger.warning("DLL overrides is not a mapping: %s", overrides)
overrides = {}
return overrides
def get_env(self, os_env=False, disable_runtime=False):
"""Return environment variables used by the game"""
# Always false to runner.get_env, the default value
# of os_env is inverted in the wine class,
# the OS env is read later.
env = super().get_env(os_env, disable_runtime=disable_runtime)
show_debug = self.runner_config.get("show_debug", "-all")
if show_debug != "inherit":
env["WINEDEBUG"] = show_debug
env["DXVK_LOG_LEVEL"] = "error"
env["UMU_LOG"] = "1"
if show_debug == "":
env["DXVK_LOG_LEVEL"] = "info"
env["UMU_LOG"] = "warning"
elif show_debug == "+all":
env["DXVK_LOG_LEVEL"] = "debug"
env["UMU_LOG"] = "debug"
env["WINEARCH"] = self.wine_arch
wine_exe = self.get_executable()
is_proton = proton.is_proton_path(wine_exe)
wine_config_version = self.read_version_from_config()
if wine_config_version == GE_PROTON_LATEST:
env["PROTONPATH"] = "GE-Proton"
is_proton_version = True
else:
is_proton_version = proton.is_proton_version(wine_config_version)
env["WINE"] = wine_exe
files_dir = get_runner_files_dir_for_version(wine_config_version)
if files_dir:
env["WINE_MONO_CACHE_DIR"] = os.path.join(files_dir, "mono")
env["WINE_GECKO_CACHE_DIR"] = os.path.join(files_dir, "gecko")
# We don't want to override gstreamer for proton, it has it's own version
if files_dir and not is_proton and is_gstreamer_build(wine_exe):
path_64 = os.path.join(files_dir, "lib64/gstreamer-1.0/")
path_32 = os.path.join(files_dir, "lib/gstreamer-1.0/")
if os.path.exists(path_64) or os.path.exists(path_32):
env["GST_PLUGIN_SYSTEM_PATH_1_0"] = path_64 + ":" + path_32
if self.prefix_path:
env["WINEPREFIX"] = self.prefix_path
if "WINEESYNC" not in env:
env["WINEESYNC"] = "1" if self.runner_config.get("esync") else "0"
# Proton uses an env-var with the opposite sense!
if "PROTON_NO_ESYNC" not in env and not self.runner_config.get("esync"):
env["PROTON_NO_ESYNC"] = "1"
if "WINEFSYNC" not in env:
env["WINEFSYNC"] = "1" if self.runner_config.get("fsync") else "0"
# Proton uses an env-var with the opposite sense!
if "PROTON_NO_FSYNC" not in env and not self.runner_config.get("fsync"):
env["PROTON_NO_FSYNC"] = "1"
if self.runner_config.get("fsr"):
env["WINE_FULLSCREEN_FSR"] = "1"
if self.runner_config.get("dxvk_nvapi"):
env["DXVK_NVAPIHACK"] = "0"
env["DXVK_ENABLE_NVAPI"] = "1"
if self.runner_config.get("battleye"):
env["PROTON_BATTLEYE_RUNTIME"] = os.path.join(settings.RUNTIME_DIR, "battleye_runtime")
if self.runner_config.get("eac"):
env["PROTON_EAC_RUNTIME"] = os.path.join(settings.RUNTIME_DIR, "eac_runtime")
using_dxvk = self.runner_config.get("dxvk") and LINUX_SYSTEM.is_vulkan_supported
if not using_dxvk:
env["PROTON_USE_WINED3D"] = "1"
if "PROTON_DXVK_D3D8" not in env:
env["PROTON_DXVK_D3D8"] = "1" if using_dxvk else "0"
if (
self.runner_config.get("Graphics") == "wayland"
and is_winewayland_available(wine_config_version)
and is_proton_version
):
env["PROTON_ENABLE_WAYLAND"] = "1"
if self.runner_config.get("proton_hdr"):
env["PROTON_ENABLE_HDR"] = "1"
for dll_manager in self.get_dll_managers(enabled_only=True):
self.dll_overrides.update(dll_manager.get_enabling_dll_overrides())
overrides = self.get_dll_overrides()
if overrides:
self.dll_overrides.update(overrides)
env["WINEDLLOVERRIDES"] = get_overrides_env(self.dll_overrides)
return env
def finish_env(self, env: Dict[str, str], game) -> None:
super().finish_env(env, game)
wine_exe = self.get_executable()
if proton.is_proton_path(wine_exe) or proton.is_umu_path(wine_exe):
game_id = proton.get_game_id(game, env)
proton.update_proton_env(wine_exe, env, game_id=game_id)
def get_runtime_env(self):
"""Return runtime environment variables with path to wine for Lutris builds"""
try:
wine_path = os.path.dirname(os.path.dirname(self.get_executable()))
except MisconfigurationError:
wine_path = None
return runtime.get_env(
version="Ubuntu-18.04",
prefer_system_libs=self.system_config.get("prefer_system_libs", True),
wine_path=wine_path,
)
def get_wine_executable_pids(self):
"""Return a list of pids of processes using the current wine exe."""
try:
exe = self.get_executable()
if proton.is_proton_path(exe):
logger.debug("Tracking PIDs of Proton games is not possible at the moment")
return set()
if not exe.startswith("/"):
exe = system.find_required_executable(exe)
pids = system.get_pids_using_file(exe)
if self.wine_arch == "win64" and os.path.basename(exe) == "wine":
pids = pids | system.get_pids_using_file(exe + "64")
except MisconfigurationError:
return set()
# Add wineserver PIDs to the mix (at least one occurence of fuser not
# picking the games's PID from wine/wine64 but from wineserver for some
# unknown reason.
pids = pids | system.get_pids_using_file(os.path.join(os.path.dirname(exe), "wineserver"))
return pids
def configure_desktop_integration(self, wine_prefix):
try:
if self.game_config.get("desktop_integration", False):
wine_prefix.install_desktop_integration()
else:
wine_prefix.remove_desktop_integration()
except Exception as ex:
logger.exception("Failed to setup desktop integration, the prefix may not be valid: %s", ex)
def play(self) -> Dict[str, Any]: # pylint: disable=too-many-return-statements
game_exe = self.game_exe
arguments: str = self.game_config.get("args", "")
launch_info: dict = {"env": self.get_env(os_env=False)}
using_dxvk = self.runner_config.get("dxvk") and LINUX_SYSTEM.is_vulkan_supported
if using_dxvk:
# Set this to 1 to enable access to more RAM for 32-bit applications
launch_info["env"]["WINE_LARGE_ADDRESS_AWARE"] = "1"
if not game_exe or not system.path_exists(game_exe):
raise MissingGameExecutableError(filename=game_exe)
if launch_info["env"].get("WINEESYNC") == "1":
limit_set = is_esync_limit_set()
if not limit_set:
raise EsyncLimitError()
if launch_info["env"].get("WINEFSYNC") == "1":
fsync_supported = is_fsync_supported()
if not fsync_supported:
raise FsyncUnsupportedError()
command = self.get_command()
game_exe, args, _working_dir = get_real_executable(game_exe, self.working_dir)
command.append(game_exe)
if args:
command = command + args
if arguments:
for arg in split_arguments(arguments):
command.append(arg)
launch_info["command"] = command
return launch_info
def filter_game_pids(self, candidate_pids: Iterable[int], game_uuid: str, game_folder: str) -> Set[int]:
"""Checks the pids given and returns a set containing only those that are part of the running game,
identified by its UUID and directory."""
if proton.is_proton_path(self.get_executable()):
folder_pids = set()
gamescope_pids = set()
has_gamescope = self.system_config.get("gamescope")
for pid in candidate_pids:
proc = Process(pid)
cmdline = proc.cmdline or ""
# pressure-vessel: This could potentially pick up PIDs not started by lutris?
if game_folder in cmdline or "pressure-vessel" in cmdline:
folder_pids.add(pid)
# Include gamescope-related processes when gamescope is enabled
if has_gamescope and (proc.name or "").startswith("gamescope"):
gamescope_pids.add(pid)
uuid_pids = set(pid for pid in candidate_pids if Process(pid).environ.get("LUTRIS_GAME_UUID") == game_uuid)
return (folder_pids & uuid_pids) | gamescope_pids
else:
return super().filter_game_pids(candidate_pids, game_uuid, game_folder)
def force_stop_game(self, game_pids: Iterable[int]) -> None:
"""Kill WINE with kindness, or at least with -k. This seems to leave a process
alive for some reason, but the caller will detect this and SIGKILL it."""
winekill(
self.prefix_path,
arch=self.wine_arch,
wine_path=self.get_executable(),
env=self.get_env(),
initial_pids=game_pids,
)
# Kill non-Wine processes (like gamescope) that winekill doesn't handle
super().force_stop_game(game_pids)
def extract_icon(self, game_slug):
"""Extracts the 128*128 icon from EXE and saves it, if not resizes the biggest icon found.
returns true if an icon is saved, false if not"""
try:
wantedsize = (128, 128)
pathtoicon = settings.ICON_PATH + "/lutris_" + game_slug + ".png"
exe = self.game_exe
if not exe or os.path.exists(pathtoicon) or not PEFILE_AVAILABLE:
return False
extractor = IconExtractor(exe)
icon = extractor.get_best_icon()
if not icon.size == wantedsize:
icon = icon.resize(wantedsize)
icon.save(pathtoicon)
return True
except Exception as ex:
logger.exception("Unable to extract icon from %s: %s", exe, ex)
return False