#!/usr/bin/env python3 #script to launch Wine with the correct environment import fcntl import array import fnmatch import os import re import shutil import errno import platform import resource import stat import subprocess import sys import shlex import uuid import json import time from ctypes import CDLL from ctypes import CFUNCTYPE from ctypes import POINTER from ctypes import Structure from ctypes import addressof from ctypes import cast from ctypes import get_errno from ctypes import sizeof from ctypes import c_int from ctypes import c_int64 from ctypes import c_uint from ctypes import c_long from ctypes import c_char_p from ctypes import c_void_p from ctypes import c_size_t from ctypes import c_ssize_t from filelock import FileLock from random import randrange from pathlib import Path import protonfixes #To enable debug logging, copy "user_settings.sample.py" to "user_settings.py" #and edit it if needed. import utilities CURRENT_PREFIX_VERSION="GE-Proton11-6" PFX="Proton: " ld_path_var = "LD_LIBRARY_PATH" # https://bugs.kde.org/show_bug.cgi?id=524702 # Ark can omit empty directory entries when extracting Proton tarballs. Keep # fresh prefixes valid even when their extracted default_pfx lost Wine's shell # directory skeleton. STANDARD_PREFIX_DIRECTORIES = ( "drive_c/ProgramData/Microsoft/Windows/Start Menu/Programs/Administrative Tools", "drive_c/ProgramData/Microsoft/Windows/Start Menu/Programs/StartUp", "drive_c/ProgramData/Microsoft/Windows/Templates", "drive_c/users/Public/Desktop", "drive_c/users/Public/Documents", "drive_c/users/Public/Music", "drive_c/users/Public/Pictures", "drive_c/users/Public/Videos", "drive_c/users/steamuser/AppData/Local/Microsoft/Windows/History", "drive_c/users/steamuser/AppData/Local/Microsoft/Windows/INetCache", "drive_c/users/steamuser/AppData/Local/Microsoft/Windows/INetCookies", "drive_c/users/steamuser/AppData/Local/Temp", "drive_c/users/steamuser/AppData/LocalLow", "drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/AccountPictures", "drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/Network Shortcuts", "drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/Printer Shortcuts", "drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/Recent", "drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/SendTo", "drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Administrative Tools", "drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/StartUp", "drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/Templates", "drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/Themes", "drive_c/users/steamuser/Contacts", "drive_c/users/steamuser/Desktop", "drive_c/users/steamuser/Documents/Downloads", "drive_c/users/steamuser/Documents/Music", "drive_c/users/steamuser/Documents/Pictures", "drive_c/users/steamuser/Documents/Templates", "drive_c/users/steamuser/Documents/Videos", "drive_c/users/steamuser/Downloads", "drive_c/users/steamuser/Favorites", "drive_c/users/steamuser/Links", "drive_c/users/steamuser/Music", "drive_c/users/steamuser/Pictures/Screenshots", "drive_c/users/steamuser/Saved Games", "drive_c/users/steamuser/Searches", "drive_c/users/steamuser/Videos", ) proton_config = set() if "PROTON_ADD_CONFIG" in os.environ: try: config = os.environ["PROTON_ADD_CONFIG"].split(',') for c in config: proton_config.add(c) except: pass def empty_directory(path): # Walk through the directory structure top-down (False) for root, dirs, files in os.walk(path, topdown=False): # Remove files and symlinks to files for name in files: item_path = os.path.join(root, name) if os.path.islink(item_path): os.remove(item_path) # Remove symlink else: os.remove(item_path) # Remove file # Remove directories and symlinks to directories for name in dirs: item_path = os.path.join(root, name) if os.path.islink(item_path): os.remove(item_path) # Remove symlink else: os.rmdir(item_path) # Remove directory # If -steamdeck mode is enabled, try to copy existing game prefixes to steamdeck's prefix location -- this helps to preserve mods and saves across systems # Note: this completely wipes the existing destination prefix and copies the source prefix over instead. # source = game partition, destination = steamdeck prefix location # From game partition to steamdeck (steamdeck enabled) if os.getenv("SteamDeck") == "1" and os.getenv("COPYPREFIX") == "1": # Get the value of the STEAM_COMPAT_INSTALL_PATH environment variable steam_compat_install_path = os.getenv('STEAM_COMPAT_INSTALL_PATH') # Navigate up two directories parent_directory = Path(steam_compat_install_path).parents[1] # Check if the "compatdata/" folder exists and is not empty compatdata_path = parent_directory / "compatdata" / str(os.environ.get("SteamAppId", 0)) if compatdata_path.exists() and os.listdir(compatdata_path): # Define the destination directory steamdeck_compatdata = Path(os.environ["STEAM_BASE_FOLDER"]) / "steamapps" / "compatdata" / str(os.environ.get("SteamAppId", 0)) # Remove destination if it already exists: if os.path.isdir(steamdeck_compatdata) and os.listdir(steamdeck_compatdata): empty_directory(steamdeck_compatdata) # Copy the "compatdata/" folder to the destination directory # Use dirs_exist_ok=True to avoid FileExistsError if the destination directory already exists shutil.copytree(compatdata_path, steamdeck_compatdata, dirs_exist_ok=True, symlinks=True) # Check if the "shadercache/" folder exists and is not empty shadercache_path = parent_directory / "shadercache" / str(os.environ.get("SteamAppId", 0)) if shadercache_path.exists() and os.listdir(shadercache_path): # Define the destination directory steamdeck_shadercache = Path(os.environ["STEAM_BASE_FOLDER"]) / "steamapps" / "shadercache" / str(os.environ.get("SteamAppId", 0)) # Remove destination if it already exists: if os.path.isdir(steamdeck_shadercache) and os.listdir(steamdeck_shadercache): empty_directory(steamdeck_shadercache) # Copy the "shadercache/" folder to the destination directory # Use dirs_exist_ok=True to avoid FileExistsError if the destination directory already exists shutil.copytree(shadercache_path, steamdeck_shadercache, dirs_exist_ok=True, symlinks=True) # source = steamdeck prefix location, destination = game partition # From steamdeck to game partition (steamdeck disabled) if os.getenv("SteamDeck") == "0" or "SteamDeck" not in os.environ: if os.getenv("COPYPREFIX") == "1": # Get the value of the STEAM_COMPAT_INSTALL_PATH environment variable steam_compat_install_path = os.getenv('STEAM_COMPAT_INSTALL_PATH') # Navigate up two directories parent_directory = Path(steam_compat_install_path).parents[1] # Check if the "compatdata/" folder exists and is not empty steamdeck_compatdata = Path(os.environ["STEAM_BASE_FOLDER"]) / "steamapps" / "compatdata" / str(os.environ.get("SteamAppId", 0)) if os.path.isdir(steamdeck_compatdata) and os.listdir(steamdeck_compatdata): # Define the destination directory compatdata_path = parent_directory / "compatdata" / str(os.environ.get("SteamAppId", 0)) # Remove destination if it already exists: if os.path.isdir(compatdata_path) and os.listdir(compatdata_path): empty_directory(compatdata_path) # Copy the steamdeck_compatdata to the compatdata_path # Use dirs_exist_ok=True to avoid FileExistsError if the destination directory already exists shutil.copytree(steamdeck_compatdata, compatdata_path, dirs_exist_ok=True, symlinks=True) # Similar logic for shadercache steamdeck_shadercache = Path(os.environ["STEAM_BASE_FOLDER"]) / "steamapps" / "shadercache" / str(os.environ.get("SteamAppId", 0)) if os.path.isdir(steamdeck_shadercache) and os.listdir(steamdeck_shadercache): # Define the destination directory shadercache_path = parent_directory / "shadercache" / str(os.environ.get("SteamAppId", 0)) # Remove destination if it already exists: if os.path.isdir(shadercache_path) and os.listdir(shadercache_path): empty_directory(shadercache_path) # Copy the steamdeck_shadercache to the shadercache_path # Use dirs_exist_ok=True to avoid FileExistsError if the destination directory already exists shutil.copytree(steamdeck_shadercache, shadercache_path, dirs_exist_ok=True, symlinks=True) def file_exists(s, *, follow_symlinks): if follow_symlinks: #'exists' returns False on broken symlinks return os.path.exists(s) #'lexists' returns True on broken symlinks return os.path.lexists(s) def nonzero(s): return len(s) > 0 and s != "0" def prepend_to_env_str(env, variable, prepend_str, separator): if variable not in env: env[variable] = prepend_str else: env[variable] = prepend_str + separator + env[variable] def append_to_env_str(env, variable, append_str, separator): if variable not in env: env[variable] = append_str else: env[variable] = env[variable] + separator + append_str def log(msg): try: sys.stderr.write(PFX + msg + os.linesep) sys.stderr.flush() except OSError: # e.g. see https://github.com/ValveSoftware/Proton/issues/6277 # There's not much we can usefully do about this: printing a # warning to stderr isn't going to work any better the second time pass def file_is_wine_builtin_dll(path): if os.path.islink(path): contents = os.readlink(path) if os.path.dirname(contents).endswith(( '/lib/wine/i386-unix', '/lib/wine/i386-windows', '/lib/wine/x86_64-unix', '/lib/wine/x86_64-windows', '/lib/wine/aarch64-unix', '/lib/wine/aarch64-windows', # old paths '/lib/wine', '/lib/wine/fakedlls', '/lib64/wine', '/lib64/wine/fakedlls', '/lib64/wine/x86_64-unix', '/lib64/wine/x86_64-windows' )): # This may be a broken link to a dll in a removed Proton install return True if not file_exists(path, follow_symlinks=True): return False try: sfile = open(path, "rb") sfile.seek(0x40) tag = sfile.read(20) return tag.startswith((b"Wine placeholder DLL", b"Wine builtin DLL")) except IOError: return False def makedirs(path): try: #replace broken symlinks with a new directory if os.path.islink(path) and not file_exists(path, follow_symlinks=True): os.remove(path) os.makedirs(path) except OSError: #already exists pass def merge_user_dir(src, dst): extant_dirs = [] for src_dir, dirs, files in os.walk(src): dst_dir = src_dir.replace(src, dst, 1) #as described below, avoid merging game save subdirs, too child_of_extant_dir = False for dir_ in extant_dirs: if dir_ in dst_dir: child_of_extant_dir = True break if child_of_extant_dir: continue #we only want to copy into directories which don't already exist. games #may not react well to two save directory instances being merged. if not file_exists(dst_dir, follow_symlinks=True) or os.path.samefile(dst_dir, dst): makedirs(dst_dir) for dir_ in dirs: src_file = os.path.join(src_dir, dir_) dst_file = os.path.join(dst_dir, dir_) if os.path.islink(src_file) and not file_exists(dst_file, follow_symlinks=True): try_copy(src_file, dst_file, copy_metadata=True, follow_symlinks=False) for file_ in files: src_file = os.path.join(src_dir, file_) dst_file = os.path.join(dst_dir, file_) if not file_exists(dst_file, follow_symlinks=True): try_copy(src_file, dst_file, copy_metadata=True, follow_symlinks=False) else: extant_dirs += dst_dir def try_copy(src, dst, prefix=None, add_write_perm=True, copy_metadata=False, optional=False, follow_symlinks=True, track_file=None, link_debug=False): try: if prefix is not None: dst = os.path.join(prefix, dst) if os.path.isdir(dst): dst = os.path.join(dst, os.path.basename(src)) if file_exists(dst, follow_symlinks=False): os.remove(dst) elif track_file and prefix is not None: track_file.write(os.path.relpath(dst, prefix) + '\n') if os.path.islink(src) and not follow_symlinks: shutil.copyfile(src, dst, follow_symlinks=False) else: copyfile(src, dst) if copy_metadata: shutil.copystat(src, dst, follow_symlinks=follow_symlinks) else: shutil.copymode(src, dst, follow_symlinks=follow_symlinks) if add_write_perm: new_mode = os.lstat(dst).st_mode | stat.S_IWUSR | stat.S_IWGRP os.chmod(dst, new_mode) if not file_exists(src + '.debug', follow_symlinks=True): link_debug = False if file_exists(dst + '.debug', follow_symlinks=False): os.remove(dst + '.debug') elif link_debug and track_file: track_file.write(os.path.relpath(dst + '.debug', prefix) + '\n') if link_debug: os.symlink(src + '.debug', dst + '.debug') except FileNotFoundError as e: if optional: log('Error while copying to \"' + dst + '\": ' + e.strerror) else: raise except PermissionError as e: if e.errno == errno.EPERM: #be forgiving about permissions errors; if it's a real problem, things will explode later anyway log('Error while copying to \"' + dst + '\": ' + e.strerror) else: raise # copy_file_range implementation for old Python versions __syscall__copy_file_range = None def copy_file_range_ctypes(fd_in, fd_out, count): "Copy data using the copy_file_range syscall through ctypes, assuming x86_64 Linux" global __syscall__copy_file_range __NR_copy_file_range = 326 if __syscall__copy_file_range is None: c_int64_p = POINTER(c_int64) prototype = CFUNCTYPE(c_ssize_t, c_long, c_int, c_int64_p, c_int, c_int64_p, c_size_t, c_uint, use_errno=True) __syscall__copy_file_range = prototype(('syscall', CDLL(None, use_errno=True))) while True: ret = __syscall__copy_file_range(__NR_copy_file_range, fd_in, None, fd_out, None, count, 0) if ret >= 0 or get_errno() != errno.EINTR: break if ret < 0: raise OSError(get_errno(), errno.errorcode.get(get_errno(), 'unknown')) return ret def copyfile_reflink(srcname, dstname): "Copy srcname to dstname, making reflink if possible" global copyfile with open(srcname, 'rb', buffering=0) as src: bytes_to_copy = os.fstat(src.fileno()).st_size try: with open(dstname, 'wb', buffering=0) as dst: while bytes_to_copy > 0: bytes_to_copy -= copy_file_range(src.fileno(), dst.fileno(), bytes_to_copy) except OSError as e: if e.errno not in (errno.EXDEV, errno.ENOSYS, errno.EINVAL, errno.EOPNOTSUPP): raise e if e.errno == errno.ENOSYS or e.errno == errno.EOPNOTSUPP: copyfile = shutil.copyfile shutil.copyfile(srcname, dstname) if hasattr(os, 'copy_file_range'): copyfile = copyfile_reflink copy_file_range = os.copy_file_range elif sys.platform == 'linux' and platform.machine() == 'x86_64' and sizeof(c_void_p) == 8: copyfile = copyfile_reflink copy_file_range = copy_file_range_ctypes else: copyfile = shutil.copyfile def try_copyfile(src, dst): try: if os.path.isdir(dst): dst = dst + "/" + os.path.basename(src) if file_exists(dst, follow_symlinks=False): os.remove(dst) copyfile(src, dst) except PermissionError as e: if e.errno == errno.EPERM: #be forgiving about permissions errors; if it's a real problem, things will explode later anyway log('Error while copying to \"' + dst + '\": ' + e.strerror) else: raise def getmtimestr(*path_fragments): path = os.path.join(*path_fragments) try: return str(os.path.getmtime(path)) except IOError: return "0" def get_validated_steamapps_parent(orig_path): try: path = os.path.realpath(orig_path) if os.path.isdir(path) and os.path.basename(path) == "steamapps": parent = os.path.dirname(path) if os.access(parent, os.W_OK) and os.stat(path).st_dev == os.stat(parent).st_dev: return parent except: pass log("Error: unable to use parent for game drive, path " + orig_path) return orig_path def try_get_game_library_dir(): if "STEAM_COMPAT_INSTALL_PATH" not in g_session.env or \ "STEAM_COMPAT_LIBRARY_PATHS" not in g_session.env: return None #find library path which is a subset of the game path library_paths = g_session.env["STEAM_COMPAT_LIBRARY_PATHS"].split(":") for path in library_paths: if path in g_session.env["STEAM_COMPAT_INSTALL_PATH"]: return get_validated_steamapps_parent(path) return None def try_get_steam_dir(): if "STEAM_COMPAT_CLIENT_INSTALL_PATH" not in g_session.env: return None return g_session.env["STEAM_COMPAT_CLIENT_INSTALL_PATH"] def setup_dir_drive(compat_option, drive_name, dest_dir): drive_path = g_compatdata.prefix_dir + "dosdevices/" + drive_name if compat_option in g_session.compat_config: if not dest_dir: if file_exists(drive_path, follow_symlinks=False): os.remove(drive_path) else: if file_exists(drive_path, follow_symlinks=False): cur_tgt = os.readlink(drive_path) if cur_tgt != dest_dir: os.remove(drive_path) os.symlink(dest_dir, drive_path) else: os.symlink(dest_dir, drive_path) elif file_exists(drive_path, follow_symlinks=False): os.remove(drive_path) def setup_game_dir_drive(): setup_dir_drive("gamedrive", "s:", try_get_game_library_dir()) def setup_steam_dir_drive(): setup_dir_drive("steamdrive", "t:", try_get_steam_dir()) def unix_to_nt_file_name(path): return '\\??\\unix' + path def setup_openvr_paths(): if 'VR_PATHREG_OVERRIDE' in g_session.env: openvr_paths = g_session.env['VR_PATHREG_OVERRIDE'] del g_session.env['VR_PATHREG_OVERRIDE'] elif 'XDG_CONFIG_HOME' in g_session.env: openvr_paths = os.path.join(g_session.env['XDG_CONFIG_HOME'], 'openvr/openvrpaths.vrpath') elif 'HOME' in g_session.env: openvr_paths = os.path.join(g_session.env['HOME'], '.config/openvr/openvrpaths.vrpath') else: openvr_paths = None if not openvr_paths or not file_exists(openvr_paths, follow_symlinks=True): return try: with open(openvr_paths, 'r') as file: contents = json.load(file) except json.decoder.JSONDecodeError: return if 'runtime' not in contents or type(contents['runtime']) != list: contents['runtime'] = [] if 'config' in contents and type(contents['config']) != list: del contents['config'] if 'log' in contents and type(contents['log']) != list: del contents['log'] if 'VR_OVERRIDE' in g_session.env: g_session.env['PROTON_VR_RUNTIME'] = g_session.env['VR_OVERRIDE'] del g_session.env['VR_OVERRIDE'] elif len(contents['runtime']) > 0: g_session.env['PROTON_VR_RUNTIME'] = contents['runtime'][0] contents['runtime'] = ["C:\\vrclient\\", "C:\\vrclient"] for i, path in enumerate(contents.get('config', [])): contents['config'][i] = unix_to_nt_file_name(path) for i, path in enumerate(contents.get('log', [])): contents['log'][i] = unix_to_nt_file_name(path) openvr_paths = os.path.join(g_compatdata.prefix_dir, "drive_c/users/steamuser/AppData/Local/openvr") makedirs(openvr_paths) openvr_paths = os.path.join(openvr_paths, "openvrpaths.vrpath") with open(openvr_paths, 'w') as file: json.dump(contents, file, indent=3) # Function to find the installed location of DLL files for use by Wine/Proton # from the NVIDIA Linux driver # # See https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/issues/71 for # background on the chosen method of DLL discovery. # # On success, returns a str() of the absolute-path to the directory at which DLL # files are stored # # On failure, returns None def find_nvidia_wine_dll_dir(): try: libdl = CDLL("libdl.so.2") except (OSError): return None try: libglx_nvidia = CDLL("libGLX_nvidia.so.0") except OSError: return None # from dlinfo(3) # # struct link_map { # ElfW(Addr) l_addr; /* Difference between the # address in the ELF file and # the address in memory */ # char *l_name; /* Absolute pathname where # object was found */ # ElfW(Dyn) *l_ld; /* Dynamic section of the # shared object */ # struct link_map *l_next, *l_prev; # /* Chain of loaded objects */ # # /* Plus additional fields private to the # implementation */ # }; RTLD_DI_LINKMAP = 2 class link_map(Structure): _fields_ = [("l_addr", c_void_p), ("l_name", c_char_p), ("l_ld", c_void_p)] # from dlinfo(3) # # int dlinfo (void *restrict handle, int request, void *restrict info) dlinfo_func = libdl.dlinfo dlinfo_func.argtypes = c_void_p, c_int, c_void_p dlinfo_func.restype = c_int # Allocate a link_map object glx_nvidia_info_ptr = POINTER(link_map)() # Run dlinfo(3) on the handle to libGLX_nvidia.so.0, storing results at the # address represented by glx_nvidia_info_ptr if dlinfo_func(libglx_nvidia._handle, RTLD_DI_LINKMAP, addressof(glx_nvidia_info_ptr)) != 0: return None # Grab the contents our of our pointer glx_nvidia_info = cast(glx_nvidia_info_ptr, POINTER(link_map)).contents # Decode the path to our library to a str() if glx_nvidia_info.l_name is None: return None try: libglx_nvidia_path = os.fsdecode(glx_nvidia_info.l_name) except UnicodeDecodeError: return None # Follow any symlinks to the actual file libglx_nvidia_realpath = os.path.realpath(libglx_nvidia_path) # Go to the relative path ./nvidia/wine from our library nvidia_wine_dir = os.path.join(os.path.dirname(libglx_nvidia_realpath), "nvidia", "wine") # Check that nvngx.dll exists here, or fail if file_exists(os.path.join(nvidia_wine_dir, "nvngx.dll"), follow_symlinks=True): return nvidia_wine_dir return None EXT2_IOC_GETFLAGS = 0x80086601 EXT2_IOC_SETFLAGS = 0x40086602 EXT4_CASEFOLD_FL = 0x40000000 def set_dir_casefold_bit(dir_path): dr = os.open(dir_path, 0o644) if dr < 0: return try: dat = array.array('I', [0]) if fcntl.ioctl(dr, EXT2_IOC_GETFLAGS, dat, True) >= 0: dat[0] = dat[0] | EXT4_CASEFOLD_FL fcntl.ioctl(dr, EXT2_IOC_SETFLAGS, dat, False) except (OSError, IOError): #no problem pass os.close(dr) def get_replace_reg_value(file, key, name, new_value=None): if not file_exists(file, follow_symlinks=True): return None replaced = False out = None if new_value is not None: out = open(file + ".new", "w") found_key = False old_value = None namestr="\"" + name + "\"=" with open(file, "r") as reg_in: for line in reg_in: if not replaced: if line[0] == '[': if found_key: if out is not None: out.close() return None if line[1:len(key) + 1] == key: found_key = True elif found_key: idx = line.find(namestr) if idx != -1: old_value = line[idx + len(namestr):] if out is not None: out.write(namestr + new_value) replaced = True continue else: return old_value if out is not None: out.write(line) if out is not None: out.close() if replaced: try: os.rename(file, file + ".old") except OSError: os.remove(file) pass try: os.rename(file + ".new", file) except OSError: log("Unable to write new registry file to " + file) pass return old_value class Proton: def __init__(self, base_dir): self.base_dir = base_dir + "/" self.contrib_dir = self.path("contrib/") self.dist_dir = self.path("files/") self.bin_dir = self.path("files/bin/") self.lib_dir = self.path("files/lib/") self.fonts_dir = self.path("files/share/fonts/") self.media_dir = self.path("files/share/media/") self.wine_fonts_dir = self.path("files/share/wine/fonts/") self.wine_inf = self.path("files/share/wine/wine.inf") self.version_file = self.path("version") self.default_pfx_dir = self.path("files/share/default_pfx/") self.user_settings_file = self.path("user_settings.py") self.wine_bin = self.bin_dir + "wine" self.wineserver_bin = self.bin_dir + "wineserver" self.xrandr_bin = self.bin_dir + "xrandr" self.dist_lock = FileLock(self.path("dist.lock"), timeout=-1) self.host_pe_arch = "x86_64-windows" self.wow64_pe_arch = "i386-windows" if os.environ.get("PROTON_USE_ARM64", "1") == "1" and \ platform.machine() == 'aarch64' and \ file_exists(self.path("files/bin-arm64/"), follow_symlinks=True): self.host_pe_arch = "aarch64-windows" self.default_pfx_dir = self.path("files/share/default_pfx_arm64/") self.bin_dir = self.path("files/bin-arm64/") self.wine_bin = self.bin_dir + "wine" self.xrandr_bin = self.bin_dir + "xrandr" self.wineserver_bin = self.bin_dir + "wineserver" def path(self, d): return self.base_dir + d def arch_pe_dir(self, d, wow64): return self.lib_dir + d + "/" + (self.wow64_pe_arch if wow64 else self.host_pe_arch) + "/" def cleanup_legacy_dist(self): old_dist_dir = self.path("dist/") if file_exists(old_dist_dir, follow_symlinks=True): with self.dist_lock: if file_exists(old_dist_dir, follow_symlinks=True): shutil.rmtree(old_dist_dir) def do_steampipe_fixups(self): fixups_json = self.path("steampipe_fixups.json") fixups_mtime = self.path("files/steampipe_fixups_mtime") if file_exists(fixups_json, follow_symlinks=True): with self.dist_lock: import steampipe_fixups current_fixup_mtime = None if file_exists(fixups_mtime, follow_symlinks=True): with open(fixups_mtime, "r") as f: current_fixup_mtime = f.readline().strip() new_fixup_mtime = getmtimestr(fixups_json) if current_fixup_mtime != new_fixup_mtime: result_code = steampipe_fixups.do_restore(self.base_dir, fixups_json) if result_code == 0: with open(fixups_mtime, "w") as f: f.write(new_fixup_mtime + "\n") def missing_default_prefix(self): '''Check if the default prefix dir is missing. Returns true if missing, false if present''' return not os.path.isdir(self.default_pfx_dir) def make_default_prefix(self): with self.dist_lock: local_env = dict(g_session.env) if self.missing_default_prefix(): #make default prefix local_env["WINEPREFIX"] = self.default_pfx_dir local_env["WINEDEBUG"] = "-all" g_session.run_proc([self.wine_bin, "wineboot"], local_env) g_session.run_proc([self.wineserver_bin, "-w"], local_env) class CompatData: def __init__(self, compatdata): self.base_dir = compatdata + "/" self.prefix_dir = self.path("pfx/") self.creation_sync_guard = self.path("pfx/creation_sync_guard") self.version_file = self.path("version") self.config_info_file = self.path("config_info") self.fex_config_file = self.path("proton-fex-config.json") self.tracked_files_file = self.path("tracked_files") self.prefix_lock = FileLock(self.path("pfx.lock"), timeout=-1) self.old_machine_guid = None def path(self, d): return self.base_dir + d def remove_tracked_files(self): if not file_exists(self.tracked_files_file, follow_symlinks=True): log("Prefix has no tracked_files??") return with open(self.tracked_files_file, "r") as tracked_files: dirs = [] for f in tracked_files: path = self.prefix_dir + f.strip() if file_exists(path, follow_symlinks=False): if os.path.isfile(path) or os.path.islink(path): os.remove(path) else: dirs.append(path) for d in dirs: try: os.rmdir(d) except OSError: #not empty pass os.remove(self.tracked_files_file) os.remove(self.version_file) def upgrade_pfx(self, old_ver): if old_ver == CURRENT_PREFIX_VERSION: return log("Upgrading prefix from " + str(old_ver) + " to " + CURRENT_PREFIX_VERSION + " (" + self.base_dir + ")") if old_ver is None: return if '-' not in old_ver: #How can this happen?? log("Prefix has an invalid version?! You may want to back up user files and delete this prefix.") #If it does, just let the Wine upgrade happen and hope it works... return try: old_proton_ver, old_prefix_ver = old_ver.split('-') old_proton_maj, old_proton_min = old_proton_ver.split('.') new_proton_ver, _ = CURRENT_PREFIX_VERSION.split('-') new_proton_maj, new_proton_min = new_proton_ver.split('.') if int(new_proton_maj) < int(old_proton_maj) or \ (int(new_proton_maj) == int(old_proton_maj) and \ int(new_proton_min) < int(old_proton_min)): log("Removing newer prefix") self.old_machine_guid = get_replace_reg_value(self.prefix_dir + "system.reg", "Software\\\\Microsoft\\\\Cryptography", "MachineGuid") if old_proton_ver == "3.7" and not file_exists(self.tracked_files_file, follow_symlinks=True): #proton 3.7 did not generate tracked_files, so copy it into place first try_copy(g_proton.path("proton_3.7_tracked_files"), self.tracked_files_file) self.remove_tracked_files() path = self.prefix_dir + "/drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher/version.txt" if file_exists(path, follow_symlinks=False) and os.path.isfile(path): os.remove(path) if file_exists(self.creation_sync_guard, follow_symlinks=False): os.remove(self.creation_sync_guard) return if old_proton_ver == "3.7" and old_prefix_ver == "1": if not file_exists(self.prefix_dir + "/drive_c/windows/syswow64/kernel32.dll", follow_symlinks=True): #shipped a busted 64-bit-only installation on 20180822. detect and wipe clean log("Detected broken 64-bit-only installation, re-creating prefix.") shutil.rmtree(self.prefix_dir) return #replace broken .NET installations with wine-mono support if file_exists(self.prefix_dir + "/drive_c/windows/Microsoft.NET/NETFXRepair.exe", follow_symlinks=True) and \ file_is_wine_builtin_dll(self.prefix_dir + "/drive_c/windows/system32/mscoree.dll"): log("Broken .NET installation detected, switching to wine-mono.") #deleting this directory allows wine-mono to work shutil.rmtree(self.prefix_dir + "/drive_c/windows/Microsoft.NET") #prior to prefix version 4.11-2, all controllers were xbox controllers. wipe out the old registry entries. if (int(old_proton_maj) < 4 or (int(old_proton_maj) == 4 and int(old_proton_min) == 11)) and \ int(old_prefix_ver) < 2: log("Removing old xinput registry entries.") with open(self.prefix_dir + "system.reg", "r") as reg_in: with open(self.prefix_dir + "system.reg.new", "w") as reg_out: for line in reg_in: if line[0] == '[' and "CurrentControlSet" in line and "IG_" in line: if "DeviceClasses" in line: reg_out.write(line.replace("DeviceClasses", "DeviceClasses_old")) elif "Enum" in line: reg_out.write(line.replace("Enum", "Enum_old")) else: reg_out.write(line) try: os.rename(self.prefix_dir + "system.reg", self.prefix_dir + "system.reg.old") except OSError: os.remove(self.prefix_dir + "system.reg") pass try: os.rename(self.prefix_dir + "system.reg.new", self.prefix_dir + "system.reg") except OSError: log("Unable to write new registry file to " + self.prefix_dir + "system.reg") pass delete_dde_keys = {} # Prior to prefix version 6.3-3, ShellExecute* APIs used DDE. # Wipe out old registry entries. if int(old_proton_maj) < 6 or (int(old_proton_maj) == 6 and int(old_proton_min) < 3) or \ (int(old_proton_maj) == 6 and int(old_proton_min) == 3 and int(old_prefix_ver) < 3): delete_dde_keys = { "[Software\\\\Classes\\\\htmlfile\\\\shell\\\\open\\\\ddeexec", "[Software\\\\Classes\\\\pdffile\\\\shell\\\\open\\\\ddeexec", "[Software\\\\Classes\\\\xmlfile\\\\shell\\\\open\\\\ddeexec", "[Software\\\\Classes\\\\ftp\\\\shell\\\\open\\\\ddeexec", "[Software\\\\Classes\\\\http\\\\shell\\\\open\\\\ddeexec", "[Software\\\\Classes\\\\https\\\\shell\\\\open\\\\ddeexec", } dde_wb = '@="\\"C:\\\\windows\\\\system32\\\\winebrowser.exe\\" -nohome"' log("Removing ShellExecute DDE registry entries.") delete_keys = {} # Remove shadergpures.sys service which was set up in prefix versions before 11 if int(old_proton_maj) < 11: delete_keys = { "[System\\\\ControlSet001\\\\Services\\\\SharedGpuResources]", "[System\\\\CurrentControlSet\\\\Services\\\\SharedGpuResources]", } log("Removing sharedgpures.sys service.") if delete_dde_keys or delete_keys: sysreg_fp = self.prefix_dir + "system.reg" new_sysreg_fp = self.prefix_dir + "system.reg.new" with open(sysreg_fp, "r") as reg_in: with open(new_sysreg_fp, "w") as reg_out: deleting_key = False for line in reg_in: if deleting_key: if line[0] != '[': continue deleting_key = False if line.split(' ')[0] in delete_keys: deleting_key = True continue if not delete_dde_keys: reg_out.write(line) continue if line[:line.find("ddeexec")+len("ddeexec")] in delete_dde_keys: reg_out.write(line.replace("ddeexec", "ddeexec_old", 1)) elif line.rstrip() == dde_wb: reg_out.write(line.replace("-nohome", "%1")) else: reg_out.write(line) # Slightly randomize backup file name to avoid colliding with # other backups. backup_sysreg_fp = "{}system.reg.{:x}.old".format(self.prefix_dir, randrange(16 ** 8)) try: os.rename(sysreg_fp, backup_sysreg_fp) except OSError: log("Failed to back up old system.reg. Simply deleting it.") os.remove(sysreg_fp) pass try: os.rename(new_sysreg_fp, sysreg_fp) except OSError: log("Unable to write new registry file to " + sysreg_fp) pass if int(old_proton_maj) < 10 or (int(old_proton_maj) == 10 and int(old_proton_min) == 0 and int(old_prefix_ver) < 105): with open(self.creation_sync_guard, "w"): pass os.sync() stale_builtins = [self.prefix_dir + "/drive_c/windows/system32/amd_ags_x64.dll", self.prefix_dir + "/drive_c/windows/syswow64/amd_ags_x64.dll", self.prefix_dir + "/drive_c/windows/system32/ir50_32.dll", self.prefix_dir + "/drive_c/windows/syswow64/ir50_32.dll" ] for builtin in stale_builtins: if file_exists(builtin, follow_symlinks=False) and file_is_wine_builtin_dll(builtin): log("Removing stale builtin " + builtin) os.remove(builtin) stale_vkd3d = [self.prefix_dir + "/drive_c/windows/system32/libvkd3d-1.dll", self.prefix_dir + "/drive_c/windows/syswow64/libvkd3d-1.dll", self.prefix_dir + "/drive_c/windows/system32/libvkd3d-shader-1.dll", self.prefix_dir + "/drive_c/windows/syswow64/libvkd3d-shader-1.dll" ] for dll in stale_vkd3d: if file_exists(dll, follow_symlinks=False): log("Removing stale vkd3d dll " + dll) os.remove(dll) except ValueError: log("Prefix has an invalid version?! You may want to back up user files and delete this prefix.") #Just let the Wine upgrade happen and hope it works... with open(self.creation_sync_guard, "w"): pass os.sync() return def pfx_copy(self, src, dst, dll_copy=False): if os.path.islink(src): contents = os.readlink(src) if os.path.dirname(contents).endswith(('/lib/wine/i386-unix', '/lib/wine/i386-windows', '/lib/wine/x86_64-unix', '/lib/wine/x86_64-windows', '/lib/wine/aarch64-unix', '/lib/wine/aarch64-windows', # old paths: '/lib64/wine/x86_64-unix', '/lib64/wine/x86_64-windows')): # wine builtin dll # make the destination an absolute symlink contents = os.path.normpath(os.path.join(os.path.dirname(src), contents)) if dll_copy: try_copyfile(src, dst) else: os.symlink(contents, dst) else: try_copyfile(src, dst) def copy_pfx(self): with open(self.tracked_files_file, "w") as tracked_files: for src_dir, dirs, files in os.walk(g_proton.default_pfx_dir): rel_dir = src_dir.replace(g_proton.default_pfx_dir, "", 1).lstrip('/') if len(rel_dir) > 0: rel_dir = rel_dir + "/" dst_dir = src_dir.replace(g_proton.default_pfx_dir, self.prefix_dir, 1) if not file_exists(dst_dir, follow_symlinks=True): makedirs(dst_dir) tracked_files.write(rel_dir + "\n") for dir_ in dirs: src_file = os.path.join(src_dir, dir_) dst_file = os.path.join(dst_dir, dir_) if os.path.islink(src_file) and not file_exists(dst_file, follow_symlinks=True): self.pfx_copy(src_file, dst_file) for file_ in files: src_file = os.path.join(src_dir, file_) dst_file = os.path.join(dst_dir, file_) if not file_exists(dst_file, follow_symlinks=True): self.pfx_copy(src_file, dst_file) tracked_files.write(rel_dir + file_ + "\n") created_dirs = [] for rel_dir in STANDARD_PREFIX_DIRECTORIES: dst_dir = self.prefix_dir.rstrip('/') for component in rel_dir.split('/'): dst_dir = os.path.join(dst_dir, component) if not file_exists(dst_dir, follow_symlinks=True): makedirs(dst_dir) created_dirs.append(os.path.relpath(dst_dir, self.prefix_dir)) # Track children before parents so prefix upgrades can remove them. for rel_dir in reversed(created_dirs): tracked_files.write(rel_dir + "/\n") # Set .update-timestamp so Wine doesn't try to update the prefix. # This is needed in case the mtime of wine.inf has changed in distribution. with open(os.path.join(self.prefix_dir, '.update-timestamp'), 'w') as update_timestamp: mtime = int(os.stat(g_proton.wine_inf).st_mtime) update_timestamp.write(str(mtime)) def update_builtin_libs(self, dll_copy_patterns): dll_copy_patterns = dll_copy_patterns.split(',') prev_tracked_files = set() with open(self.tracked_files_file, "r") as tracked_files: for line in tracked_files: prev_tracked_files.add(line.strip()) with open(self.tracked_files_file, "a") as tracked_files: for src_dir, _, files in os.walk(g_proton.default_pfx_dir): rel_dir = src_dir.replace(g_proton.default_pfx_dir, "", 1).lstrip('/') if len(rel_dir) > 0: rel_dir = rel_dir + "/" dst_dir = src_dir.replace(g_proton.default_pfx_dir, self.prefix_dir, 1) if not file_exists(dst_dir, follow_symlinks=True): makedirs(dst_dir) tracked_files.write(rel_dir + "\n") for file_ in files: src_file = os.path.join(src_dir, file_) dst_file = os.path.join(dst_dir, file_) if not file_is_wine_builtin_dll(src_file): # Not a builtin library continue if file_is_wine_builtin_dll(dst_file): os.unlink(dst_file) elif file_exists(dst_file, follow_symlinks=False): # builtin library was replaced continue else: os.makedirs(dst_dir, exist_ok=True) dll_copy = any(fnmatch.fnmatch(file_, pattern) for pattern in dll_copy_patterns) self.pfx_copy(src_file, dst_file, dll_copy) tracked_name = rel_dir + file_ if tracked_name not in prev_tracked_files: tracked_files.write(tracked_name + "\n") def create_symlink(self, lname, fname): if file_exists(lname, follow_symlinks=False): if os.path.islink(lname): os.remove(lname) os.symlink(fname, lname) else: os.symlink(fname, lname) def create_fonts_symlinks(self): ALTERNATIVES = { ('1313860', 'arial.ttf'), # FIFA 21 ('1506830', 'arial.ttf'), # FIFA 22 } windowsfonts = self.prefix_dir + "/drive_c/windows/Fonts" makedirs(windowsfonts) sgi = os.environ.get('SteamGameId', '') for fonts_dir in [g_proton.fonts_dir, g_proton.wine_fonts_dir]: for font in os.listdir(fonts_dir): if not font.endswith('.ttf') and not font.endswith('.ttc'): continue lname = os.path.join(windowsfonts, font) fname = os.path.join(fonts_dir, font) if (sgi, font) in ALTERNATIVES: fname = os.path.join(fonts_dir, 'alt', font) self.create_symlink(lname, fname) def remove_prefix_file(self, path): path = os.path.join(self.prefix_dir, path) if file_exists(path, follow_symlinks=False): os.remove(path) if file_exists(path + ".debug", follow_symlinks=False): os.remove(path + ".debug") def setup_ddraw(self, use_d7vk): wine_ddraw64 = g_proton.arch_pe_dir("wine", False) + "ddraw.dll" wine_ddraw32 = g_proton.arch_pe_dir("wine", True) + "ddraw.dll" d7vk_ddraw = g_proton.arch_pe_dir("wine/d7vk", True) + "ddraw.dll" system32_ddraw = "drive_c/windows/system32/ddraw.dll" syswow64_ddraw = "drive_c/windows/syswow64/ddraw.dll" syswow64_wine_ddraw = "drive_c/windows/syswow64/ddraw_.dll" if not use_d7vk: if file_exists(os.path.join(self.prefix_dir, syswow64_wine_ddraw), follow_symlinks=False): self.remove_prefix_file(syswow64_ddraw) self.remove_prefix_file(syswow64_wine_ddraw) try_copy(wine_ddraw32, syswow64_ddraw, prefix=self.prefix_dir, link_debug=True) return self.remove_prefix_file(syswow64_ddraw) self.remove_prefix_file(syswow64_wine_ddraw) if not file_exists(os.path.join(self.prefix_dir, system32_ddraw), follow_symlinks=True): try_copy(wine_ddraw64, system32_ddraw, prefix=self.prefix_dir, link_debug=True) if not file_exists(d7vk_ddraw, follow_symlinks=True): log("D7VK requested but ddraw.dll was not found at %s" % d7vk_ddraw) try_copy(wine_ddraw32, syswow64_ddraw, prefix=self.prefix_dir, link_debug=True) return try_copy(wine_ddraw32, syswow64_wine_ddraw, prefix=self.prefix_dir, link_debug=True) try_copy(d7vk_ddraw, syswow64_ddraw, prefix=self.prefix_dir, link_debug=True) append_to_env_str(g_session.env, "WINEDLLOVERRIDES", "ddraw=n,b", ";") def migrate_user_paths(self): #move winxp-style paths to vista+ paths. we can't do this in #upgrade_pfx because Steam may drop cloud files here at any time. for (old, new, link) in \ [ ("drive_c/users/steamuser/Local Settings/Application Data", self.prefix_dir + "drive_c/users/steamuser/AppData/Local", "../AppData/Local"), ("drive_c/users/steamuser/Application Data", self.prefix_dir + "drive_c/users/steamuser/AppData/Roaming", "./AppData/Roaming"), ("drive_c/users/steamuser/My Documents", self.prefix_dir + "drive_c/users/steamuser/Documents", "./Documents"), ]: #running unofficial Proton/Wine builds against a Proton prefix could #create an infinite symlink loop. detect this and clean it up. if file_exists(new, follow_symlinks=False) and os.path.islink(new) and os.readlink(new).endswith(old): os.remove(new) old = self.prefix_dir + old if file_exists(old, follow_symlinks=False) and not os.path.islink(old): merge_user_dir(src=old, dst=new) os.rename(old, old + " BACKUP") if not file_exists(old, follow_symlinks=False): makedirs(os.path.dirname(old)) os.symlink(src=link, dst=old) elif os.path.islink(old) and not (os.readlink(old) == link): os.remove(old) os.symlink(src=link, dst=old) def setup_prefix(self): with self.prefix_lock: if file_exists(self.version_file, follow_symlinks=True): with open(self.version_file, "r") as f: old_ver = f.readline().strip() else: old_ver = None self.upgrade_pfx(old_ver) if not file_exists(self.creation_sync_guard, follow_symlinks=False): makedirs(self.prefix_dir + "/drive_c") set_dir_casefold_bit(self.prefix_dir + "/drive_c") self.copy_pfx() machine_guid = self.old_machine_guid if machine_guid is None: machine_guid = "\"" + str(uuid.uuid4()) + "\"" get_replace_reg_value(self.prefix_dir + "system.reg", "Software\\\\Microsoft\\\\Cryptography", "MachineGuid", machine_guid) os.sync() with open(self.creation_sync_guard, "w"): pass os.sync() self.migrate_user_paths() if not file_exists(self.prefix_dir + "/dosdevices/c:", follow_symlinks=False): os.makedirs(f"{self.prefix_dir}/dosdevices", exist_ok=True) os.symlink("../drive_c", self.prefix_dir + "/dosdevices/c:") if not file_exists(self.prefix_dir + "/dosdevices/z:", follow_symlinks=False): os.makedirs(f"{self.prefix_dir}/dosdevices", exist_ok=True) os.symlink("/", self.prefix_dir + "/dosdevices/z:") # collect configuration info steamdir = os.environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] use_wined3d = "wined3d" in g_session.compat_config use_dxvk_dxgi = not use_wined3d and \ not ("WINEDLLOVERRIDES" in g_session.env and "dxgi=b" in g_session.env["WINEDLLOVERRIDES"]) use_nvapi = (('disablenvapi' not in g_session.compat_config or 'forcenvapi' in g_session.compat_config) and g_proton.host_pe_arch != "aarch64-windows") use_dxvk_d3d8 = "dxvkd3d8" in g_session.compat_config builtin_dll_copy = os.environ.get("PROTON_DLL_COPY", #dxsetup redist "d3dcompiler_*.dll," + "d3dcsx*.dll," + "d3dx*.dll," + "dx8vb.dll," + "x3daudio*.dll," + "xactengine*.dll," + "xapofx*.dll," + "xaudio*.dll," + "xinput*.dll," + #vcruntime redist "atl1*.dll," + "atl.dll," + "concrt*.dll," + "msvcp1*.dll," + "msvcrt*.dll," + "msvcp7*.dll," + "msvcp6*.dll," + "msvcp_win.dll," + "msvcr1*.dll," + "msvcrt*.dll," + "msvcr7*.dll," + "vcamp1*.dll," + "vcomp1*.dll," + "vccorlib1*.dll," + "vcruntime1*.dll," + "ucrtbase.dll," + #there are different instances (comctl32.dll in system32 and a copy of comctl32_v6.dll #in winsxs, handle that by leaving a copy) "comctl32.dll," + #some games balk at ntdll symlink(?) "ntdll.dll," + #some games require official vulkan loader "vulkan-1.dll," + #let the games install native "ir50_32.dll" ) # If any of this info changes, we must rerun the tasks below prefix_info = '\n'.join(( CURRENT_PREFIX_VERSION, g_proton.fonts_dir, g_proton.lib_dir, steamdir, getmtimestr(steamdir, 'legacycompat', 'steamclient.dll'), getmtimestr(steamdir, 'legacycompat', 'steamclient64.dll'), getmtimestr(steamdir, 'legacycompat', 'Steam.dll'), g_proton.default_pfx_dir, getmtimestr(g_proton.default_pfx_dir, 'system.reg'), str(use_wined3d), str(use_dxvk_dxgi), builtin_dll_copy, str(use_nvapi), str(use_dxvk_d3d8), )) # check whether any prefix config has changed try: with open(self.config_info_file, "r") as f: old_prefix_info = f.read() except IOError: old_prefix_info = "" if old_ver != CURRENT_PREFIX_VERSION or old_prefix_info != prefix_info: # update builtin dll symlinks or copies self.update_builtin_libs(builtin_dll_copy) with open(self.config_info_file, "w") as f: f.write(prefix_info) with open(self.version_file, "w") as f: f.write(CURRENT_PREFIX_VERSION + "\n") #create font files symlinks self.create_fonts_symlinks() with open(self.tracked_files_file, "a") as tracked_files: #copy steam files into place steam_dir = "drive_c/Program Files (x86)/Steam/" makedirs(self.prefix_dir + steam_dir) filestocopy = [("steamclient.dll", "steamclient.dll"), ("steamclient64.dll", "steamclient64.dll"), ("GameOverlayRenderer64.dll", "GameOverlayRenderer64.dll"), ("SteamService.exe", "steam.exe"), ("Steam.dll", "Steam.dll")] for (src,tgt) in filestocopy: srcfile = steamdir + '/legacycompat/' + src if os.path.isfile(srcfile): try_copy(srcfile, steam_dir + tgt, prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) filestocopy = [("steamclient64.dll", "steamclient64.dll"), ("GameOverlayRenderer.dll", "GameOverlayRenderer.dll"), ("GameOverlayRenderer64.dll", "GameOverlayRenderer64.dll")] for (src,tgt) in filestocopy: srcfile = g_proton.path(src) if os.path.isfile(srcfile): try_copy(srcfile, steam_dir + tgt, prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) # CW Bug 19152. IL-2 Sturmovik: Cliffs of Dover Blitz Edition needs user's localconfig.vdf if os.environ.get("SteamGameId", 0) == "754530": srcdir = os.path.join(steamdir, 'userdata') dstdir = os.path.join(self.prefix_dir, steam_dir, 'userdata') # figuring out the current user is hard, so copy the config for all users for userid in os.listdir(srcdir): srcvdf = os.path.join(srcdir, userid, 'config', 'localconfig.vdf') dstvdf = os.path.join(dstdir, userid, 'config', 'localconfig.vdf') if not os.path.exists(srcvdf): continue os.makedirs(os.path.dirname(dstvdf), exist_ok=True) shutil.copyfile(srcvdf, dstvdf) #copy openvr files into place makedirs(self.prefix_dir + "/drive_c/vrclient/bin") try_copy(g_proton.arch_pe_dir("wine", True) + "vrclient.dll", "drive_c/vrclient/bin", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) try_copy(g_proton.arch_pe_dir("wine", False) + "vrclient_x64.dll", "drive_c/vrclient/bin", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) try_copy(g_proton.arch_pe_dir("wine/dxvk", True) + "openvr_api_dxvk.dll", "drive_c/windows/syswow64", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) try_copy(g_proton.arch_pe_dir("wine/dxvk", False) + "openvr_api_dxvk.dll", "drive_c/windows/system32", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) #copy x86_64 wineopenxr files into place makedirs(self.prefix_dir + "/drive_c/openxr") try_copy(g_proton.dist_dir + "share/openxr/wineopenxr64.json", "drive_c/openxr", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) if use_wined3d: dxvkfiles = [] vkd3d_protonfiles = [] wined3dfiles = ["d3d12", "d3d11", "d3d10", "d3d10core", "d3d10_1", "d3d9"] else: dxvkfiles = ["d3d11", "d3d10core", "d3d9"] vkd3d_protonfiles = ["d3d12", "d3d12core"] wined3dfiles = [] if use_dxvk_dxgi: dxvkfiles.append("dxgi") else: wined3dfiles.append("dxgi") if use_dxvk_d3d8: dxvkfiles.append("d3d8") else: wined3dfiles.append("d3d8") icufiles = ["icuin68", "icuuc68", "icudt68"] for f in wined3dfiles: try_copy(g_proton.default_pfx_dir + "drive_c/windows/system32/" + f + ".dll", "drive_c/windows/system32", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) try_copy(g_proton.default_pfx_dir + "drive_c/windows/syswow64/" + f + ".dll", "drive_c/windows/syswow64", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) for f in dxvkfiles: try_copy(g_proton.arch_pe_dir("wine/dxvk", False) + f + ".dll", "drive_c/windows/system32", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) try_copy(g_proton.arch_pe_dir("wine/dxvk", True) + f + ".dll", "drive_c/windows/syswow64", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) g_session.dlloverrides[f] = "n" for f in vkd3d_protonfiles: optional = False if f == "d3d12core": optional = True try_copy(g_proton.arch_pe_dir("wine/vkd3d-proton", False) + f + ".dll", "drive_c/windows/system32", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True, optional=optional) try_copy(g_proton.arch_pe_dir("wine/vkd3d-proton", True) + f + ".dll", "drive_c/windows/syswow64", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True, optional=optional) g_session.dlloverrides[f] = "n" for f in icufiles: dst = "drive_c/windows/system32/" + f + ".dll" if not file_exists(self.prefix_dir + dst, follow_symlinks=False): tracked_files.write(dst + '\n') self.create_symlink(self.prefix_dir + dst, g_proton.arch_pe_dir("wine/icu", False) + f + ".dll") dst = "drive_c/windows/syswow64/" + f + ".dll" if not file_exists(self.prefix_dir + dst, follow_symlinks=False): tracked_files.write(dst + '\n') self.create_symlink(self.prefix_dir + dst, g_proton.arch_pe_dir("wine/icu", True) + f + ".dll") allow_nvidia_libs = os.path.isdir(g_proton.lib_dir + "wine/nvidia-libs") disable_nvlibs32 = "nvidialibsno32" in g_session.compat_config or "wow64" in g_session.compat_config use_nvoptix = "nvoptix" in g_session.compat_config and allow_nvidia_libs use_nvml = ("nvml" in g_session.compat_config and allow_nvidia_libs) or use_nvoptix use_nvenc = "nvenc" in g_session.compat_config and allow_nvidia_libs use_nvcuda = ("nvcuda" in g_session.compat_config and allow_nvidia_libs) or use_nvenc or use_nvoptix nvapi_path = "nvapi/" # If the user requested the NVAPI be available, copy it into place. # If they didn't, clean up any stray nvapi DLLs. if use_nvapi: try_copy(g_proton.arch_pe_dir("wine/" + nvapi_path, False) + "nvapi64.dll", "drive_c/windows/system32", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) try_copy(g_proton.arch_pe_dir("wine/" + nvapi_path, False) + "nvofapi64.dll", "drive_c/windows/system32", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) try_copy(g_proton.arch_pe_dir("wine/" + nvapi_path, True) + "nvapi.dll", "drive_c/windows/syswow64", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) g_session.dlloverrides["nvapi64"] = "n" g_session.dlloverrides["nvofapi64"] = "n" g_session.dlloverrides["nvapi"] = "n" g_session.dlloverrides["nvcuda"] = "b" else: nvapi64_dll = self.prefix_dir + "drive_c/windows/system32/nvapi64.dll" nvapi32_dll = self.prefix_dir + "drive_c/windows/syswow64/nvapi.dll" if file_exists(nvapi64_dll, follow_symlinks=False): os.unlink(nvapi64_dll) if file_exists(nvapi64_dll + '.debug', follow_symlinks=False): os.unlink(nvapi64_dll + '.debug') if file_exists(nvapi32_dll, follow_symlinks=False): os.unlink(nvapi32_dll) if file_exists(nvapi32_dll + '.debug', follow_symlinks=False): os.unlink(nvapi32_dll + '.debug') if use_nvcuda and use_nvapi: try_copy(g_proton.arch_pe_dir("wine/nvidia-libs/nvcuda", False) + "nvcuda.dll.so", "drive_c/windows/system32/nvcuda.dll", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) g_session.dlloverrides["nvcuda"] = "n,b" else: nvcuda64_dll = self.prefix_dir + "drive_c/windows/system32/nvcuda.dll" if file_exists(nvcuda64_dll, follow_symlinks=False): os.unlink(nvcuda64_dll) try_copy(g_proton.default_pfx_dir + "drive_c/windows/system32/nvcuda.dll", "drive_c/windows/system32", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) if use_nvcuda and use_nvapi and not disable_nvlibs32: try_copy(g_proton.arch_pe_dir("wine/nvidia-libs/nvcuda", True) + "nvcuda.dll.so", "drive_c/windows/syswow64/nvcuda.dll", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) g_session.dlloverrides["nvcuda"] = "n,b" else: nvcuda32_dll = self.prefix_dir + "drive_c/windows/syswow64/nvcuda.dll" if file_exists(nvcuda32_dll, follow_symlinks=False): os.unlink(nvcuda32_dll) try_copy(g_proton.default_pfx_dir + "drive_c/windows/syswow64/nvcuda.dll", "drive_c/windows/syswow64", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) if use_nvenc and use_nvapi: try_copy(g_proton.arch_pe_dir("wine/nvidia-libs/nvenc", False) + "nvcuvid.dll.so", "drive_c/windows/system32/nvcuvid.dll", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) try_copy(g_proton.arch_pe_dir("wine/nvidia-libs/nvenc", False) + "nvencodeapi64.dll.so", "drive_c/windows/system32/nvencodeapi64.dll", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) g_session.dlloverrides["nvcuvid"] = "n,b" g_session.dlloverrides["nvencodeapi64"] = "n,b" else: nvcuvid64_dll = self.prefix_dir + "drive_c/windows/system32/nvcuvid.dll" nvencodeapi64_dll = self.prefix_dir + "drive_c/windows/system32/nvencodeapi64.dll" if file_exists(nvcuvid64_dll, follow_symlinks=False): os.unlink(nvcuvid64_dll) if file_exists(nvencodeapi64_dll, follow_symlinks=False): os.unlink(nvencodeapi64_dll) if use_nvenc and use_nvapi and not disable_nvlibs32: try_copy(g_proton.arch_pe_dir("wine/nvidia-libs/nvenc", True) + "nvcuvid.dll.so", "drive_c/windows/syswow64/nvcuvid.dll", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) try_copy(g_proton.arch_pe_dir("wine/nvidia-libs/nvenc", True) + "nvencodeapi.dll.so", "drive_c/windows/syswow64/nvencodeapi.dll", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) g_session.dlloverrides["nvcuvid"] = "n,b" g_session.dlloverrides["nvencodeapi"] = "n,b" else: nvcuvid32_dll = self.prefix_dir + "drive_c/windows/syswow64/nvcuvid.dll" nvencodeapi32_dll = self.prefix_dir + "drive_c/windows/syswow64/nvencodeapi.dll" if file_exists(nvcuvid32_dll, follow_symlinks=False): os.unlink(nvcuvid32_dll) if file_exists(nvencodeapi32_dll, follow_symlinks=False): os.unlink(nvencodeapi32_dll) if use_nvml and use_nvapi: prepend_to_env_str(g_session.env, "WINEDLLPATH", g_proton.lib_dir + "wine/nvidia-libs/nvml/wine", ":") else: nvml64_dll = self.prefix_dir + "drive_c/windows/system32/nvml.dll" nvml32_dll = self.prefix_dir + "drive_c/windows/syswow64/nvml.dll" if file_exists(nvml64_dll, follow_symlinks=False): os.unlink(nvml64_dll) if file_exists(nvml32_dll, follow_symlinks=False): os.unlink(nvml32_dll) if use_nvoptix and use_nvapi: try_copy(g_proton.arch_pe_dir("wine/nvidia-libs/nvoptix", False) + "nvoptix.dll.so", "drive_c/windows/system32/nvoptix.dll", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) g_session.dlloverrides["nvoptix"] = "n,b" else: nvoptix64_dll = self.prefix_dir + "drive_c/windows/system32/nvoptix.dll" nvoptix32_dll = self.prefix_dir + "drive_c/windows/syswow64/nvoptix.dll" if file_exists(nvoptix64_dll, follow_symlinks=False): os.unlink(nvoptix64_dll) if file_exists(nvoptix32_dll, follow_symlinks=False): os.unlink(nvoptix32_dll) if os.path.exists(g_proton.lib_dir + "wine/discord-rpc-bridge/bridge.exe"): makedirs(self.prefix_dir + "drive_c/windows/system32/discord/") try_copy(g_proton.lib_dir + "wine/discord-rpc-bridge/bridge.exe", "drive_c/windows/system32/discord/bridge.exe", prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) # Try to detect known DLLs that ship with the NVIDIA Linux Driver # and add them into the prefix if g_session.nvidia_wine_dll_dir: for dll in ["_nvngx.dll", "nvngx.dll"]: try_copy(g_session.nvidia_wine_dll_dir + "/" + dll, "drive_c/windows/system32", optional=True, prefix=self.prefix_dir, track_file=tracked_files, link_debug=True) setup_game_dir_drive() setup_steam_dir_drive() setup_openvr_paths() # valve ffmpeg configuration below is disabled for proton-ge, we ship our own ffmpeg build. # add Steam ffmpeg libraries to path #prepend_to_env_str(g_session.env, ld_path_var, steamdir + "/ubuntu12_64/video/:" + steamdir + "/ubuntu12_32/video/:" + steamdir + "/steamrtarm64/video/", ":") def comma_escaped(s): escaped = False idx = -1 while s[idx] == '\\': escaped = not escaped idx = idx - 1 return escaped additional_config = utilities.proton_add_config() #hopefully short-lived, app-specific workarounds for Proton bugs def default_compat_config(): ret = set() ret.update(additional_config) if "SteamAppId" in os.environ: appid = os.environ["SteamAppId"] if appid in [ #affected by CW bug 19741 "1017900", #Age of Empires: Definitive Edition #affected by CW bug 20240 "1331440", #FUSER #affected by Unity race "2620730", #DEVIATOR - CW bug 24913 "2882920", #SSR Wives: The Murder Of My Winter Crush Demo - CW bug 25730 "2712910", #Spark & Kling - CW bug 25778 ]: ret.add("nomfdxgiman") if appid in [ # OPWR may be causing text input delays in login windows in these games on XWayland due to # blit happening before presentation "1172620", #Sea of Thieves "962130", #Grounded "495420", #State of Decay 2: Juggernaut Edition "976730", #Halo: The Master Chief Collection "1017900", #Age of Empires: Definitive Edition "1056090", #Ori and the Will of the Wisps "1293830", #Forza Horizon 4 "1551360", #Forza Horizon 5 "813780", #Age of Empires II: Definitive Edition "933110", #Age of Empires III: Definitive Edition "1466860", #Age of Empires IV "1097840", #Gears 5 "1244950", #Battletoads "1189800", #Bleeding Edge "1184050", #Gears Tactics "1240440", #Halo Infinite "1250410", #Microsoft Flight Simulator "1672970", #Minecraft Dungeons "1180660", #Tell Me Why "1238430", #Tell Me Why Chapter 2 "1266670", #Tell Me Why Chapter 3 # Other issues arising from OWPR code path in apps, e. g., hitting unimplemented bits in # d3dcompiler. "230410", #Warframe "3513350", #Wuthering Waves "3728370", ]: ret.add("noopwr") if appid in [ "2710", #Act of War: Direct Action "1621680", #Sword and Fairy 4 "888040", #Metal Fatigue ]: ret.add("noforcelgadd") if appid in [ "257420", #Serious Sam 4 "2021880", #Ara: History Untold ]: ret.add("hidevggpu") if appid in [ "1977170", #Jusant ]: ret.add("hideintelgpu") if appid in [ "202990", #Call of Duty: Black Ops II - Multiplayer "212910", #Call of Duty: Black Ops II - Zombies "499100", #Dark Parables: The Exiled Prince Collector's Edition (499100) "1404090", #Trivia Tricks "2052410", #WITCH ON THE HOLY NIGHT "789910", #Planet of the Apes: Last Frontier "1183470", #Imperiums: Greek Wars "876340", #VR Slots 3D ]: ret.add("heapdelayfree") if appid in [ "21980", #Call of Juarez: Bound in Blood "553850", #Helldivers 2 "2055290", #Sonic Colors: Ultimate ]: ret.add("heapzeromemory") if appid in [ "71230", #Crazy Taxi "3328910", #MySims Kingdom ]: ret.add("heaptopdown") if appid in [ "2630", #Call of Duty 2 "1060210", #Disaster Report 4: Summer Memories "414740", #RAID: World War II "201510", #Flatout 3 "1233880", #Disgaea 4 Complete+ ]: ret.add("nofsync") ret.add("noesync") if appid in [ "1237970", #Titanfall 2 ]: for idx, arg in enumerate(sys.argv): if '-northstar' in arg: ret.add("northstar") if appid in [ # disable dxvknvapi for titles which dislike it "1088850", #Marvel's Guardians of the Galaxy "1418100", #Swords of Legends Online "2080180", #Go Home Annie Demo "1939100", #Go Home Annie "435150", #Divinity: Original Sin 2 - Definitive Edition "2176900", #Fablecraft "2853730", #Skull and Bones "1761380", #Otherworld Legends (just to disable nvcuda.dll) ]: ret.add("disablenvapi") if appid in [ "1808500", #ARC Raiders "2073850", #The Finals "108710", #Alan Wake "202750", #Alan Wake's American Nightmare "505170", #Carmageddon: Max Damage "255220", #GRID Autosport "44350", #GRID 2 "407810", #Hard Reset Redux "233130", #Shadow Warrior "2067160", #Simulakros "2621010", #Simulakros Demo "368500", #Assassin's Creed Syndicate "1524630", #KeepUp Survival "1233570", ]: try: with open('/proc/modules') as f: drivers = set([line.partition(' ')[0] for line in f.read().splitlines()]) if not drivers.intersection({'nvidia', 'nouveau', 'nova'}): ret.add("disablenvapi") except OSError: ret.add("disablenvapi") if appid in [ "2698940", #The Crew Motorfest "2079120", #Warudo ]: ret.add("hidenvgpu") if appid in [ "2395210", #Tony Hawk's Pro Skater 1 + 2 "1577120", #The Quarry ]: ret.add("forcenvapi") if appid in [ "1252330" #Deathloop ]: ret.add("hideapu") if appid in [ "249610", # Galactic Arms Race "287240", # Eterium Demo "280200", # Eterium "312530", # Duck Game "448230", # AsteroidsHD "1072860", # Real Scary "512490", # Zombie Estate 2 ]: ret.add("fnad3d11") #options to also be enabled for prerequisite setup steps ret.add("gamedrive") if "STEAM_COMPAT_APP_ID" in os.environ: appid = os.environ["STEAM_COMPAT_APP_ID"] if appid in [ "247660", #Deadly Premonition: The Director's Cut "1026680", #FINAL FANTASY VIII - REMASTERED "3513350", #Wuthering Waves "3837340", #FINAL FANTASY VII "337000", #Deus Ex: Mankind Divided ]: ret.add("noxalia") if appid in [ "275850", #No Man's Sky "2012840", #Portal with RTX ]: ret.add("nohardwarescheduling") if "STORE" in os.environ: store = os.environ["STORE"] if store in [ "battlenet", "ea", "ubisoft", ]: ret.add("writecopy") # hack to force gamedrive compat_config always for umu if os.environ.get('UMU_ID', ''): ret.add("gamedrive") return ret default_cpu_limit = { "19900" : "16", # Far Cry 2 "298110" : "16", # Far Cry 4 "20920" : "16", # The Witcher 2: Assassins of Kings Enchanced Edition "35130" : "16", # Lara Croft and the Guardian of Light "55150" : "16", # Warhammer 40,000: Space Marine "204450" : "16", # Call of Juarez: Gunslinger "15620" : "8", # Warhammer 40,000: Dawn of War II "20570" : "8", # Warhammer 40,000: Dawn of War II - Chaos Rising "56400" : "8", # Warhammer 40,000: Dawn of War II - Retribution "618970" : "4", # Outcast - Second Contact "10150" : "8", # Prototype "2229830" : "1", # Command & Conquer and The Covert Operations "259170" : "8", # Alone in the Dark (2008) "11440" : "4", # DiRT "316260" : "16", # Disney Universe "286810" : "30", # Hard Truck Apocalypse: Rise of Clans / Ex Machina: Meridian 113 "70000" : "28", # Dino D-Day "115320" : "8", # Prototype 2 "65540" : "16", # Gothic 1 Classic "1038250" : "P", # DIRT 5 } fex_application_profiles = { "292030" : { # The Witcher 3: Wild Hunt "setup*": { "X87ReducedPrecision": "0" } } } def get_current_cpu_affinity(): try: affinity = os.sched_getaffinity(0) if len(affinity) >= os.cpu_count(): return "" topo_str = ",".join(map(str, list(affinity))) return f"{len(affinity)}:{topo_str}" except: return "" class Session: def __init__(self): self.log_file = None self.env = dict(os.environ) self.xrandr_bin = g_proton.xrandr_bin self.dlloverrides = { "steam.exe": "b", #always use our special built-in steam.exe "dotnetfx35.exe": "b", #replace the broken installer, as does Windows "dotnetfx35setup.exe": "b", "beclient.dll": "b,n", "beclient_x64.dll": "b,n", "winebth.sys": "d", #disable winebth.sys as it crashes winedevice.exe } # CW Bug 21737. Locoland executable happens to be steam.exe. if os.environ.get("SteamGameId", 0) == "352130": del self.dlloverrides["steam.exe"] if os.environ.get("SteamGameId", 0) not in ["2767030", "2274200"]: self.dlloverrides["opencl"] = "n,d" self.compat_config = default_compat_config() self.cmdlineappend = [] if "STEAM_COMPAT_CONFIG" in os.environ: config = os.environ["STEAM_COMPAT_CONFIG"] while config: (cur, _, config) = config.partition(',') if cur.startswith("cmdlineappend:"): while comma_escaped(cur): (a, _, c) = config.partition(',') cur = cur[:-1] + ',' + a config = c self.cmdlineappend.append(cur[14:].replace('\\\\','\\')) else: self.compat_config.add(cur) #turn forcelgadd on by default unless it is disabled in compat config if "noforcelgadd" not in self.compat_config: self.compat_config.add("forcelgadd") appid = os.environ.get("SteamGameId", 0) if not appid: umu_id = os.environ.get("UMU_ID", "") if umu_id.startswith("umu-") and umu_id[4:].isdigit(): appid = umu_id[4:] # These games "support" PS controller input but only provide Xbox icons # It's better to remap PS hidraw to xinput to mimic xbox on these games. # If we use native PS mappings that the game detects, the mappings are wrong. # When forcing xinput and remapping internally, mappings are correct # Even then, AC II still needs manual remapping of axby in-game. The rest are ok if appid in [ "15100", # Assassin's Creed "33230", # Assassin's Creed II "260210", # Assassin's Creed: Liberation HD "368500", # Assassin's Creed Syndicate "201870", # Assassin's Creed: Revelations "277590", # Assassin's Creed Freedom Cry "311560", # Assassin's Creed Rogue "289650", # Assassin's Creed Unity "242050", # Assassin's Creed IV: Black Flag "354380", # Assassin's Creed Chronicles: China "359610", # Assassin's Creed Chronicles: India "359600", # Assassin's Creed Chronicles: Russia "582660", # Black Desert Online "814380", # Sekiro "287290", # Resident Evil Revelations 2 "1364780", # Street Fighter 6 "1446780", # Monster Hunter Rise "345350", # FINAL FANTASY XIII: Lightning Returns ]: self.env["PROTON_SONY_HIDRAW_XINPUT"] = "1" # These games have perfect DS4 mappings, but do not support DS5/DS5 Edge. # In this case we fake a DS4 controller for perfect button mappings and icons if appid in [ "911400", # Assassin's Creed III / Liberation Remastered "812140", # Assassin's Creed Odyssey "582160", # Assassin's Creed Origins "1222140", # Detroit: Become Human "1174180", # Red Dead Redemption 2 "1190460", # Death Stranding "1593500", # God of War (2018) "1151640", # Horizon Zero Dawn Complete Edition "1172710", # Dune Awakening "1034860", # Grandia "330390", # Grandia II "3255380", # Lunar "731490", # Crash Bandicoot N'Sane Trilogy "253230", # A Hat in Time "638970", # Yakuza 0 "612880", # Wolfenstein II: The New Colossus # Batman Arkham Knight turns out to ship with a flawed libScePad_x64.dll # that prevents both the DS4 v1 and v2 from working. Replacing it with # one from another game fixed the issue and let both DS4 models work # properly. Because this means the game needs to be modded to support # the DS4 properly, it's debatable if this entry should make it into the # final commit. "208650", # Batman: Arkham Knight # The enhanced edition of Little nightmares has full support for the # DualSense. This entry is here in case anyone prefers the original and # still wants to use the DualSense. "424840", # Little Nightmares (Original) "996580", # Spyro Reignited Trilogy # Recommended by rjbs91. I don't have Greedfall, so I would recommend # testing it's support; whether it supports the DS4 v2 or only the v1. # rjbs91 claims it has proper button mappings and glyphs. "606880", # Greedfall "570940", # Dark Souls Remastered ]: self.env["PROTON_SONY_DUALSENSE_AS_DUALSHOCK4"] = "1" # Some games only support DS4 v1 if appid in [ "731490", # Crash Bandicoot N'Sane Trilogy "638970", # Yakuza 0 "612880", # Wolfenstein II: The New Colossus "1034860", # Grandia "330390", # Grandia II "3255380", # Lunar ]: self.env["PROTON_SONY_DUALSHOCK4_V2_AS_V1"] = "1" # These games rely on Steam Input action manifests. Expose XInput through # lsteamclient's standalone Steam Input implementation. The fallback detects # the current controller layout itself and updates it across hotplug events. if appid in [ "2246340", # Monster Hunter Wilds "1151640", # Horizon Zero Dawn Complete Edition "2322010", # God of War Ragnarok ]: self.env["PROTON_SONY_HIDRAW_XINPUT"] = "1" self.env["PROTON_STEAMINPUT_FALLBACK"] = "1" # Work around quirk caused by our Dynamic exe relocation patch if appid in [ "201870", # Assassin's Creed: Revelations ]: self.env["WINE_DISABLE_EXE_ASLR"] = "1" # Work around MH Wilds audio device quirk if appid in [ "2246340", # Monster Hunter Wilds ]: self.env["PROTON_ENABLE_MHWILDS_USB_AUDIO"] = "1" # Initialize the DualSense USB audio system, preserve its internal-speaker # route, and select the controller through Death Stranding's native BB # output setting while keeping the four-channel haptic stream active. if appid in [ "1850570", # Death Stranding Director's Cut "3280350", # Death Stranding 2 ]: self.env["PROTON_DEATH_STRANDING_CONTROLLER_EFFECTS"] = "1" # Director's Cut does not expose the PS5 environmental-speaker option. # Death Stranding 2 has a native in-game setting and must retain it. if appid == "1850570": self.env["PROTON_DEATH_STRANDING_FORCE_ENVIRONMENT_EFFECTS"] = "1" if "PROTON_PIPEWIRE_ALSA_PLUGIN" not in self.env: if g_proton.host_pe_arch == "aarch64-windows": alsa_arch = "aarch64-linux-gnu" else: alsa_arch = "x86_64-linux-gnu" self.env["PROTON_PIPEWIRE_ALSA_PLUGIN"] = os.path.join( g_proton.lib_dir, alsa_arch, "alsa-lib", "libasound_module_pcm_pipewire.so") if "PROTON_CPU_TOPOLOGY" in self.env: self.env["WINE_CPU_TOPOLOGY"] = self.env["PROTON_CPU_TOPOLOGY"] elif appid in default_cpu_limit: self.env["WINE_CPU_TOPOLOGY"] = default_cpu_limit[appid] elif "WINE_CPU_TOPOLOGY" not in self.env and platform.machine() == 'aarch64': topo_str = get_current_cpu_affinity() if topo_str != "": self.env["WINE_CPU_TOPOLOGY"] = topo_str if "WINE_HIDE_AMD_GPU" not in self.env and appid in [ "1282690", ]: self.env["WINE_HIDE_AMD_GPU"] = "1" if "WINE_DISABLE_GAMESCOPE_MAX_SIZE_HACK" not in self.env and appid in [ "3754990", "3495730" ]: self.env["WINE_DISABLE_GAMESCOPE_MAX_SIZE_HACK"] = "1" def init_wine(self): if "HOST_LC_ALL" in self.env and len(self.env["HOST_LC_ALL"]) > 0: #steam sets LC_ALL=C to help some games, but Wine requires the real value #in order to do path conversion between win32 and host. steam sets #HOST_LC_ALL to allow us to use the real value. self.env["LC_ALL"] = self.env["HOST_LC_ALL"] else: self.env.pop("LC_ALL", "") # FIXME: temporary workaround to libxkbcommon's current integration if "XLOCALEDIR" not in self.env: self.env["XLOCALEDIR"] = g_proton.dist_dir + "share/X11/locale" # CW-Bug-Id: #23185 Enable the new SDL 2.30 Steam Input integration. if "SteamVirtualGamepadInfo_Proton" in self.env and "SteamVirtualGamepadInfo" not in self.env: self.env["SteamVirtualGamepadInfo"] = self.env["SteamVirtualGamepadInfo_Proton"] if "PROTON_USE_WOW64" not in self.env and "SteamGameId" in self.env: if self.env["SteamGameId"] in [ "747920", # Hero Plus ]: self.env["PROTON_USE_WOW64"] = "1" self.env.pop("WINEARCH", "") if "PROTON_USE_WOW64" in self.env and self.env["PROTON_USE_WOW64"] == "1" and platform.machine() == "x86_64": self.env["WINEARCH"] = "wow64" if 'ORIG_'+ld_path_var not in os.environ: # Allow wine to restore this when calling an external app. self.env['ORIG_'+ld_path_var] = os.environ.get(ld_path_var, '') if g_proton.host_pe_arch == "aarch64-windows": ld_library_path = [ g_proton.lib_dir + "aarch64-linux-gnu", g_proton.lib_dir + "x86_64-linux-gnu", g_proton.lib_dir + "i386-linux-gnu", ] else: ld_library_path = [ g_proton.lib_dir + "x86_64-linux-gnu", g_proton.lib_dir + "i386-linux-gnu", ] prepend_to_env_str(self.env, ld_path_var, ':'.join(ld_library_path), ":") dllpaths = [g_proton.lib_dir + "vkd3d", g_proton.lib_dir + "wine"] if "WINEDLLPATH" in os.environ: dllpaths.append(os.environ["WINEDLLPATH"]) self.env["WINEDLLPATH"] = ':'.join(dllpaths) if "STEAM_COMPAT_MEDIA_PATH" in os.environ: old_audiofoz_path = os.environ["STEAM_COMPAT_MEDIA_PATH"] + "/audio.foz" if file_exists(old_audiofoz_path, follow_symlinks=False): os.remove(old_audiofoz_path) self.env["MEDIACONV_AUDIO_DUMP_FILE"] = os.environ["STEAM_COMPAT_MEDIA_PATH"] + "/audiov2.foz" self.env["MEDIACONV_VIDEO_DUMP_FILE"] = os.environ["STEAM_COMPAT_MEDIA_PATH"] + "/video.foz" if "STEAM_COMPAT_TRANSCODED_MEDIA_PATH" in os.environ: self.env["MEDIACONV_AUDIO_TRANSCODED_FILE"] = os.environ["STEAM_COMPAT_TRANSCODED_MEDIA_PATH"] + "/transcoded_audio.foz" self.env["MEDIACONV_VIDEO_TRANSCODED_FILE"] = os.environ["STEAM_COMPAT_TRANSCODED_MEDIA_PATH"] + "/transcoded_video.foz" if os.environ.get("PROTON_MEDIACONV_NO_VIDEO", "1" if os.environ.get("SteamGameId", 0) in ( "283640", # Salt and Sanctuary "455490", # Don't Die Dateless, Dummy! "787810", # Rogue Heroes: Ruins of Tasos "1199570", # Rogue Heroes: Ruins of Tasos Demo "1491460", # Tor Eternum "1588990", # Tor Eternum Demo "1895130", # Darza's Dominion ) else "0") != "0": self.env["MEDIACONV_BLANK_VIDEO_FILE"] = g_proton.media_dir + "blank.mka" else: self.env["MEDIACONV_BLANK_VIDEO_FILE"] = g_proton.media_dir + "blank.mkv" self.env["MEDIACONV_BLANK_AUDIO_FILE"] = g_proton.media_dir + "blank.ptna" self.env["ESPEAK_DATA_PATH"] = g_proton.dist_dir + "share" prepend_to_env_str(self.env, "PATH", g_proton.bin_dir, ":") def check_environment(self, env_name, config_name): if env_name not in self.env: return False if nonzero(self.env[env_name]): self.compat_config.add(config_name) else: self.compat_config.discard(config_name) return True def try_log_slr_versions(self): try: if "PRESSURE_VESSEL_RUNTIME_BASE" in self.env: with open(self.env["PRESSURE_VESSEL_RUNTIME_BASE"] + "/VERSIONS.txt", "r") as f: for line in f: line = line.strip() if len(line) > 0 and not line.startswith("#"): cleaned = line.split("#")[0].strip().replace("\t", " ") split = cleaned.split(" ", maxsplit=1) self.log_file.write(split[0] + ": " + split[1] + "\n") except (OSError, IOError, TypeError, KeyError): pass def setup_logging(self, *, append_forever): basedir = self.env.get("PROTON_LOG_DIR", os.environ["HOME"]) if append_forever: #SteamGameId is not always available lfile_path = basedir + "/steam-proton.log" else: if "SteamGameId" not in os.environ: return False lfile_path = basedir + "/steam-" + os.environ["SteamGameId"] + ".log" if file_exists(lfile_path, follow_symlinks=False): os.remove(lfile_path) makedirs(basedir) self.log_file = open(lfile_path, "a") return True def log_enabled_for(self, target: str, default_value: bool = False) -> bool: log_list = self.env["PROTON_LOG"] if nonzero(log_list): for entry in log_list.split(','): if entry[1:] == target: if entry.startswith('+'): return True elif entry.startswith('-'): return False break return default_value # logging enabled, return specified default when not found return False # logging disabled, all false def generate_fex_app_config(self): """ Translate some environment variables into FEX config which is in files. """ app_config = { "Config": {}, "ThunksDB": {} } # appinfo based environment variable as default try: app_config["Config"]["TSOEnabled"] = self.env['STEAM_FEX_TSOENABLED'] except (KeyError, ValueError): pass try: app_config["Config"]["Multiblock"] = self.env['STEAM_FEX_MULTIBLOCK'] except (KeyError, ValueError): pass # if user has specified an override, take those values try: config_env = self.env['STEAM_COMPAT_FEX_CONFIG'] if config_env: app_config["Config"]["TSOEnabled"] = "1" if ("TSOEnabled:1" in config_env) else "0" app_config["Config"]["Multiblock"] = "1" if ("Multiblock:1" in config_env) else "0" except KeyError: pass if "PROTON_LOG" in self.env: app_config["Config"]["SilentLog"] = "0" if self.log_enabled_for("fex", True) else "1" if "SteamGameId" in os.environ and os.environ["SteamGameId"] in fex_application_profiles: try: app_config["AppOverrides"] = fex_application_profiles[os.environ["SteamGameId"]] except (KeyError, ValueError): pass return app_config def get_primary_monitor(self): try: result = subprocess.run( [self.xrandr_bin], check=True, capture_output=True, text=True, ) except FileNotFoundError: print("Error: xrandr is not installed or not in PATH.", file=sys.stderr) return None except subprocess.CalledProcessError as e: print(f"Error: failed to run xrandr: {e}", file=sys.stderr) return None for line in result.stdout.splitlines(): # Example: # HDMI-A-1 connected primary 3840x2160+3840+0 ... match = re.match(r"^(\S+)\s+connected\s+primary\b", line) if match: return match.group(1) return None def init_session(self, update_prefix_files): self.env["WINEPREFIX"] = g_compatdata.prefix_dir #load environment overrides used_user_settings = {} if file_exists(g_proton.user_settings_file, follow_symlinks=True): try: import user_settings # pyright: ignore [reportMissingImports] for key, value in user_settings.user_settings.items(): if key not in self.env: self.env[key] = value used_user_settings[key] = value except Exception: log("************************************************") log("THERE IS AN ERROR IN YOUR user_settings.py FILE:") log("%s" % sys.exc_info()[1]) log("************************************************") if "PROTON_LOG" in self.env and nonzero(self.env["PROTON_LOG"]): self.env.setdefault("WINEDEBUG", "+timestamp,+pid,+tid,+seh,+unwind,+threadname,+debugstr,+loaddll,+mscoree") self.env.setdefault("DXVK_LOG_LEVEL", "info") self.env.setdefault("DXVK_NVAPI_LOG_LEVEL", "info") self.env.setdefault("VKD3D_DEBUG", "warn") self.env.setdefault("VKD3D_SHADER_DEBUG", "fixme") self.env.setdefault("WINE_MONO_TRACE", "E:System.NotImplementedException") if self.env["PROTON_LOG"] != "1": append_to_env_str(self.env, "WINEDEBUG", self.env["PROTON_LOG"], ",") #for performance, logging is disabled by default; override with user_settings.py self.env.setdefault("WINEDEBUG", "-all") self.env.setdefault("DXVK_LOG_LEVEL", "none") self.env.setdefault("VKD3D_DEBUG", "none") self.env.setdefault("VKD3D_SHADER_DEBUG", "none") # Enable wine-nvml by default on Nvidia (experimental) if utilities.is_driver_loaded({'nvidia'}): self.compat_config.add("nvml") if "wined3d11" in self.compat_config: self.compat_config.add("wined3d") self.check_environment("PROTON_USE_WOW64", "wow64") if not self.check_environment("PROTON_USE_WINED3D", "wined3d"): self.check_environment("PROTON_USE_WINED3D11", "wined3d") self.check_environment("PROTON_NO_WM_DECORATION", "nowmdecoration") self.check_environment("PROTON_DXVK_D3D8", "dxvkd3d8") self.check_environment("PROTON_NO_D3D11", "nod3d11") self.check_environment("PROTON_NO_D3D10", "nod3d10") self.check_environment("PROTON_NO_ESYNC", "noesync") self.check_environment("PROTON_NO_FSYNC", "nofsync") self.check_environment("PROTON_FORCE_LARGE_ADDRESS_AWARE", "forcelgadd") self.check_environment("PROTON_OLD_GL_STRING", "oldglstr") self.check_environment("PROTON_HIDE_NVIDIA_GPU", "hidenvgpu") self.check_environment("PROTON_HIDE_VANGOGH_GPU", "hidevggpu") self.check_environment("PROTON_HIDE_INTEL_GPU", "hideintelgpu") self.check_environment("PROTON_SET_GAME_DRIVE", "gamedrive") self.check_environment("PROTON_SET_STEAM_DRIVE", "steamdrive") self.check_environment("PROTON_NO_XIM", "noxim") self.check_environment("PROTON_HEAP_DELAY_FREE", "heapdelayfree") self.check_environment("PROTON_HEAP_ZERO_MEMORY", "heapzeromemory") self.check_environment("PROTON_DISABLE_NVAPI", "disablenvapi") self.check_environment("PROTON_FORCE_NVAPI", "forcenvapi") self.check_environment("PROTON_NVIDIA_LIBS", "nvidialibs") self.check_environment("PROTON_NVIDIA_LIBS_NO_32BIT", "nvidialibsno32") self.check_environment("PROTON_NVIDIA_NVCUDA", "nvcuda") self.check_environment("PROTON_NVIDIA_NVENC", "nvenc") self.check_environment("PROTON_NVIDIA_NVML", "nvml") self.check_environment("PROTON_NVIDIA_NVOPTIX", "nvoptix") self.check_environment("PROTON_HIDE_APU", "hideapu") self.check_environment("PROTON_ENABLE_WAYLAND", "wayland") self.check_environment("PROTON_USE_WAYLAND", "wayland") self.check_environment("PROTON_PREFER_SDL", "sdlinput") self.check_environment("PROTON_USE_SDL", "sdlinput") # Sony controllers need hidraw to feed the XInput-backed Steam Input # fallback. Native Xbox controllers continue to use their XInput slot. if (self.env.get("PROTON_STEAMINPUT_FALLBACK") == "1" or self.env.get("PROTON_STEAMINPUT_XINPUT_FALLBACK") == "1"): self.compat_config.discard("sdlinput") self.env.pop("PROTON_DISABLE_HIDRAW", None) self.check_environment("PROTON_NATIVE_AGS", "nativeags") self.check_environment("PROTON_FSR3_UPGRADE", "fsr3") self.check_environment("PROTON_FSR4_UPGRADE", "fsr4") self.check_environment("PROTON_FSR4_RDNA3_UPGRADE", "fsr4rdna3") self.check_environment("PROTON_FSR4_INDICATOR", "fsr4hud") self.check_environment("PROTON_DLSS_UPGRADE", "dlss") self.check_environment("PROTON_DLSS_INDICATOR", "dlsshud") self.check_environment("PROTON_XESS_UPGRADE", "xess") self.check_environment("PROTON_USE_OPTISCALER", "optiscaler") self.check_environment("PROTON_NATIVE_AGS", "nativeags") self.check_environment("PROTON_ENABLE_HDR", "hdr") self.check_environment("PROTON_USE_HDR", "hdr") self.check_environment("PROTON_NO_NTSYNC", "nontsync") self.check_environment("PROTON_USE_WRITECOPY", "writecopy") if "nontsync" in self.compat_config: self.env.pop("WINENTSYNC", "") else: self.env["WINENTSYNC"] = "1" if "noesync" in self.compat_config: self.env.pop("WINEESYNC", "") self.env["WINEESYNC"] = "0" else: self.env["WINEESYNC"] = "1" if "nowmdecoration" in self.compat_config: self.env["WAYLANDDRV_SSD"] = "0" self.env["WINE_NO_WM_DECORATION"] = "1" if "nvidialibs" in self.compat_config: self.compat_config.add("nvcuda") self.compat_config.add("nvenc") self.compat_config.add("nvml") self.compat_config.add("nvoptix") self.check_environment("DXVK_NVAPI_VKREFLEX", "vkreflex") if "vkreflex" in self.compat_config and "disablenvapi" not in self.compat_config and utilities.is_driver_loaded({'nvidia'}): append_to_env_str(self.env, "VK_IMPLICIT_LAYER_PATH", g_proton.dist_dir + "share/dxvk-nvapi-vkreflex-layer/implicit_layer.d", ":") self.check_environment("LOW_LATENCY_LAYER", "lowlatencylayer") if "lowlatencylayer" in self.compat_config: append_to_env_str(self.env, "VK_IMPLICIT_LAYER_PATH", g_proton.dist_dir + "share/low_latency_layer/implicit_layer.d", ":") self.check_environment("ENABLE_VKBASALT", "vkbasalt") if "vkbasalt" in self.compat_config: append_to_env_str(self.env, "VK_IMPLICIT_LAYER_PATH", g_proton.dist_dir + "share/vkbasalt/implicit_layer.d", ":") self.check_environment("PYROVEIL", "pyroveil") if "pyroveil" in self.compat_config: append_to_env_str(self.env, "VK_IMPLICIT_LAYER_PATH", g_proton.dist_dir + "share/pyroveil/implicit_layer.d", ":") pyroveil_config = self.env.get("PYROVEIL_CONFIG", "") if pyroveil_config: self.env["PYROVEIL_CONFIG"] = pyroveil_config if os.path.isabs(pyroveil_config) else g_proton.dist_dir + "share/pyroveil/hacks/" + pyroveil_config if "nofsync" in self.compat_config: self.env.pop("WINEFSYNC", "") self.env["WINEFSYNC"] = "0" else: self.env["WINEFSYNC"] = "1" if "oldglstr" in self.compat_config: #mesa override self.env["MESA_EXTENSION_MAX_YEAR"] = "2003" #nvidia override self.env["__GL_ExtensionStringVersion"] = "17700" if os.environ.get("SteamGameId", 0) in [ "661920", #Claybook "558260", #Gravel "386360", #SMITE "487720", #Agony "879420", #Agony UNRATED "649950", #Ashen "1150080", #Azur Lane: Crosswave "463150", #BARRIER X "420290", #Blackwake "1092660", #Blair Witch "1052070", #Burning Daylight "399810", #Call of Cthulhu "967390", #Chronos: Before the Ashes "968870", #Close to the Sun "544920", #Darwin Project "1055610", #Deep Space Battle Simulator "682990", #Drug Dealer Simulator "551770", #ECHO "392110", #ENDLESS Space 2 "447020", #Farming Simulator 17 "834280", #Fishing Sim World: Pro Tour "890880", #FROSTBITE: Deadly Climate "433530", #Heliborne - Enhanced Edition "521890", #Hello Neighbor "783170", #INSOMNIA: The Ark "1195460", #Last Year "1029890", #Layers of Fear 2 (2019) "535850", #Micro Machines World Series "711750", #Monster Energy Supercross - The Official Videogame "882020", #Monster Energy Supercross - The Official Videogame 2 "824280", #Monster Jam Steel Titans "775900", #MotoGP18 "415200", #Motorsport Manager "748360", #MY HERO ONE'S JUSTICE "1058450", #MY HERO ONE'S JUSTICE 2 "556180", #Mysterium: A Psychic Clue Game "561600", #MXGP3 - The Official Motocross Videogame "798290", #MXGP PRO "366870", #Narcosis "485030", #PLANET ALPHA "469610", #Rick and Morty: Virtual Rick-ality "759740", #RIDE 3 "1342260", #SAMURAI SHODOWN "425670", #Seraph "1096570", #SONG OF HORROR COMPLETE EDITION "492230", #Space Hulk: Tactics "996580", #Spyro Reignited Trilogy "437630", #State of Mind "442780", #STRAFE: Gold Edition "433100", #The Town of Light "406970", #The Uncertain: Last Quiet Day "451520", #theBlu: Season 1 "1237970", #TitanfallĀ® 2 "1051200", #Trover Saves the Universe "285190", #Warhammer 40,000: Dawn of War III "1133320", #Westworld Awakening ]: self.env["OPENSSL_ia32cap"] = "~0x20000000" if os.environ.get("SteamGameId", 0) in [ "500810", #Arcanum "4249100", #Resident Evil (1996) "4249110", #Resident Evil 2 (1998) "4249130", #Dino Crisis "4249150", #Breath of Fire IV ]: self.dlloverrides["ddraw"] = "n,b" # CW Bug 26050 if os.environ.get("SteamGameId", 0) == "3780660": self.dlloverrides["dinput"] = "n,b" # CW Bug 26133 if os.environ.get("SteamGameId", 0) == "2471120": self.dlloverrides["winmm"] = "n,b" # CW Bug 26376 if os.environ.get("SteamGameId", 0) == "1928420": self.dlloverrides["gameinput"] = "d" if "PROTON_LIMIT_RESOLUTIONS" not in self.env: if os.environ.get("SteamGameId", 0) in [ "39540", #SpellForce: Platinum Edition ]: self.env["PROTON_LIMIT_RESOLUTIONS"] = "16" elif os.environ.get("SteamGameId", 0) in [ "524220", #NieR: Automata "814380", #Sekiro: Shadows Die Twice "374320", #DARK SOULS III "357190", #Ultimate Marvel vs Capcom 3 "730830", #Escape from Monkey Island "229480", #Dungeons & Dragons: Chronicles of Mystara ]: self.env["PROTON_LIMIT_RESOLUTIONS"] = "32" if "PROTON_SPOOF_STEAMINPUT_VIDPID" not in self.env: if os.environ.get("SteamGameId", 0) in [ "939960", #Far Cry New Dawn "250900", #The Binding of Isaac: Rebirth "297860", #Split/Second ]: self.env["PROTON_SPOOF_STEAMINPUT_VIDPID"] = "1" if "forcelgadd" in self.compat_config: self.env["WINE_LARGE_ADDRESS_AWARE"] = "1" else: if "noforcelgadd" in self.compat_config: self.env["WINE_LARGE_ADDRESS_AWARE"] = "0" if "heapdelayfree" in self.compat_config: self.env["WINE_HEAP_DELAY_FREE"] = "1" if "heapzeromemory" in self.compat_config: self.env["WINE_HEAP_ZERO_MEMORY"] = "1" if "heaptopdown" in self.compat_config: self.env["WINE_HEAP_TOP_DOWN"] = "1" if "vkd3dbindlesstb" in self.compat_config: append_to_env_str(self.env, "VKD3D_CONFIG", "force_bindless_texel_buffer", ",") if "northstar" in self.compat_config: append_to_env_str(self.env, "WINEDLLOVERRIDES", "wsock32=n,b", ";") if "vkd3dfl12" in self.compat_config: if "VKD3D_FEATURE_LEVEL" not in self.env: self.env["VKD3D_FEATURE_LEVEL"] = "12_0" if "hidevggpu" in self.compat_config: self.env["WINE_HIDE_VANGOGH_GPU"] = "1" if "hidenvgpu" in self.compat_config and "forcenvapi" not in self.compat_config: self.env["WINE_HIDE_NVIDIA_GPU"] = "1" if "hideintelgpu" in self.compat_config: self.env["WINE_HIDE_INTEL_GPU"] = "1" if "hideapu" in self.compat_config: self.env["WINE_HIDE_APU"] = "1" if "usenativexinput13" in self.compat_config: self.dlloverrides["xinput1_3"] = "n" if "disablelibglesv2" in self.compat_config: self.dlloverrides["libglesv2"] = "d" if "nativeags" in self.compat_config: self.dlloverrides["atiadlxx"] = "b" self.dlloverrides["atidxx64"] = "b" self.dlloverrides["amd_ags_x64"] = "n" if "nomfdxgiman" in self.compat_config: self.env["WINE_DO_NOT_CREATE_DXGI_DEVICE_MANAGER"] = "1" if "noopwr" in self.compat_config: self.env["WINE_DISABLE_VULKAN_OPWR"] = "1" if "noopwrx11" in self.compat_config and "wayland" not in self.compat_config: self.env["WINE_DISABLE_VULKAN_OPWR"] = "1" if "PROTON_USE_XALIA" not in self.env: if "noxalia" in self.compat_config: self.env["PROTON_USE_XALIA"] = "0" else: self.env["PROTON_USE_XALIA"] = "1" if "xalia" not in self.compat_config: self.env["XALIA_SUPPORTED_ONLY"] = "1" if "nohardwarescheduling" in self.compat_config and "WINE_DISABLE_HARDWARE_SCHEDULING" not in self.env: self.env["WINE_DISABLE_HARDWARE_SCHEDULING"] = "1" if "writecopy" in self.compat_config: self.env["WINE_SIMULATE_WRITECOPY"] = "1" if "PROTON_CRASH_REPORT_DIR" in self.env: self.env["WINE_CRASH_REPORT_DIR"] = self.env["PROTON_CRASH_REPORT_DIR"] if "sdlinput" in self.compat_config: self.env["PROTON_DISABLE_HIDRAW"] = "1" self.env["PROTON_NO_STEAMINPUT"] = "1" if "SDL_GAMECONTROLLER_IGNORE_DEVICES" in self.env: del self.env["SDL_GAMECONTROLLER_IGNORE_DEVICES"] if "hdr" in self.compat_config: self.env["DXVK_HDR"] = "1" if "fnad3d11" in self.compat_config and "FNA3D_FORCE_DRIVER" not in self.env: self.env["FNA3D_FORCE_DRIVER"] = "D3D11" # igdext is loaded either using the LoadLibraryExW hack on 1.3 or lower # or using the INTC_ALT_DRIVER_EXTENSIONS_PATH on 2.0 or newer # TODO: Load it through driver store data like it is on windows # TODO: D3D11 has a different loading mechanism because why not self.env["INTC_ALT_DRIVER_EXTENSIONS_PATH"] = "C:\\Windows\\System32" # PROTON_DISABLE_HIDRAW workarounds if (os.environ.get("SteamGameId", 0) == "2322010" and # God of War: Ragnarok os.environ.get("SteamDeck", 0) == "1"): # Disable hidraw for Sony DualShock and DualSense controllers. self.env["PROTON_DISABLE_HIDRAW"] = "0x054C/0x05C4,0x054C/0x09CC,0x054C/0x0BA0,0x054C/0x0CE6,0x054C/0x0DF2" if "__GLVND_DISALLOW_PATCHING" not in self.env: self.env["__GLVND_DISALLOW_PATCHING"] = "1" if (os.environ.get("SteamGameId", 0) == "1183470"): # Imperiums: Greek Wars self.env.setdefault("WINE_MONO_HIDETYPES", "1") self.env.setdefault("WINE_MONO_HIDETYPES", "0") # Proton-EM custom options: # Don't use wayland backend when WAYLAND_DISPLAY is not set if "wayland" in self.compat_config and "WAYLAND_DISPLAY" in self.env: # Make the temporary Steam overlay bridge discoverable only when # Steam actually injected its overlay and the X11 path remains # available. win32u bootstraps the selected layer for launchers. overlay_preload = ":".join(( self.env.get("LD_PRELOAD", ""), self.env.get("WINE_LD_PRELOAD", ""), )) winex11_order = self.dlloverrides.get("winex11.drv") winex11_available = ( winex11_order is None or "b" in winex11_order.lower() or "n" in winex11_order.lower() ) if ("gameoverlayrenderer.so" in overlay_preload and winex11_available and not nonzero(self.env.get("DISABLE_WINE_WAYLAND_STEAM_OVERLAY_LAYER", "0"))): self.env["WINE_WAYLAND_STEAM_OVERLAY_LAYER"] = "1" append_to_env_str( self.env, "VK_IMPLICIT_LAYER_PATH", g_proton.dist_dir + "share/steam-overlay-wayland/implicit_layer.d", ":") # Set primary monitor if given if "PROTON_WAYLAND_MONITOR" in self.env: self.env["WAYLANDDRV_PRIMARY_MONITOR"] = self.env["PROTON_WAYLAND_MONITOR"] # Try to determine the primary monitor if not given. if "WAYLANDDRV_PRIMARY_MONITOR" not in self.env: primary_monitor = self.get_primary_monitor() if primary_monitor: self.env["WAYLANDDRV_PRIMARY_MONITOR"] = primary_monitor # Select Wayland explicitly while keeping X11 available for # per-process compatibility fallbacks used by launcher UIs. self.env["WINE_GRAPHICS_DRIVER"] = "wayland" self.dlloverrides["winewayland.drv"] = "b" # This is required for OpenGL to work on winewayland self.env["WINE_USE_EGL"] = "1" # FSHack is not supported on winewayland (yet) self.env["WINE_DISABLE_FULLSCREEN_HACK"] = "1" # Enable the move hack self.env.setdefault("WINE_MOVE_HACK", "1") # It is not possible for winewayland to support xalia since # there is no way to position toplevel surfaces dynamically in wayland self.env["PROTON_USE_XALIA"] = "0" # Steam advertises its per-game virtual controller through these # variables. Keep that device available when the game's Steam Input # toggle is enabled: the overlay controller UI consumes its translated # XInput stream. Otherwise suppress the desktop virtual controller and # leave the game's native controller path active. steam_input_enabled = ( ("SteamVirtualGamepadInfo" in self.env and nonzero(self.env["SteamVirtualGamepadInfo"])) or ("SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD" in self.env and nonzero(self.env["SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD"])) ) if "PROTON_NO_STEAMINPUT" not in self.env: self.env["PROTON_NO_STEAMINPUT"] = "0" if steam_input_enabled else "1" # Disable other steam input related stuff if nonzero(self.env["PROTON_NO_STEAMINPUT"]): # When steam input is disabled we need the ignored controllers to work again if "SDL_GAMECONTROLLER_IGNORE_DEVICES" in self.env: del self.env["SDL_GAMECONTROLLER_IGNORE_DEVICES"] # Some other steam input related options we need to remove if "SteamVirtualGamepadInfo" in self.env: del self.env["SteamVirtualGamepadInfo"] if "SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD" in self.env: del self.env["SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD"] # FIXME: temporary workaround for libxkbcommon's current integration self.env.setdefault("XLOCALEDIR", g_proton.dist_dir + "share/X11/locale") # Keep DISPLAY available for processes that explicitly fall back to # XWayland. WINE_GRAPHICS_DRIVER still selects native Wayland for # the desktop and every process without such a fallback. # Use SDL backend for inputs instead of hidraw/Steam Input. if "sdlinput" in self.compat_config: self.env["PROTON_DISABLE_HIDRAW"] = "1" self.env["PROTON_NO_STEAMINPUT"] = "1" if "SDL_GAMECONTROLLER_IGNORE_DEVICES" in self.env: del self.env["SDL_GAMECONTROLLER_IGNORE_DEVICES"] if not ("PROTON_EMULATE_STEAMINPUT" in self.env and nonzero(self.env["PROTON_EMULATE_STEAMINPUT"])): if "SteamVirtualGamepadInfo" in self.env: del self.env["SteamVirtualGamepadInfo"] if "SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD" in self.env: del self.env["SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD"] if (os.environ.get("SteamGameId", 0) == "1183470"): # Imperiums: Greek Wars self.env.setdefault("WINE_MONO_HIDETYPES", "1") # NVIDIA software may check for the "DriverStore" by querying the # NGXCore\NGXPath registry key via `D3DDDI_QUERYREGISTRY_SERVICEKEY` for # a given adapter. In the case where this path cannot be found, the # `NVIDIA_WINE_DLL_DIR` environment variable is read as a fallback. # # TODO: Add support for populating NGXCore\NGXPath so we can remove the # NGX copies done in setup_prefix(), and this environment variable. self.nvidia_wine_dll_dir = find_nvidia_wine_dll_dir() if self.nvidia_wine_dll_dir: self.env["NVIDIA_WINE_DLL_DIR"] = self.nvidia_wine_dll_dir if "PROTON_LOG" in self.env and nonzero(self.env["PROTON_LOG"]): if self.setup_logging(append_forever=False): self.log_file.write("======================\n") with open(g_proton.version_file, "r") as f: self.log_file.write("Proton: " + f.readline().strip() + "\n") if "SteamGameId" in self.env: self.log_file.write("SteamGameId: " + self.env["SteamGameId"] + "\n") self.log_file.write("Command: " + str(sys.argv[2:] + self.cmdlineappend) + "\n") self.log_file.write("Options: " + str(self.compat_config) + "\n") self.try_log_slr_versions() try: uname = os.uname() kernel_version = f"{uname.sysname} {uname.release} {uname.version} {uname.machine}" except OSError: kernel_version = "unknown" self.log_file.write(f"Kernel: {kernel_version}\n") self.log_file.write("Language: LC_ALL " + str(self.env.get("HOST_LC_ALL", None)) + ", LC_MESSAGES " + str(self.env.get("LC_MESSAGES", None)) + ", LC_CTYPE " + str(self.env.get("LC_CTYPE", None)) + "\n") self.log_file.write("PATH: " + str(self.env.get("PATH", None)) + "\n") #dump some important variables into the log header for var in ["WINEDLLOVERRIDES", "WINEDEBUG"]: if var in os.environ: self.log_file.write("System " + var + ": " + os.environ[var] + "\n") if var in used_user_settings: self.log_file.write("User settings " + var + ": " + used_user_settings[var] + "\n") if var in self.env: self.log_file.write("Effective " + var + ": " + self.env[var] + "\n") # check for low fd limit _soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_NOFILE) if hard_limit < 524288: self.log_file.write(f"WARNING: Low file descriptor limit: {hard_limit} (see https://github.com/ValveSoftware/Proton/wiki/File-Descriptors)\n") if os.path.exists("/proc/sys/vm/max_map_count"): with open("/proc/sys/vm/max_map_count", "r") as f: max_map_count = int(f.read()) if max_map_count < 1048576: self.log_file.write(f"WARNING: Low /proc/sys/vm/max_map_count: {max_map_count} will prevent some games from working\n") utilities.log_environment(self.env, self.log_file) self.log_file.write("======================\n") self.log_file.flush() else: self.env["WINEDEBUG"] = "-all" if "PROTON_REMOTE_DEBUG_CMD" in self.env: self.remote_debug_cmd = shlex.split(self.env["PROTON_REMOTE_DEBUG_CMD"]) else: self.remote_debug_cmd = None if update_prefix_files: g_compatdata.setup_prefix() if "fsr4hud" in self.compat_config: self.env["FSR_WATERMARK"] = "1" self.env["FSR_FG_WATERMARK"] = "1" if "dlsshud" in self.compat_config: self.env["DXVK_NVAPI_SET_NGX_DEBUG_OPTIONS"] = "DLSSIndicator=1024,DLSSGIndicator=2," else: self.env.setdefault("DXVK_NVAPI_SET_NGX_DEBUG_OPTIONS", "DLSSIndicator=0,DLSSGIndicator=0,") if "nod3d11" in self.compat_config: self.dlloverrides["d3d11"] = "" if "dxgi" in self.dlloverrides: del self.dlloverrides["dxgi"] if "nod3d10" in self.compat_config: self.dlloverrides["d3d10_1"] = "" self.dlloverrides["d3d10"] = "" self.dlloverrides["dxgi"] = "" if "nativevulkanloader" in self.compat_config: self.dlloverrides["vulkan-1"] = "n" if "disablenvapi" not in self.compat_config or "forcenvapi" in self.compat_config: self.env["DXVK_ENABLE_NVAPI"] = "1" if "forcenvapi" in self.compat_config: self.env["DXVK_NVAPI_ALLOW_OTHER_DRIVERS"] = "1" self.env["DXVK_NVAPI_DRIVER_VERSION"] = "99999" self.env["WINE_HIDE_AMD_GPU"] = "1" if not ("WINEDLLOVERRIDES" in g_session.env and "atiadlxx" in g_session.env["WINEDLLOVERRIDES"]) and "SteamAppId" in os.environ: if os.environ["SteamAppId"] in [ "2767030", ]: g_session.dlloverrides["atiadlxx"] = "b" if not ("PROTON_LIMIT_ADDRESS_SPACE" in g_session.env) and "SteamAppId" in os.environ: if os.environ["SteamAppId"] in [ "1282270", "2963870", ]: self.env["PROTON_LIMIT_ADDRESS_SPACE"] = "1" s = "" for dll in self.dlloverrides: setting = self.dlloverrides[dll] if len(s) > 0: s = s + ";" + dll + "=" + setting else: s = dll + "=" + setting append_to_env_str(self.env, "WINEDLLOVERRIDES", s, ";") if platform.machine() == 'aarch64': if os.environ.get("SteamGameId", 0) == "1167630": # Teardown # https://github.com/microsoft/mimalloc/issues/958 self.env["MIMALLOC_DISABLE_REDIRECT"] = "1" if not 'FEX_APP_CONFIG' in self.env: # custom per-app config driven by Steam env vars app_config = self.generate_fex_app_config() with open(g_compatdata.fex_config_file, 'w') as f: f.write(json.dumps(app_config, indent=2)) self.env['FEX_APP_CONFIG'] = g_compatdata.fex_config_file self.env["FEX_APP_CONFIG_LOCATION"] = os.path.join(g_proton.dist_dir, "share/fex-emu/") def run_proc(self, args, local_env=None): if local_env is None: local_env = self.env return subprocess.call(args, env=local_env, stderr=self.log_file, stdout=self.log_file) def run(self): if shutil.which('steam-runtime-launcher-interface-0') is not None: adverb = ['steam-runtime-launcher-interface-0', 'proton'] else: adverb = [] if self.remote_debug_cmd: remote_debug_cmd = self.remote_debug_cmd if not os.path.isabs(remote_debug_cmd[0]): remote_debug_cmd[0] = g_proton.path(remote_debug_cmd[0]) remote_debug_proc = subprocess.Popen([g_proton.wine_bin] + self.remote_debug_cmd, env=self.env, stderr=self.log_file, stdout=self.log_file) else: remote_debug_proc = None # CoD: Black Ops 3 workaround if os.environ.get("SteamGameId", 0) in [ "311210", # CoD: Black Ops 3 "1985810", # CoD: Black Ops Cold War "1549250", # Undecember ]: argv = [g_proton.wine_bin, "c:\\Program Files (x86)\\Steam\\steam.exe"] # Don't use steam if it's not a steam game # Prevent this warning for non-steam games: # [S_API FAIL] SteamAPI_Init() failed; no appID found. # Either launch the game from Steam, or put the file steam_appid.txt containing the correct appID in your game folder. elif "UMU_ID" in os.environ and os.environ.get("UMU_USE_STEAM", "0") != "1": if len(sys.argv) >= 3 and sys.argv[2].startswith('/'): log(sys.argv[2]) log("Executable is a unix path, launching with 'umu.exe'.") if g_proton.host_pe_arch == "x86_64-windows": #run with winepreloader directly to avoid restart through start.exe self.env["WINELOADERNOEXEC"] = "1" argv = [g_proton.lib_dir + "/wine/x86_64-unix/wine-preloader", g_proton.lib_dir + "/wine/x86_64-unix/wine", "c:\\windows\\system32\\umu.exe"] else: argv = [g_proton.wine_bin, "c:\\windows\\system32\\umu.exe"] else: log("Executable is inside wine prefix, launching normally.") argv = [g_proton.wine_bin] else: if "UMU_ID" in os.environ: append_to_env_str(self.env, "WINEDLLOVERRIDES", "lsteamclient=d", ";") if g_proton.host_pe_arch == "x86_64-windows": #run with winepreloader directly to avoid restart through start.exe self.env["WINELOADERNOEXEC"] = "1" argv = [g_proton.lib_dir + "/wine/x86_64-unix/wine-preloader", g_proton.lib_dir + "/wine/x86_64-unix/wine", "c:\\windows\\system32\\steam.exe"] else: argv = [g_proton.wine_bin, "c:\\windows\\system32\\steam.exe"] rc = self.run_proc(adverb + argv + sys.argv[2:] + self.cmdlineappend) if remote_debug_proc: remote_debug_proc.kill() try: remote_debug_proc.communicate(timeout=2) except subprocess.TimeoutExpired: log("terminate remote debugger") remote_debug_proc.terminate() remote_debug_proc.communicate() return rc if __name__ == "__main__": if "STEAM_COMPAT_DATA_PATH" not in os.environ: log("No compat data path?") sys.exit(1) g_proton = Proton(os.path.dirname(sys.argv[0])) g_proton.cleanup_legacy_dist() g_proton.do_steampipe_fixups() g_compatdata = CompatData(os.environ["STEAM_COMPAT_DATA_PATH"]) g_session = Session() g_session.init_wine() if g_proton.missing_default_prefix(): g_proton.make_default_prefix() g_session.init_session(sys.argv[1] != "runinprefix") # protonfixes execution protonfixes.setup(g_session.env, "PATH", ld_path_var, prepend_to_env_str) protonfixes.setup_mount_drives(setup_dir_drive) protonfixes.winetricks(g_session.env, g_proton.wine_bin, g_proton.wineserver_bin) # HACK: handle DXVK_FRAME_RATE and VKD3D_FRAME_RATE through DXVK_CONFIG protonfixes.setup_frame_rate(g_session.env, append_to_env_str) g_session.check_environment("PROTON_LOCAL_SHADER_CACHE", "localshadercache") protonfixes.setup_local_shader_cache(g_session.compat_config, g_session.env) g_session.check_environment("PROTON_FSR3_UPGRADE", "fsr3") g_session.check_environment("PROTON_FSR4_UPGRADE", "fsr4") g_session.compat_config.add("mlfg") g_session.check_environment("PROTON_MLFG_UPGRADE", "mlfg") g_session.check_environment("PROTON_DLSS_UPGRADE", "dlss") g_session.check_environment("PROTON_XESS_UPGRADE", "xess") g_session.check_environment("PROTON_USE_OPTISCALER", "optiscaler") protonfixes.setup_upscalers(g_session.compat_config, g_session.env, g_compatdata.base_dir, g_compatdata.prefix_dir) protonfixes.execute() g_compatdata.setup_ddraw("PROTON_USE_D7VK" in g_session.env and nonzero(g_session.env["PROTON_USE_D7VK"])) #determine mode rc = 0 if sys.argv[1] == "run": #start target app setup_game_dir_drive() setup_steam_dir_drive() rc = g_session.run() elif sys.argv[1] == "waitforexitandrun": #wait for wineserver to shut down g_session.run_proc([g_proton.wineserver_bin, "-w"]) #then run rc = g_session.run() elif sys.argv[1] == "runinprefix": rc = g_session.run_proc([g_proton.wine_bin] + sys.argv[2:]) elif sys.argv[1] == "destroyprefix": g_compatdata.remove_tracked_files() elif sys.argv[1] == "getcompatpath": #linux -> windows path path = subprocess.check_output([g_proton.wine_bin, "winepath", "-w", sys.argv[2]], env=g_session.env, stderr=g_session.log_file) sys.stdout.buffer.write(path) elif sys.argv[1] == "getnativepath": #windows -> linux path path = subprocess.check_output([g_proton.wine_bin, "winepath", sys.argv[2]], env=g_session.env, stderr=g_session.log_file) sys.stdout.buffer.write(path) else: log("Need a verb.") sys.exit(1) sys.exit(rc) #pylint --disable=C0301,C0326,C0330,C0111,C0103,R0902,C1801,R0914,R0912,R0915 # vim: set syntax=python: