|
1 | 1 | """Game fix The Lord of the Rings Online"""
|
2 | 2 |
|
3 | 3 | from protonfixes import util
|
4 |
| -import subprocess |
5 |
| -import sys |
6 |
| -from tempfile import mkstemp |
7 |
| -import __main__ as protonmain |
8 |
| - |
9 |
| -source = """ |
10 |
| -def _mouse_fix_subprocess() -> None: |
11 |
| - # this will only be imported in the temporary script |
12 |
| - import struct |
13 |
| - import time |
14 |
| - import fcntl |
15 |
| - from Xlib.X import LASTEvent |
16 |
| - from Xlib.display import Display |
17 |
| - from Xlib.ext import xinput |
18 |
| - from Xlib.error import XError |
19 |
| - from Xlib.protocol.request import GetProperty |
20 |
| - from Xlib.xobject.drawable import Window |
21 |
| - from typing import Union |
22 |
| -
|
23 |
| - def is_window_focused(dpy: Display, window: Window) -> bool: |
24 |
| - '''Returns True if the given window is currently focused (active).''' |
25 |
| - root = dpy.screen().root |
26 |
| - net_active_window = dpy.get_atom('_NET_ACTIVE_WINDOW') |
27 |
| - prop = root.get_full_property(net_active_window, 0) |
28 |
| - if not prop: |
29 |
| - return False |
30 |
| - return prop.value[0] == window.id |
31 |
| -
|
32 |
| - def get_window_name(dpy: Display, win: Window) -> Union[GetProperty, None]: |
33 |
| - '''Retrieve the window name using WM_NAME or _NET_WM_NAME.''' |
34 |
| - name = win.get_wm_name() |
35 |
| - if name: |
36 |
| - return name |
37 |
| - net_name = win.get_full_property(dpy.get_atom('_NET_WM_NAME'), 0) |
38 |
| - if net_name: |
39 |
| - return net_name.value.decode('utf-8') |
40 |
| - return None |
41 |
| -
|
42 |
| - def find_window_by_title( |
43 |
| - dpy: Display, title: str, win: Union[Display, None] = None |
44 |
| - ) -> Union[Window, None]: |
45 |
| - '''Recursively find a window with a title containing the given string.''' |
46 |
| - if win is None: |
47 |
| - win = dpy.screen().root |
48 |
| - name = get_window_name(dpy, win) |
49 |
| - if name and title == name: |
50 |
| - return win |
51 |
| - for child in win.query_tree().children: |
52 |
| - found = find_window_by_title(dpy, title, child) |
53 |
| - if found: |
54 |
| - return found |
55 |
| - return None |
56 |
| -
|
57 |
| - def get_game_window(dpy: Display, title: str) -> Union[Window, None]: |
58 |
| - game_window = None |
59 |
| - retries = 0 |
60 |
| - while game_window is None: |
61 |
| - game_window = find_window_by_title(dpy, title) |
62 |
| - if game_window is None: |
63 |
| - # give some time for the game window to open |
64 |
| - time.sleep(1) |
65 |
| - retries += 1 |
66 |
| - if retries >= 30: |
67 |
| - # close display before exit |
68 |
| - dpy.close() |
69 |
| - exit() |
70 |
| - return game_window |
71 |
| -
|
72 |
| - def mouse_fix(title: str) -> None: |
73 |
| - # Check if there is a display |
74 |
| - if not os.getenv('DISPLAY', None): |
75 |
| - raise RuntimeError('No display detected') |
76 |
| - display_id = os.getenv('DISPLAY') |
77 |
| - # Prevent running 2 instances at once of the mouse fix |
78 |
| - lockfile_path = f'/tmp/mouse_fix_display_lock_{display_id.replace(":", "_")}' |
79 |
| - try: |
80 |
| - lockfile = open(lockfile_path, 'w') |
81 |
| - fcntl.flock(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB) |
82 |
| - except OSError: |
83 |
| - return |
84 |
| - dpy = Display() |
85 |
| - # Check if the XInput extension is available |
86 |
| - if not dpy.query_extension('XInputExtension'): |
87 |
| - raise RuntimeError('X Input Extension not available') |
88 |
| -
|
89 |
| - # Initialize the XInput extension |
90 |
| - xinput_version = xinput.query_version(dpy) |
91 |
| - print( |
92 |
| - 'XInput version:', |
93 |
| - xinput_version.major_version, |
94 |
| - xinput_version.minor_version, |
95 |
| - file=sys.stderr |
96 |
| - ) |
97 |
| - # Wait for the game window to appear |
98 |
| - print(f"Waiting for window with title containing '{title}'...", file=sys.stderr) |
99 |
| - game_window = get_game_window(dpy, title) |
100 |
| - print(f'Found game window: {get_window_name(dpy, game_window)}', file=sys.stderr) |
101 |
| - # detect button press and button release events |
102 |
| - dpy.screen().root.xinput_select_events( |
103 |
| - [ |
104 |
| - ( |
105 |
| - xinput.AllDevices, |
106 |
| - (1 << xinput.RawButtonPress) | (1 << xinput.RawButtonRelease), |
107 |
| - ) |
108 |
| - ] |
109 |
| - ) |
110 |
| - buttons_held = set() |
111 |
| - while True: |
112 |
| - event = dpy.next_event() |
113 |
| - if event.type != LASTEvent or not hasattr(event, 'extension'): |
114 |
| - continue |
115 |
| - event_data = struct.unpack_from('HHHH', event.data) |
116 |
| - dpy.xfixes_query_version() |
117 |
| - # On button press |
118 |
| - if event.evtype == xinput.RawButtonPress: |
119 |
| - button = event_data[3] |
120 |
| - if 1 <= button <= 3 and event_data[0] == 7: |
121 |
| - buttons_held.add(button) |
122 |
| - # only trigger hide_cursor when the first button is added to the set -> len(buttons_held) == 1 |
123 |
| - # do not check for > 0, this will trigger the hide_cursor more than once when multiple buttons are |
124 |
| - # pressed and will hide the cursor forever unless the game is closed |
125 |
| - if len(buttons_held) == 1 and is_window_focused(dpy, game_window): |
126 |
| - dpy.screen().root.xfixes_hide_cursor() |
127 |
| - dpy.sync() |
128 |
| - elif not is_window_focused(dpy, game_window): |
129 |
| - # reset button state when going out of focus |
130 |
| - buttons_held = set() |
131 |
| - # get the new game window when we move from launcher to actual game window, they have the same name |
132 |
| - try: |
133 |
| - geometry = game_window.get_geometry() |
134 |
| - if ( |
135 |
| - hasattr(geometry, 'data') |
136 |
| - and geometry.data.get('height', 0) == 553 |
137 |
| - ): |
138 |
| - game_window = get_game_window(dpy, title) |
139 |
| - continue |
140 |
| - except XError: |
141 |
| - game_window = get_game_window(dpy, title) |
142 |
| - continue |
143 |
| - continue |
144 |
| -
|
145 |
| - # On button release |
146 |
| - elif event.evtype == xinput.RawButtonRelease: |
147 |
| - button = event_data[3] |
148 |
| - if 1 <= button <= 3: |
149 |
| - buttons_held.discard(button) |
150 |
| - if len(buttons_held) == 0: |
151 |
| - dpy.screen().root.xfixes_show_cursor() |
152 |
| - dpy.sync() |
153 |
| - continue |
154 |
| -
|
155 |
| - mouse_fix('The Lord of the Rings Online™') |
156 |
| -""" |
157 |
| - |
158 |
| -script = f""" |
159 |
| -import os |
160 |
| -import sys |
161 |
| -{source} |
162 |
| -
|
163 |
| -if __name__ == "__main__": |
164 |
| - sys.exit(_mouse_fix_subprocess()) |
165 |
| - """ |
166 | 4 |
|
167 | 5 |
|
168 | 6 | def main() -> None:
|
169 | 7 | """Disable libglesv2"""
|
170 | 8 | ## gpu acceleration on wined3d https://bugs.winehq.org/show_bug.cgi?id=44985
|
171 | 9 | # Make the store work.
|
172 | 10 | util.winedll_override('libglesv2', util.OverrideOrder.DISABLED)
|
173 |
| - |
174 |
| - # Fix visible mouse in middle of screen while rotating camera |
175 |
| - # This needs to run as a subprocess while the game is running, |
176 |
| - # the process will close itself when the game window isn't detected for 30 seconds |
177 |
| - util.log.info('Applying mouse fix') |
178 |
| - _, tmp = mkstemp(suffix='.py', text=True) |
179 |
| - with open(tmp, mode='w', encoding='utf-8') as f: |
180 |
| - f.write(script) |
181 |
| - env = protonmain.g_session.env.copy() |
182 |
| - env['PYTHONPATH'] = ':'.join(sys.path) |
183 |
| - subprocess.Popen( |
184 |
| - [sys.executable, tmp], close_fds=True, stderr=subprocess.DEVNULL, env=env |
185 |
| - ) |
0 commit comments