mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
feat(launcher): select client-in-game mode from preferences
Add RCLOL_CLIENT_IN_GAME_TRICKS=1 as the practical launcher switch. It reads lol-user-experience from LCULocalPreferences.yaml: - potatoModeEnabled false or unreadable: no action; - closeLeagueClientDuringGame true: close the client UI; - closeLeagueClientDuringGame false: hide the client windows. The existing close and hide switches remain low-level options when the automatic switch is not set. The hide helper accepts the master switch, and the resolver plus preference cases are covered by tests and documentation. The local .desktop entry was validated with the master switch; the installed preferences select hide on this machine.
This commit is contained in:
1 parent
3c046252a7
commit
4faa622336
9 files changed
+231
-61
No files matched your search
@@ -32,3 +32,4 @@ jobs:
|
||||
- run: python -m unittest discover -s tests -p 'test_exec_guest.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_close_league_client_in_game.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_hide_league_client_in_game.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_client_in_game_tricks.py'
|
||||
@@ -109,13 +109,21 @@ First installation and future Riot updates can still encounter compatibility gap
|
||||
|
||||
For another region, set `RCLOL_REGION` and `RCLOL_LOCALE` before launch. For example `RCLOL_REGION=NA RCLOL_LOCALE=en_US ./scripts/run.sh`. Linux display selection follows `$DISPLAY`; the legacy launcher fallback is `:1`.
|
||||
|
||||
If you want to keep the client open during matches, opt in to hiding its window instead of closing it:
|
||||
For automatic client handling during a match, opt in with one switch:
|
||||
|
||||
```sh
|
||||
RCLOL_HIDE_CLIENT_IN_GAME=1 ./scripts/run.sh
|
||||
RCLOL_CLIENT_IN_GAME_TRICKS=1 ./scripts/run.sh
|
||||
```
|
||||
|
||||
The helper detects the match, hides the `LeagueClientUx`/`Riot Client` X11 windows, and restores them when the game ends. It does not close the client, patch CEF or touch the game window. It is off by default, requires X11/Xwayland, and `RCLOL_CLOSE_CLIENT_IN_GAME=1` takes precedence. See [troubleshooting](docs/TROUBLESHOOTING.md#keep-the-client-open-but-hide-it-during-a-match) for the measured CPU effect and caveats.
|
||||
It reads `lol-user-experience` from the installed League client's
|
||||
`LCULocalPreferences.yaml`. When `potatoModeEnabled` is true, it closes the
|
||||
client UI if `closeLeagueClientDuringGame` is true; otherwise it hides the
|
||||
`LeagueClientUx`/`Riot Client` X11 windows and restores them when the game
|
||||
ends. With client optimizations disabled or unreadable preferences it does
|
||||
nothing. The `RCLOL_CLOSE_CLIENT_IN_GAME` and `RCLOL_HIDE_CLIENT_IN_GAME`
|
||||
low-level switches remain available when the automatic switch is not set. See
|
||||
[troubleshooting](docs/TROUBLESHOOTING.md#automatic-client-handling-during-a-match)
|
||||
for the measured effect and caveats.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
+11
-6
@@ -78,12 +78,17 @@ opts in to Hyprland-only recovery of a default-prefix Riot session with no
|
||||
visible window; it is off by default. See [troubleshooting](TROUBLESHOOTING.md#riot-exits-and-league-reports-that-it-lost-its-connection)
|
||||
for the observed failures, recovery guards and NSURLSession limitations.
|
||||
|
||||
`RCLOL_HIDE_CLIENT_IN_GAME=1` opts in to hiding the managed `LeagueClientUx`
|
||||
and `Riot Client` X11 windows while a match runs, then showing them when the
|
||||
game process ends. It keeps the client open and is mutually exclusive with
|
||||
`RCLOL_CLOSE_CLIENT_IN_GAME=1` (close takes precedence). It is off by default,
|
||||
requires X11/Xwayland, and does not patch client binaries, CEF or the game. See
|
||||
[the troubleshooting measurement](TROUBLESHOOTING.md#keep-the-client-open-but-hide-it-during-a-match)
|
||||
`RCLOL_CLIENT_IN_GAME_TRICKS=1` opts in to automatic client handling during a
|
||||
match. It reads `lol-user-experience` from the installed client's
|
||||
`LCULocalPreferences.yaml`: with `potatoModeEnabled: true`, it closes the client
|
||||
UI when `closeLeagueClientDuringGame: true`, otherwise it hides the managed
|
||||
`LeagueClientUx`/`Riot Client` X11 windows and restores them when the game
|
||||
ends. With client optimizations disabled or preferences unreadable it does
|
||||
nothing. The low-level `RCLOL_CLOSE_CLIENT_IN_GAME=1` and
|
||||
`RCLOL_HIDE_CLIENT_IN_GAME=1` switches remain available when the automatic
|
||||
switch is not set. The feature is off by default, requires X11/Xwayland, and
|
||||
does not patch client binaries, CEF or the game. See
|
||||
[the troubleshooting measurement](TROUBLESHOOTING.md#automatic-client-handling-during-a-match)
|
||||
for the measured CPU effect and limitations.
|
||||
|
||||
`RCLOL_FILTER_HOST_ENV=1` opts in to removing host desktop-session variables before
|
||||
|
||||
+29
-27
@@ -192,30 +192,39 @@ observation, without waiting for a match:
|
||||
python3 tools/close-league-client-in-game.py --once --dry-run
|
||||
```
|
||||
|
||||
### Keep the client open but hide it during a match
|
||||
### Automatic client handling during a match
|
||||
|
||||
`LeagueClientUx` can consume a noticeable amount of CPU while the client stays
|
||||
open during a match. An opt-in alternative to closing it hides the managed
|
||||
client window instead:
|
||||
A single opt-in switch reads the client's own preferences and chooses the mode:
|
||||
|
||||
```sh
|
||||
RCLOL_HIDE_CLIENT_IN_GAME=1 ./scripts/run.sh
|
||||
RCLOL_CLIENT_IN_GAME_TRICKS=1 ./scripts/run.sh
|
||||
```
|
||||
|
||||
The launcher detects when a match starts, unmaps the `LeagueClientUx` and
|
||||
`Riot Client` X11 windows, and maps them again when the game process ends. The
|
||||
client is not closed, the League backend and Riot client keep running, and the
|
||||
game window is never hidden. `RCLOL_CLOSE_CLIENT_IN_GAME=1` takes precedence
|
||||
when both flags are set.
|
||||
It reads `lol-user-experience` from the installed client's
|
||||
`LCULocalPreferences.yaml`:
|
||||
|
||||
The helper is off by default and window-management only: it does not patch
|
||||
client binaries, CEF, anti-cheat or DRM. It attributes the game to this prefix
|
||||
through the loader's `__mldr_sockpath`, and fails closed when ownership, proc
|
||||
state, environment or window information cannot be read. It requires an X11 or
|
||||
Xwayland desktop; on a native Wayland window manager without Xwayland it cannot
|
||||
find the windows.
|
||||
| Client preference | Launcher action |
|
||||
|---|---|
|
||||
| `potatoModeEnabled: false` or unreadable | no client-in-game helper |
|
||||
| `potatoModeEnabled: true`, `closeLeagueClientDuringGame: true` | close the client UI |
|
||||
| `potatoModeEnabled: true`, `closeLeagueClientDuringGame: false` | hide the client windows |
|
||||
|
||||
On a CPU-bound host the effect appears after CEF has been hidden for a few
|
||||
The hide path unmaps the `LeagueClientUx` and `Riot Client` X11 windows and maps
|
||||
them again when the game process ends. The client is not closed, the League
|
||||
backend and Riot client keep running, and the game window is never hidden. The
|
||||
close path uses the UI-close helper described above.
|
||||
|
||||
The low-level `RCLOL_HIDE_CLIENT_IN_GAME=1` and
|
||||
`RCLOL_CLOSE_CLIENT_IN_GAME=1` switches remain available when the automatic
|
||||
switch is not set. All modes are off by default. The helpers are
|
||||
window-management only: they do not patch client binaries, CEF, anti-cheat or
|
||||
DRM. They attribute the game to this prefix through the loader's
|
||||
`__mldr_sockpath`, and fail closed when ownership, proc state, environment or
|
||||
window information cannot be read. The hide helper requires an X11 or Xwayland
|
||||
desktop; on a native Wayland window manager without Xwayland it cannot find the
|
||||
windows.
|
||||
|
||||
On a CPU-bound host the hide effect appears after CEF has been hidden for a few
|
||||
seconds. In a local 30-second test on an i5-3470 with a Radeon RX 550, hiding
|
||||
`LeagueClientUx` reduced the summed CPU of the matched client/CEF processes from
|
||||
120.7% to 93.4% (about 23%, or 27 percentage points, of one core); showing the
|
||||
@@ -224,16 +233,9 @@ a game that is GPU-bound, a client with no active UI work, or a different
|
||||
desktop may show less benefit. The hidden window can also need a moment to
|
||||
repaint after it is shown.
|
||||
|
||||
`RCLOL_CLOSE_CLIENT_IN_GAME=1` remains the more aggressive option when in-client
|
||||
chat during the match is not needed. Configure one of the two, not both:
|
||||
|
||||
```sh
|
||||
# Keep the client open, hide its windows while playing
|
||||
RCLOL_HIDE_CLIENT_IN_GAME=1 ./scripts/run.sh
|
||||
|
||||
# Or close the client UI while playing
|
||||
RCLOL_CLOSE_CLIENT_IN_GAME=1 ./scripts/run.sh
|
||||
```
|
||||
With client optimizations enabled and close-during-game enabled, the close
|
||||
helper remains the more aggressive option when in-client chat during the match
|
||||
is not needed.
|
||||
|
||||
Diagnose the match detector without changing any window:
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Select the automatic client-in-game mode from LCULocalPreferences.yaml."""
|
||||
import importlib.util
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
'client_in_game_tricks', ROOT / 'tools/client-in-game-tricks.py')
|
||||
tricks = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(tricks)
|
||||
|
||||
|
||||
class ClientInGameTricksTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.path = Path(self.temp.name) / 'LCULocalPreferences.yaml'
|
||||
|
||||
def write(self, optimization=None, close=None):
|
||||
lines = ['lol-user-experience:', ' data:']
|
||||
if optimization is not None:
|
||||
lines.append(f' potatoModeEnabled: {optimization}')
|
||||
if close is not None:
|
||||
lines.append(f' closeLeagueClientDuringGame: {close}')
|
||||
self.path.write_text('\n'.join(lines) + '\n')
|
||||
|
||||
def test_close_when_both_preferences_are_true(self):
|
||||
self.write(True, True)
|
||||
self.assertEqual(tricks.mode(self.path), 'close')
|
||||
|
||||
def test_hide_when_optimizations_are_on_and_close_is_false(self):
|
||||
self.write(True, False)
|
||||
self.assertEqual(tricks.mode(self.path), 'hide')
|
||||
|
||||
def test_hide_when_close_preference_is_missing(self):
|
||||
self.write(True)
|
||||
self.assertEqual(tricks.mode(self.path), 'hide')
|
||||
|
||||
def test_none_when_optimizations_are_off(self):
|
||||
self.write(False, True)
|
||||
self.assertEqual(tricks.mode(self.path), 'none')
|
||||
|
||||
def test_none_when_optimizations_are_missing_or_file_is_unreadable(self):
|
||||
self.write(None, True)
|
||||
self.assertEqual(tricks.mode(self.path), 'none')
|
||||
self.assertEqual(tricks.mode(self.path.with_name('missing.yaml')), 'none')
|
||||
|
||||
def test_last_duplicate_value_wins_and_quotes_are_accepted(self):
|
||||
self.path.write_text(
|
||||
'potatoModeEnabled: false\n'
|
||||
'closeLeagueClientDuringGame: false\n'
|
||||
'potatoModeEnabled: "true"\n'
|
||||
'closeLeagueClientDuringGame: "true"\n')
|
||||
self.assertEqual(tricks.mode(self.path), 'close')
|
||||
|
||||
def test_main_prints_mode(self):
|
||||
self.write(True, False)
|
||||
output = io.StringIO()
|
||||
with patch('sys.stdout', output):
|
||||
self.assertEqual(tricks.main([str(self.path)]), 0)
|
||||
self.assertEqual(output.getvalue().strip(), 'hide')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -5,7 +5,7 @@ import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
@@ -217,6 +217,24 @@ class HideClientInGameTests(unittest.TestCase):
|
||||
}):
|
||||
self.assertEqual(hide_in_game.main([]), 0)
|
||||
|
||||
def test_master_tricks_switch_enables_hide(self):
|
||||
class FakeX11:
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
watcher = Mock()
|
||||
with patch.dict(os.environ, {
|
||||
'RCLOL_CLIENT_IN_GAME_TRICKS': '1',
|
||||
'RCLOL_HIDE_CLIENT_IN_GAME': '',
|
||||
'RCLOL_CLOSE_CLIENT_IN_GAME': '',
|
||||
}), patch.object(hide_in_game, 'target_prefix',
|
||||
return_value=self.prefix), \
|
||||
patch.object(hide_in_game, 'X11', return_value=FakeX11()), \
|
||||
patch.object(hide_in_game, 'Watcher', return_value=watcher):
|
||||
self.assertEqual(hide_in_game.main([]), 0)
|
||||
watcher.run.assert_called_once_with()
|
||||
watcher.restore.assert_called_once_with()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Resolve the client-in-game mode from League's local preferences.
|
||||
|
||||
The public launcher switch is RCLOL_CLIENT_IN_GAME_TRICKS=1. It reads
|
||||
`lol-user-experience` from LCULocalPreferences.yaml:
|
||||
|
||||
- potatoModeEnabled must be true (client optimizations enabled);
|
||||
- closeLeagueClientDuringGame true -> close the client UI while a match runs;
|
||||
- closeLeagueClientDuringGame false -> hide the client windows instead;
|
||||
- missing/unreadable preferences -> no action (none).
|
||||
|
||||
This helper only selects a mode. It does not change preferences or files.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CLOSE_KEY = 'closeLeagueClientDuringGame'
|
||||
OPTIMIZATION_KEY = 'potatoModeEnabled'
|
||||
TRUE_VALUES = {'true', 'yes', '1'}
|
||||
FALSE_VALUES = {'false', 'no', '0'}
|
||||
|
||||
|
||||
def read_bool(path, key):
|
||||
"""Return True/False for the last value, or None when absent/unreadable."""
|
||||
try:
|
||||
lines = Path(path).read_text().splitlines()
|
||||
except (OSError, UnicodeError):
|
||||
return None
|
||||
value = None
|
||||
for line in lines:
|
||||
match = re.match(r'^\s*' + re.escape(key) + r':\s*(\S+)', line)
|
||||
if match:
|
||||
value = match.group(1).strip('"\'').lower()
|
||||
if value in TRUE_VALUES:
|
||||
return True
|
||||
if value in FALSE_VALUES:
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def mode(path):
|
||||
if read_bool(path, OPTIMIZATION_KEY) is not True:
|
||||
return 'none'
|
||||
return 'close' if read_bool(path, CLOSE_KEY) is True else 'hide'
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
argv = sys.argv[1:] if argv is None else argv
|
||||
path = argv[0] if argv else ''
|
||||
print(mode(path))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Opt-in: hide the League client UI while a match is running.
|
||||
|
||||
`RCLOL_HIDE_CLIENT_IN_GAME=1` is an alternative to closing the client UI with
|
||||
`RCLOL_CLOSE_CLIENT_IN_GAME=1`. It hides the managed X11 windows for
|
||||
`LeagueClientUx` and `Riot Client`, and shows them again when the game process
|
||||
exits. The Riot client, League backend, CEF helpers and game keep running.
|
||||
The launcher's `RCLOL_CLIENT_IN_GAME_TRICKS=1` mode selects this helper when
|
||||
the client's own close-during-game preference is false. The low-level
|
||||
`RCLOL_HIDE_CLIENT_IN_GAME=1` switch remains available for direct launches. It
|
||||
hides the managed X11 windows for `LeagueClientUx` and `Riot Client`, and shows
|
||||
them again when the game process exits. The Riot client, League backend, CEF
|
||||
helpers and game keep running.
|
||||
|
||||
This is launcher/window management only. It does not patch client binaries,
|
||||
CEF, anti-cheat or DRM, and it never hides the game window. It is off by
|
||||
@@ -332,13 +334,14 @@ def main(argv=None):
|
||||
help='one game observation and exit (no window changes)')
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if not args.once and os.environ.get('RCLOL_CLOSE_CLIENT_IN_GAME') == '1':
|
||||
tricks = os.environ.get('RCLOL_CLIENT_IN_GAME_TRICKS') == '1'
|
||||
if not args.once and not tricks and os.environ.get('RCLOL_CLOSE_CLIENT_IN_GAME') == '1':
|
||||
print('hide-league-client-in-game: ignored; RCLOL_CLOSE_CLIENT_IN_GAME=1 takes precedence',
|
||||
file=sys.stderr, flush=True)
|
||||
return 0
|
||||
if not args.once and os.environ.get('RCLOL_HIDE_CLIENT_IN_GAME') != '1':
|
||||
print('hide-league-client-in-game: disabled; set RCLOL_HIDE_CLIENT_IN_GAME=1',
|
||||
flush=True)
|
||||
if not args.once and not tricks and os.environ.get('RCLOL_HIDE_CLIENT_IN_GAME') != '1':
|
||||
print('hide-league-client-in-game: disabled; set RCLOL_HIDE_CLIENT_IN_GAME=1 or '
|
||||
'RCLOL_CLIENT_IN_GAME_TRICKS=1', flush=True)
|
||||
return 0
|
||||
prefix = target_prefix()
|
||||
if prefix is None:
|
||||
|
||||
+23
-16
@@ -12,23 +12,30 @@ runner_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
if [ "${RCLOL_RECOVER_HIDDEN_CLIENT:-0}" = 1 ]; then
|
||||
python3 "$runner_dir/recover-hidden-riot-client.py"
|
||||
fi
|
||||
# Opt-in: close the League client UI while a match runs. It keeps the CPU for the game
|
||||
# and leaves the client the backend starts when the match ends as the only CEF instance;
|
||||
# a second instance on the same profile never paints its window, which is the black
|
||||
# window seen with Riot's own "Close client during game" option. Off by default.
|
||||
if [ "${RCLOL_CLOSE_CLIENT_IN_GAME:-0}" = 1 ]; then
|
||||
# Opt-in automatic client-in-game handling. This single switch reads the
|
||||
# client's own lol-user-experience preferences: close the UI when the user
|
||||
# selected "close client during game", otherwise hide the client windows.
|
||||
# It does nothing unless potatoModeEnabled (client optimizations) is true.
|
||||
if [ "${RCLOL_CLIENT_IN_GAME_TRICKS:-0}" = 1 ]; then
|
||||
pref="$prefix_dir/Applications/League of Legends.app/Contents/LoL/Config/LCULocalPreferences.yaml"
|
||||
mode=$(python3 "$runner_dir/client-in-game-tricks.py" "$pref" 2>/dev/null || echo none)
|
||||
case "$mode" in
|
||||
close)
|
||||
echo 'client-in-game-tricks: closeLeagueClientDuringGame=true -> close helper' >&2
|
||||
python3 "$runner_dir/close-league-client-in-game.py" --detach &
|
||||
;;
|
||||
hide)
|
||||
echo 'client-in-game-tricks: closeLeagueClientDuringGame=false -> hide helper' >&2
|
||||
python3 "$runner_dir/hide-league-client-in-game.py" --detach &
|
||||
;;
|
||||
*)
|
||||
echo 'client-in-game-tricks: no action (client optimizations disabled or preferences unreadable)' >&2
|
||||
;;
|
||||
esac
|
||||
elif [ "${RCLOL_CLOSE_CLIENT_IN_GAME:-0}" = 1 ]; then
|
||||
python3 "$runner_dir/close-league-client-in-game.py" --detach &
|
||||
fi
|
||||
# Opt-in alternative for users who keep the client open during a match: hide
|
||||
# the managed client windows instead of closing the UI. It never touches the
|
||||
# League backend, the Riot client or the game window; it restores the window
|
||||
# when the game process ends. Close and hide are mutually exclusive.
|
||||
if [ "${RCLOL_HIDE_CLIENT_IN_GAME:-0}" = 1 ]; then
|
||||
if [ "${RCLOL_CLOSE_CLIENT_IN_GAME:-0}" = 1 ]; then
|
||||
echo 'RCLOL_HIDE_CLIENT_IN_GAME ignored: RCLOL_CLOSE_CLIENT_IN_GAME=1 takes precedence' >&2
|
||||
else
|
||||
python3 "$runner_dir/hide-league-client-in-game.py" --detach &
|
||||
fi
|
||||
elif [ "${RCLOL_HIDE_CLIENT_IN_GAME:-0}" = 1 ]; then
|
||||
python3 "$runner_dir/hide-league-client-in-game.py" --detach &
|
||||
fi
|
||||
# The bundled downloader rejects IPv6 CONNECT_TO addresses. Only Riot CDN
|
||||
# AAAA lookups are suppressed; set this to 0 to retain the upstream resolver.
|
||||
|
||||
Reference in new issue
Block a user