|
1 | 1 | import subprocess |
2 | 2 | import os |
3 | 3 | import shutil |
| 4 | +import logging |
4 | 5 | from config_manager import ConfigManager |
| 6 | + |
5 | 7 | class GameManager: |
6 | 8 | def __init__(self, proton_manager): |
7 | 9 | self.proton_manager = proton_manager |
8 | 10 | self.config_manager = ConfigManager() |
| 11 | + |
9 | 12 | def add_game(self, game): |
10 | 13 | games = self.config_manager.load_games() |
11 | 14 | games.append(game) |
12 | 15 | self.config_manager.save_games(games) |
| 16 | + logging.info(f"Added game: {game['name']}") |
| 17 | + |
13 | 18 | def remove_game(self, name): |
14 | 19 | games = self.config_manager.load_games() |
15 | 20 | games = [g for g in games if g['name'] != name] |
16 | 21 | self.config_manager.save_games(games) |
| 22 | + logging.info(f"Removed game: {name}") |
| 23 | + |
17 | 24 | def launch_game(self, game, gamescope=False): |
18 | 25 | runner = game['runner'] |
19 | 26 | exe = game['exe'] |
| 27 | + if not os.path.exists(exe) and runner != 'Steam': |
| 28 | + raise Exception(f"Executable does not exist: {exe}") |
20 | 29 | launch_options = game.get('launch_options', '').split() |
| 30 | + env = os.environ.copy() |
| 31 | + prefix = game.get('prefix', '') |
| 32 | + if runner in ['Wine', 'Proton']: |
| 33 | + if not prefix: |
| 34 | + raise Exception("Prefix not set for Wine/Proton runner") |
| 35 | + # Ensure prefix is writable by the current user |
| 36 | + try: |
| 37 | + os.makedirs(prefix, exist_ok=True) |
| 38 | + # Fix ownership if running as root (optional, for robustness) |
| 39 | + if os.geteuid() == 0: # Running as root |
| 40 | + user_id = os.getuid() |
| 41 | + group_id = os.getgid() |
| 42 | + subprocess.run(['chown', '-R', f'{user_id}:{group_id}', prefix], check=True) |
| 43 | + except Exception as e: |
| 44 | + raise Exception(f"Failed to set up prefix {prefix}: {e}") |
| 45 | + env['WINEPREFIX'] = prefix |
| 46 | + if game.get('enable_dxvk', False): |
| 47 | + env['WINEDLLOVERRIDES'] = 'd3d11=n,b;dxgi=n,b' |
| 48 | + # Per-game overrides if set, else global |
| 49 | + env['WINEESYNC'] = '1' if game.get('enable_esync', self.config_manager.load_settings()['enable_esync']) else '0' |
| 50 | + env['WINEFSYNC'] = '1' if game.get('enable_fsync', self.config_manager.load_settings()['enable_fsync']) else '0' |
| 51 | + env['DXVK_ASYNC'] = '1' if game.get('enable_dxvk_async', self.config_manager.load_settings()['enable_dxvk_async']) else '0' |
| 52 | + cmd = [] |
21 | 53 | if gamescope: |
22 | 54 | if not shutil.which('gamescope'): |
23 | | - raise Exception("Gamescope is not installed. Please install it using 'sudo apt install gamescope'.") |
24 | | - cmd = ['gamescope', '--'] |
25 | | - if '--bigpicture' in launch_options: |
26 | | - cmd.extend(['-e', '-f']) |
27 | | - launch_options.remove('--bigpicture') |
| 55 | + raise Exception("Gamescope is not installed. Please install it via your package manager (e.g., apt, dnf, pacman).") |
| 56 | + cmd = ['gamescope'] |
| 57 | + # Expanded Gamescope options without duplication |
| 58 | + options_to_remove = [] |
| 59 | + if '--adaptive-sync' in launch_options: |
| 60 | + cmd.append('--adaptive-sync') |
| 61 | + options_to_remove.append('--adaptive-sync') |
| 62 | + if '--force-grab-cursor' in launch_options: |
| 63 | + cmd.append('--force-grab-cursor') |
| 64 | + options_to_remove.append('--force-grab-cursor') |
| 65 | + width_idx = next((i for i, opt in enumerate(launch_options) if opt.startswith('--width=')), None) |
| 66 | + if width_idx is not None: |
| 67 | + cmd.append('-W') |
| 68 | + cmd.append(launch_options[width_idx].split('=')[1]) |
| 69 | + options_to_remove.append(launch_options[width_idx]) |
| 70 | + height_idx = next((i for i, opt in enumerate(launch_options) if opt.startswith('--height=')), None) |
| 71 | + if height_idx is not None: |
| 72 | + cmd.append('-H') |
| 73 | + cmd.append(launch_options[height_idx].split('=')[1]) |
| 74 | + options_to_remove.append(launch_options[height_idx]) |
28 | 75 | if '--fullscreen' in launch_options: |
29 | 76 | cmd.append('-f') |
30 | | - launch_options.remove('--fullscreen') |
31 | | - else: |
32 | | - cmd = [] |
| 77 | + options_to_remove.append('--fullscreen') |
33 | 78 | if '--bigpicture' in launch_options: |
34 | | - launch_options.remove('--bigpicture') # Handled separately if needed |
35 | | - if '--fullscreen' in launch_options: |
36 | | - launch_options.append('-f') |
37 | | - if runner == 'Native': |
38 | | - cmd.extend([exe] + launch_options) |
39 | | - subprocess.Popen(cmd) |
40 | | - elif runner == 'Wine': |
41 | | - env = os.environ.copy() |
42 | | - env['WINEPREFIX'] = game['prefix'] |
43 | | - cmd.extend(['wine', exe] + launch_options) |
44 | | - subprocess.Popen(cmd, env=env) |
45 | | - elif runner == 'Flatpak': |
46 | | - cmd.extend(['flatpak', 'run', exe] + launch_options) |
47 | | - subprocess.Popen(cmd) |
48 | | - elif runner == 'Snap': |
49 | | - cmd.extend(['snap', 'run', exe] + launch_options) |
50 | | - subprocess.Popen(cmd) |
51 | | - else: # Proton |
52 | | - proton_path = self.proton_manager.get_proton_path(runner) |
53 | | - prefix = game['prefix'] |
54 | | - os.makedirs(prefix, exist_ok=True) |
55 | | - env = os.environ.copy() |
56 | | - env['STEAM_COMPAT_DATA_PATH'] = prefix |
57 | | - cmd.extend([proton_path, 'run', exe] + launch_options) |
58 | | - subprocess.Popen(cmd, env=env) |
| 79 | + cmd.extend(['-e', '-f']) |
| 80 | + options_to_remove.append('--bigpicture') |
| 81 | + for opt in options_to_remove: |
| 82 | + if opt in launch_options: |
| 83 | + launch_options.remove(opt) |
| 84 | + cmd.append('--') |
| 85 | + try: |
| 86 | + if runner == 'Native': |
| 87 | + cmd.extend([exe] + launch_options) |
| 88 | + elif runner == 'Wine': |
| 89 | + cmd.extend(['wine', exe] + launch_options) |
| 90 | + elif runner == 'Flatpak': |
| 91 | + if not shutil.which('flatpak'): |
| 92 | + raise Exception("Flatpak not installed.") |
| 93 | + cmd.extend(['flatpak', 'run', exe] + launch_options) |
| 94 | + elif runner == 'Snap': |
| 95 | + if not shutil.which('snap'): |
| 96 | + raise Exception("Snap not installed.") |
| 97 | + cmd.extend(['snap', 'run', exe] + launch_options) |
| 98 | + elif runner == 'Steam': |
| 99 | + app_id = game.get('app_id', '') |
| 100 | + if not app_id: |
| 101 | + raise Exception("Steam App ID not set.") |
| 102 | + cmd.extend(['steam', '-applaunch', app_id] + launch_options) |
| 103 | + else: # Proton |
| 104 | + proton_path = self.proton_manager.get_proton_path(runner) |
| 105 | + if not os.path.exists(proton_path): |
| 106 | + raise Exception(f"Proton binary not found for {runner}") |
| 107 | + env['STEAM_COMPAT_DATA_PATH'] = prefix |
| 108 | + # Set STEAM_COMPAT_CLIENT_INSTALL_PATH |
| 109 | + steam_dir = os.path.expanduser('~/.steam/steam') |
| 110 | + if not os.path.exists(steam_dir): |
| 111 | + steam_dir = os.path.expanduser('~/.local/share/Steam') |
| 112 | + if not os.path.exists(steam_dir): |
| 113 | + # Try Flatpak Steam |
| 114 | + steam_dir = os.path.expanduser('~/.var/app/com.valvesoftware.Steam/data/Steam') |
| 115 | + if not os.path.exists(steam_dir): |
| 116 | + raise Exception("Steam installation not found. Please ensure Steam is installed.") |
| 117 | + env['STEAM_COMPAT_CLIENT_INSTALL_PATH'] = steam_dir |
| 118 | + cmd.extend([proton_path, 'run', exe] + launch_options) |
| 119 | + # Additional Proton fallbacks |
| 120 | + if 'GE-Proton' in runner: |
| 121 | + try: |
| 122 | + subprocess.Popen(cmd, env=env) |
| 123 | + return |
| 124 | + except: |
| 125 | + cmd = [proton_path, 'waitforexitandrun', exe] + launch_options |
| 126 | + log_file = os.path.join(self.config_manager.logs_dir, f"{game['name'].replace(' ', '_')}.log") |
| 127 | + with open(log_file, 'w') as f: |
| 128 | + process = subprocess.Popen(cmd, env=env, stdout=f, stderr=f) |
| 129 | + logging.info(f"Launched game: {game['name']} with cmd: {' '.join(cmd)}") |
| 130 | + except Exception as e: |
| 131 | + logging.error(f"Failed to launch {game['name']}: {e}") |
| 132 | + raise |
0 commit comments