mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
fix(launcher): address in-game helper review
- restore hidden windows even when the launcher output pipe is closed; - exit the hide watcher after ten idle minutes and start it only after the installed-client check; - reject missing, malformed or invalid close-during-game preferences instead of silently selecting hide; - document the preference contract and idle behavior.
This commit is contained in:
1 parent
732e9c8881
commit
f8bb14624e
8 files changed
+176
-67
No files matched your search
@@ -117,11 +117,13 @@ RCLOL_CLIENT_IN_GAME_TRICKS=1 ./scripts/run.sh
|
||||
|
||||
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
|
||||
client UI if `closeLeagueClientDuringGame` is true, and hides the
|
||||
`LeagueClientUx`/`Riot Client` X11 windows when that value is false. Missing or
|
||||
invalid preference values, disabled client optimizations and unreadable files
|
||||
select no action. The `RCLOL_CLOSE_CLIENT_IN_GAME` and
|
||||
`RCLOL_HIDE_CLIENT_IN_GAME` low-level switches remain available when the
|
||||
automatic switch is not set. The helper exits after ten minutes without a
|
||||
client or match once it has seen one. See
|
||||
[troubleshooting](docs/TROUBLESHOOTING.md#automatic-client-handling-during-a-match)
|
||||
for the measured effect and caveats.
|
||||
|
||||
|
||||
+6
-5
@@ -81,13 +81,14 @@ for the observed failures, recovery guards and NSURLSession limitations.
|
||||
`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
|
||||
UI when `closeLeagueClientDuringGame: true`, and hides the managed
|
||||
`LeagueClientUx`/`Riot Client` X11 windows when that value is false. Missing or
|
||||
invalid preference values, disabled client optimizations and unreadable files
|
||||
select no action. 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
|
||||
does not patch client binaries, CEF or the game. The hide helper exits after ten
|
||||
minutes without a client or match once it has seen one. See
|
||||
[the troubleshooting measurement](TROUBLESHOOTING.md#automatic-client-handling-during-a-match)
|
||||
for the measured CPU effect and limitations.
|
||||
|
||||
|
||||
@@ -206,13 +206,16 @@ It reads `lol-user-experience` from the installed client's
|
||||
| Client preference | Launcher action |
|
||||
|---|---|
|
||||
| `potatoModeEnabled: false` or unreadable | no client-in-game helper |
|
||||
| `potatoModeEnabled: true`, close preference missing or invalid | no action |
|
||||
| `potatoModeEnabled: true`, `closeLeagueClientDuringGame: true` | close the client UI |
|
||||
| `potatoModeEnabled: true`, `closeLeagueClientDuringGame: false` | hide the client windows |
|
||||
|
||||
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.
|
||||
close path uses the UI-close helper described above. The hide helper exits after
|
||||
ten minutes without a client or match once it has seen one; a missing or invalid
|
||||
close preference selects no helper.
|
||||
|
||||
The low-level `RCLOL_HIDE_CLIENT_IN_GAME=1` and
|
||||
`RCLOL_CLOSE_CLIENT_IN_GAME=1` switches remain available when the automatic
|
||||
|
||||
@@ -36,9 +36,12 @@ class ClientInGameTricksTests(unittest.TestCase):
|
||||
self.write(True, False)
|
||||
self.assertEqual(tricks.mode(self.path), 'hide')
|
||||
|
||||
def test_hide_when_close_preference_is_missing(self):
|
||||
def test_none_when_close_preference_is_missing_or_invalid(self):
|
||||
self.write(True)
|
||||
self.assertEqual(tricks.mode(self.path), 'hide')
|
||||
self.assertEqual(tricks.mode(self.path), 'none')
|
||||
for value in ('maybe', '"true', 'true#note', '"true"junk'):
|
||||
self.write(True, value)
|
||||
self.assertEqual(tricks.mode(self.path), 'none', value)
|
||||
|
||||
def test_none_when_optimizations_are_off(self):
|
||||
self.write(False, True)
|
||||
@@ -64,6 +67,7 @@ class ClientInGameTricksTests(unittest.TestCase):
|
||||
'lol-user-experience:\n'
|
||||
' data:\n'
|
||||
' potatoModeEnabled: true\n'
|
||||
' closeLeagueClientDuringGame: false\n'
|
||||
'other-section:\n'
|
||||
' potatoModeEnabled: true\n'
|
||||
' closeLeagueClientDuringGame: true\n')
|
||||
|
||||
@@ -217,6 +217,46 @@ class HideClientInGameTests(unittest.TestCase):
|
||||
watcher._hide_new()
|
||||
self.assertEqual(watcher.hidden, {0xabc})
|
||||
|
||||
def test_restore_log_failure_still_shows_the_window(self):
|
||||
class BrokenOutput:
|
||||
def write(self, _text):
|
||||
raise BrokenPipeError
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
x11 = FakeX11()
|
||||
watcher = hide_in_game.Watcher(
|
||||
self.prefix, x11,
|
||||
window_source=lambda _prefix, _cache: [0xabc],
|
||||
state_source=lambda _prefix: True,
|
||||
sleep=lambda _seconds: None,
|
||||
out=BrokenOutput(),
|
||||
)
|
||||
watcher.hidden = {0xabc}
|
||||
watcher._show_all('test')
|
||||
self.assertEqual(x11.shown, [0xabc])
|
||||
self.assertEqual(watcher.hidden, set())
|
||||
|
||||
def test_idle_timeout_leaves_after_client_disappears(self):
|
||||
states = iter([True, False, False])
|
||||
windows = iter([[0xabc], [], []])
|
||||
times = iter([0.0, 10.0, 700.0])
|
||||
x11 = FakeX11()
|
||||
output = io.StringIO()
|
||||
watcher = hide_in_game.Watcher(
|
||||
self.prefix, x11,
|
||||
window_source=lambda _prefix, _cache: next(windows),
|
||||
state_source=lambda _prefix: next(states),
|
||||
sleep=lambda _seconds: None,
|
||||
out=output,
|
||||
clock=lambda: next(times),
|
||||
)
|
||||
self.assertEqual(watcher.run(max_steps=3), 0)
|
||||
self.assertEqual(x11.unmapped, [0xabc])
|
||||
self.assertEqual(x11.shown, [0xabc])
|
||||
self.assertIn('no client or match', output.getvalue())
|
||||
|
||||
def test_hides_then_shows_on_match_end(self):
|
||||
states = iter([True, True, False])
|
||||
x11 = FakeX11()
|
||||
@@ -278,6 +318,11 @@ class HideClientInGameTests(unittest.TestCase):
|
||||
}):
|
||||
self.assertEqual(hide_in_game.main([]), 0)
|
||||
|
||||
def test_helper_starts_after_the_installed_client_check(self):
|
||||
source = (ROOT / 'tools/run-riot-client.sh').read_text()
|
||||
self.assertLess(source.index('Riot Client is not installed in'),
|
||||
source.index('RCLOL_CLIENT_IN_GAME_TRICKS'))
|
||||
|
||||
def test_master_tricks_switch_enables_hide(self):
|
||||
class FakeX11:
|
||||
def close(self):
|
||||
|
||||
@@ -7,7 +7,8 @@ The public launcher switch is RCLOL_CLIENT_IN_GAME_TRICKS=1. It reads
|
||||
- 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 preferences or an unreadable file -> no action (none).
|
||||
- missing or invalid closeLeagueClientDuringGame or potatoModeEnabled -> no action
|
||||
(none).
|
||||
|
||||
Only the named `lol-user-experience` section is considered, so identical keys
|
||||
in another section cannot select a helper. This helper only selects a mode; it
|
||||
@@ -24,8 +25,39 @@ TRUE_VALUES = {'true', 'yes', '1'}
|
||||
FALSE_VALUES = {'false', 'no', '0'}
|
||||
|
||||
|
||||
def parse_bool(value):
|
||||
"""Return True/False/None for one YAML scalar, ignoring a trailing comment."""
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return None
|
||||
if value[0] in ('"', "'"):
|
||||
quote = value[0]
|
||||
end = value.find(quote, 1)
|
||||
if end == -1:
|
||||
return None
|
||||
scalar = value[1:end]
|
||||
remainder = value[end + 1:]
|
||||
if remainder and not remainder[0].isspace():
|
||||
return None
|
||||
remainder = remainder.strip()
|
||||
if remainder and not remainder.startswith('#'):
|
||||
return None
|
||||
else:
|
||||
scalar = value
|
||||
for index, char in enumerate(scalar):
|
||||
if char == '#' and (index == 0 or scalar[index - 1].isspace()):
|
||||
scalar = scalar[:index].rstrip()
|
||||
break
|
||||
scalar = scalar.lower()
|
||||
if scalar in TRUE_VALUES:
|
||||
return True
|
||||
if scalar in FALSE_VALUES:
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def read_bool(path, key, section=SECTION):
|
||||
"""Return True/False for the last key in section, or None when absent.
|
||||
"""Return True/False for the last key in section, or None when invalid.
|
||||
|
||||
OSError/UnicodeError is raised for an unreadable file so callers can fail
|
||||
closed instead of treating a read race as a missing preference.
|
||||
@@ -45,19 +77,8 @@ def read_bool(path, key, section=SECTION):
|
||||
continue
|
||||
if not in_section or not stripped.startswith(key + ':'):
|
||||
continue
|
||||
value = stripped.split(':', 1)[1].strip()
|
||||
if value[:1] in ('"', "'"):
|
||||
quote = value[0]
|
||||
end = value.find(quote, 1)
|
||||
value = value[1:end] if end != -1 else value[1:]
|
||||
else:
|
||||
value = value.split('#', 1)[0].strip()
|
||||
value = value.lower()
|
||||
if value in TRUE_VALUES:
|
||||
return True
|
||||
if value in FALSE_VALUES:
|
||||
return False
|
||||
return None
|
||||
value = parse_bool(stripped.split(':', 1)[1])
|
||||
return value
|
||||
|
||||
|
||||
def mode(path):
|
||||
@@ -71,7 +92,11 @@ def mode(path):
|
||||
close = read_bool(path, CLOSE_KEY)
|
||||
except (OSError, UnicodeError):
|
||||
return 'none'
|
||||
return 'close' if close is True else 'hide'
|
||||
if close is True:
|
||||
return 'close'
|
||||
if close is False:
|
||||
return 'hide'
|
||||
return 'none'
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
|
||||
@@ -39,6 +39,7 @@ GAME_NAME = 'LeagueofLegends'
|
||||
SOCK_VARIABLE = b'__mldr_sockpath'
|
||||
SOCK_NAME = '.darlingserver.sock'
|
||||
POLL_SECONDS = 2.0
|
||||
IDLE_SECONDS = 600
|
||||
TARGET_CLASSES = ('leagueclientux', 'riot client')
|
||||
|
||||
|
||||
@@ -263,35 +264,46 @@ class Watcher:
|
||||
"""Hide while a match is running, restore on end, uncertainty or exit."""
|
||||
|
||||
def __init__(self, prefix, x11, window_source=target_windows,
|
||||
state_source=game_running, sleep=time.sleep, out=sys.stdout):
|
||||
state_source=game_running, sleep=time.sleep, out=sys.stdout,
|
||||
clock=time.monotonic):
|
||||
self.prefix = prefix
|
||||
self.x11 = x11
|
||||
self.window_source = window_source
|
||||
self.state_source = state_source
|
||||
self.sleep = sleep
|
||||
self.out = out
|
||||
self.clock = clock
|
||||
self.hidden = set()
|
||||
self.class_cache = {}
|
||||
self.reported_no_window = False
|
||||
self.seen = False
|
||||
self.idle_since = None
|
||||
|
||||
def _log(self, message, stream=None):
|
||||
try:
|
||||
print(message, file=self.out if stream is None else stream, flush=True)
|
||||
except Exception:
|
||||
# A closed launcher pipe must not prevent window restoration.
|
||||
pass
|
||||
|
||||
def _show_all(self, reason):
|
||||
if not self.hidden:
|
||||
return
|
||||
print(f'hide-league-client-in-game: {reason}; showing the client', file=self.out, flush=True)
|
||||
self._log(f'hide-league-client-in-game: {reason}; showing the client')
|
||||
for window in sorted(self.hidden):
|
||||
try:
|
||||
self.x11.show(window)
|
||||
print(f'hide-league-client-in-game: shown 0x{window:x}', file=self.out, flush=True)
|
||||
except Exception as error:
|
||||
print(f'hide-league-client-in-game: cannot show 0x{window:x}: {error}',
|
||||
file=sys.stderr, flush=True)
|
||||
self._log(f'hide-league-client-in-game: cannot show 0x{window:x}: {error}',
|
||||
sys.stderr)
|
||||
continue
|
||||
self._log(f'hide-league-client-in-game: shown 0x{window:x}')
|
||||
self.hidden.clear()
|
||||
|
||||
def _hide_new(self):
|
||||
windows = self.window_source(self.prefix, self.class_cache)
|
||||
if not windows and not self.hidden and not self.reported_no_window:
|
||||
print('hide-league-client-in-game: no managed client window; not hiding',
|
||||
file=self.out, flush=True)
|
||||
self._log('hide-league-client-in-game: no managed client window; not hiding')
|
||||
self.reported_no_window = True
|
||||
for window in windows:
|
||||
if window in self.hidden:
|
||||
@@ -301,29 +313,44 @@ class Watcher:
|
||||
self.x11.unmap(window)
|
||||
except Exception as error:
|
||||
self.hidden.discard(window)
|
||||
print(f'hide-league-client-in-game: cannot hide 0x{window:x}: {error}',
|
||||
file=sys.stderr, flush=True)
|
||||
self._log(f'hide-league-client-in-game: cannot hide 0x{window:x}: {error}',
|
||||
sys.stderr)
|
||||
continue
|
||||
self._log(f'hide-league-client-in-game: hidden 0x{window:x}')
|
||||
|
||||
def _windows_present(self):
|
||||
try:
|
||||
print(f'hide-league-client-in-game: hidden 0x{window:x}',
|
||||
file=self.out, flush=True)
|
||||
return bool(self.window_source(self.prefix, self.class_cache))
|
||||
except Exception:
|
||||
# Logging must not lose a successfully unmapped window.
|
||||
pass
|
||||
# Uncertainty must not turn into an automatic exit.
|
||||
return True
|
||||
|
||||
def run(self, max_steps=None):
|
||||
steps = 0
|
||||
while True:
|
||||
now = self.clock()
|
||||
state = self.state_source(self.prefix)
|
||||
if state is None:
|
||||
self._show_all('unreadable /proc state')
|
||||
elif state:
|
||||
self.seen = True
|
||||
self.idle_since = None
|
||||
if not self.hidden and self.reported_no_window:
|
||||
self.reported_no_window = False
|
||||
self._hide_new()
|
||||
else:
|
||||
self._show_all('the match ended')
|
||||
self.reported_no_window = False
|
||||
if self._windows_present():
|
||||
self.seen = True
|
||||
self.idle_since = None
|
||||
elif self.seen:
|
||||
if self.idle_since is None:
|
||||
self.idle_since = now
|
||||
if now - self.idle_since >= IDLE_SECONDS:
|
||||
self._log('hide-league-client-in-game: no client or match for '
|
||||
f'{IDLE_SECONDS}s: leaving')
|
||||
return 0
|
||||
steps += 1
|
||||
if max_steps is not None and steps >= max_steps:
|
||||
return 0
|
||||
|
||||
+27
-25
@@ -12,31 +12,6 @@ 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 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 &
|
||||
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.
|
||||
export RCLOL_CDN_IPV4_ONLY="${RCLOL_CDN_IPV4_ONLY:-1}"
|
||||
@@ -84,6 +59,33 @@ if [ -z "${RIOT_LAUNCHER_HOST:-}" ] && [ ! -e "$client" ]; then
|
||||
ln -s "../Users/Shared/Riot Games/Riot Client.app" "$client"
|
||||
fi
|
||||
fi
|
||||
# Opt-in automatic client-in-game handling. Start the watcher only after the
|
||||
# installed-client check above: a failed installer launch must not leave a
|
||||
# helper behind. The preference selector returns no action for missing or
|
||||
# invalid close-during-game values.
|
||||
if [ -z "${RIOT_LAUNCHER_HOST:-}" ]; then
|
||||
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 missing/invalid)' >&2
|
||||
;;
|
||||
esac
|
||||
elif [ "${RCLOL_CLOSE_CLIENT_IN_GAME:-0}" = 1 ]; then
|
||||
python3 "$runner_dir/close-league-client-in-game.py" --detach &
|
||||
elif [ "${RCLOL_HIDE_CLIENT_IN_GAME:-0}" = 1 ]; then
|
||||
python3 "$runner_dir/hide-league-client-in-game.py" --detach &
|
||||
fi
|
||||
fi
|
||||
A=${RIOT_LAUNCHER_HOST:-"$client/Contents/MacOS/RiotClientServices"}
|
||||
# External XIM (for example Fcitx) consumes keys before Darling forwards them.
|
||||
# Use Xlib's local input method; allow explicit testing of another modifier.
|
||||
|
||||
Reference in new issue
Block a user