mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
Merge main into fix/recover-stale-client
Resolve conflicts with #122: keep both feature docs and CI steps, and drop this branch's copy of the close-helper block, which main now starts after the installed-client check. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
a11a144767
12 files changed
+1367
-14
No files matched your search
@@ -16,6 +16,10 @@ jobs:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Require C++ regression compiler
|
||||
run: |
|
||||
command -v c++
|
||||
c++ --version
|
||||
- run: python scripts/validate.py
|
||||
- run: python tools/riot-input-offline-check.py
|
||||
- run: python tools/riot-launcher-offline-check.py
|
||||
@@ -29,7 +33,10 @@ jobs:
|
||||
- run: python -m unittest discover -s tests -p 'test_urlsession_runner.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_system_profiler.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_metal_device_info.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_indium_address_shadow.py' -v
|
||||
- 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_recover_stale_client.py'
|
||||
- run: python -m unittest discover -s tests -p 'test_clean_stale_helpers.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'
|
||||
@@ -134,6 +134,24 @@ launcher that started them has exited). A helper of a live launcher, another
|
||||
prefix, the game, Riot/League/CEF, `darlingserver` and the runtime are never
|
||||
touched. See [troubleshooting](docs/TROUBLESHOOTING.md#multiple-client-in-game-helpers).
|
||||
|
||||
For automatic client handling during a match, opt in with one switch:
|
||||
|
||||
```sh
|
||||
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, 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.
|
||||
|
||||
## Testing
|
||||
|
||||
Start with standalone regression checks, then play a match manually. See [testing](docs/TESTING.md). Do not attach a debugger, query client APIs, inject diagnostic hooks or automate client/game input. This project ships no client-side automation, inspection or profiling helpers.
|
||||
|
||||
@@ -96,6 +96,20 @@ helper of a live launcher, another prefix, the game, Riot/League/CEF and
|
||||
environment, stat entry or parent cannot be read. See
|
||||
[troubleshooting](TROUBLESHOOTING.md#multiple-client-in-game-helpers).
|
||||
|
||||
`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`, 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. 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.
|
||||
|
||||
`RCLOL_FILTER_HOST_ENV=1` opts in to removing host desktop-session variables before
|
||||
the guest starts: `WAYLAND_DISPLAY`, `XDG_SESSION_*`, `XDG_CURRENT_DESKTOP`,
|
||||
GNOME/KDE/GDM, systemd, SSH agent, terminal, toolkit (`QT_*`, `GTK_*`, `GDK_*`),
|
||||
|
||||
@@ -227,6 +227,60 @@ observation, without waiting for a match:
|
||||
python3 tools/close-league-client-in-game.py --once --dry-run
|
||||
```
|
||||
|
||||
### Automatic client handling during a match
|
||||
|
||||
A single opt-in switch reads the client's own preferences and chooses the mode:
|
||||
|
||||
```sh
|
||||
RCLOL_CLIENT_IN_GAME_TRICKS=1 ./scripts/run.sh
|
||||
```
|
||||
|
||||
It reads `lol-user-experience` from the installed client's
|
||||
`LCULocalPreferences.yaml`:
|
||||
|
||||
| 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. 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
|
||||
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
|
||||
window returned it to 119.9%. This is not a universal FPS or percentage claim:
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```sh
|
||||
python3 tools/hide-league-client-in-game.py --once
|
||||
```
|
||||
|
||||
### Riot reports NO INTERNET FOUND while the host is online
|
||||
|
||||
Older base runtimes ship Google DNS servers (`8.8.8.8` and `8.8.4.4`), which
|
||||
|
||||
@@ -289,7 +289,7 @@ index b7ffcce..5b1e84c 100644
|
||||
};
|
||||
};
|
||||
diff --git a/private-include/indium/command-encoder.private.hpp b/private-include/indium/command-encoder.private.hpp
|
||||
index c3b1b93..3dab2ce 100644
|
||||
index c3b1b93..6b8783d 100644
|
||||
--- a/private-include/indium/command-encoder.private.hpp
|
||||
+++ b/private-include/indium/command-encoder.private.hpp
|
||||
@@ -1,6 +1,7 @@
|
||||
@@ -300,7 +300,7 @@ index c3b1b93..3dab2ce 100644
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <array>
|
||||
@@ -17,117 +18,295 @@
|
||||
@@ -17,117 +18,330 @@
|
||||
|
||||
#include <iridium/iridium.hpp>
|
||||
|
||||
@@ -529,6 +529,40 @@ index c3b1b93..3dab2ce 100644
|
||||
+ struct AddressPool {
|
||||
+ std::shared_ptr<Buffer> chunk;
|
||||
+ size_t used = 0;
|
||||
+
|
||||
+ // Two shader stages are live in the render encoder. Cache their small
|
||||
+ // immutable tables in CPU memory, not in the GPU-mapped scratch chunk.
|
||||
+ // This is only a read cache: misses retain the original byte comparison.
|
||||
+ struct Shadow {
|
||||
+ bool valid = false;
|
||||
+ uint32_t offset;
|
||||
+ VkDeviceSize range;
|
||||
+ size_t count;
|
||||
+ std::array<uint64_t, 64> addresses;
|
||||
+ };
|
||||
+ std::array<Shadow, 2> shadows;
|
||||
+ size_t nextShadow = 0;
|
||||
+
|
||||
+ bool sameAddresses(uint32_t offset, VkDeviceSize range, const uint64_t* addresses, size_t count) const {
|
||||
+ for (const auto& shadow : shadows) {
|
||||
+ // A table's last eight bytes can be padding. If a new layout
|
||||
+ // reads that padding, only the mapped bytes define the old key.
|
||||
+ if (shadow.valid && shadow.offset == offset && shadow.range == range && count <= shadow.count)
|
||||
+ return !memcmp(shadow.addresses.data(), addresses, count * sizeof(uint64_t));
|
||||
+ }
|
||||
+ return !memcmp(static_cast<const char*>(chunk->contents()) + offset, addresses, count * sizeof(uint64_t));
|
||||
+ }
|
||||
+
|
||||
+ void rememberAddresses(uint32_t offset, VkDeviceSize range, const uint64_t* addresses, size_t count) {
|
||||
+ if (count > 64) return;
|
||||
+ auto& shadow = shadows[nextShadow];
|
||||
+ nextShadow = (nextShadow + 1) % shadows.size();
|
||||
+ shadow.offset = offset;
|
||||
+ shadow.range = range;
|
||||
+ shadow.count = count;
|
||||
+ memcpy(shadow.addresses.data(), addresses, count * sizeof(uint64_t));
|
||||
+ shadow.valid = true;
|
||||
+ }
|
||||
+ };
|
||||
|
||||
- if (functionResources.buffers.size() > 0) {
|
||||
@@ -583,17 +617,18 @@ index c3b1b93..3dab2ce 100644
|
||||
+ if (bindingReuseEnabled() && previous && addressPool.chunk && previous->range == tableSize &&
|
||||
+ previous->offset <= chunkSize && tableSize <= chunkSize - previous->offset &&
|
||||
+ previous->buffer == static_cast<PrivateBuffer*>(addressPool.chunk.get())->buffer() &&
|
||||
+ !memcmp(static_cast<const char*>(addressPool.chunk->contents()) + previous->offset,
|
||||
+ addresses.data(), addresses.size() * sizeof(uint64_t)))
|
||||
+ addressPool.sameAddresses(previous->offset, previous->range, addresses.data(), addresses.size()))
|
||||
+ return *previous;
|
||||
+ size_t tableOffset = (addressPool.used + alignment - 1) & ~(alignment - 1);
|
||||
+ if (!addressPool.chunk || tableOffset + tableSize > chunkSize) {
|
||||
+ addressPool.chunk = privateDevice->acquireScratchChunk();
|
||||
+ for (auto& shadow : addressPool.shadows) shadow.valid = false;
|
||||
+ // we need to keep the chunk alive until the operation is completed
|
||||
+ keepAliveBuffers.push_back(addressPool.chunk);
|
||||
+ tableOffset = 0;
|
||||
+ }
|
||||
+ memcpy(static_cast<char*>(addressPool.chunk->contents()) + tableOffset, addresses.data(), addresses.size() * sizeof(uint64_t));
|
||||
+ if (previous) addressPool.rememberAddresses(static_cast<uint32_t>(tableOffset), tableSize, addresses.data(), addresses.size());
|
||||
+ addressPool.used = tableOffset + tableSize;
|
||||
+ // plain pointer: a shared_ptr cast here was an atomic reference count round trip per stage per draw
|
||||
+ auto* privateAddressBuffer = static_cast<PrivateBuffer*>(addressPool.chunk.get());
|
||||
@@ -654,7 +689,7 @@ index c3b1b93..3dab2ce 100644
|
||||
descSet.descriptorCount = 1;
|
||||
descSet.pBufferInfo = &info;
|
||||
}
|
||||
@@ -136,17 +315,27 @@ namespace Indium {
|
||||
@@ -136,17 +350,27 @@ namespace Indium {
|
||||
auto& bindingInfo = funcInfo.bindings[j];
|
||||
|
||||
if (bindingInfo.type == Iridium::BindingType::Texture) {
|
||||
@@ -691,7 +726,7 @@ index c3b1b93..3dab2ce 100644
|
||||
auto& descSet = writeDescSet.emplace_back();
|
||||
descSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
descSet.dstSet = descriptorSets[i];
|
||||
@@ -156,19 +345,21 @@ namespace Indium {
|
||||
@@ -156,19 +380,21 @@ namespace Indium {
|
||||
descSet.descriptorCount = 1;
|
||||
descSet.pImageInfo = &info;
|
||||
} else if (bindingInfo.type == Iridium::BindingType::Sampler) {
|
||||
@@ -724,7 +759,7 @@ index c3b1b93..3dab2ce 100644
|
||||
info.sampler = privateSampler->sampler();
|
||||
|
||||
auto& descSet = writeDescSet.emplace_back();
|
||||
@@ -188,10 +379,4 @@ namespace Indium {
|
||||
@@ -188,10 +414,4 @@ namespace Indium {
|
||||
return descriptorSets;
|
||||
};
|
||||
|
||||
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
// CPU-only buffer/Vulkan shims; the scratch, pool and table helper are extracted
|
||||
// from the authoritative component patch, not copied into this fixture.
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <vector>
|
||||
static size_t allocationCalls = 0;
|
||||
void* operator new(size_t size) {
|
||||
++allocationCalls;
|
||||
if (auto* p = std::malloc(size ? size : 1)) return p;
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
void operator delete(void* p) noexcept { std::free(p); }
|
||||
void operator delete(void* p, size_t) noexcept { std::free(p); }
|
||||
using VkBuffer = uint64_t;
|
||||
using VkDeviceSize = uint64_t;
|
||||
constexpr VkBuffer VK_NULL_HANDLE = 0;
|
||||
namespace Iridium { enum class BindingType { Buffer, Texture, Sampler }; }
|
||||
static bool enabled = true;
|
||||
bool bindingReuseEnabled() { return enabled; }
|
||||
void logUnboundResource(const char*, size_t, size_t, size_t) {}
|
||||
struct Buffer { virtual ~Buffer() = default; virtual void* contents() = 0; };
|
||||
struct PrivateBuffer : Buffer {
|
||||
uint64_t id, address;
|
||||
size_t reads = 0;
|
||||
std::vector<unsigned char> bytes;
|
||||
PrivateBuffer(uint64_t id, uint64_t address) : id(id), address(address), bytes(256*1024, 0xa5) {}
|
||||
void* contents() override { ++reads; return bytes.data(); }
|
||||
VkBuffer buffer() const { return id; }
|
||||
uint64_t gpuAddress() const { return address; }
|
||||
};
|
||||
struct PrivateDevice {
|
||||
size_t chunks = 0;
|
||||
uint64_t fallback = 0xf00d;
|
||||
uint64_t fallbackBufferAddress() { return fallback; }
|
||||
std::shared_ptr<Buffer> acquireScratchChunk() { ++chunks; return std::make_shared<PrivateBuffer>(chunks, chunks << 20); }
|
||||
};
|
||||
struct FunctionResources { std::vector<std::pair<std::shared_ptr<Buffer>, size_t>> buffers; };
|
||||
struct Resolved { PrivateBuffer* value = nullptr; };
|
||||
struct ResolvedFunctionResources { std::vector<Resolved> buffers; };
|
||||
struct Binding { Iridium::BindingType type; size_t index; };
|
||||
struct FunctionInfo { std::vector<Binding> bindings; };
|
||||
|
||||
/* ACTUAL_PATCHED_HELPER */
|
||||
|
||||
struct Check {
|
||||
AddressPool pool;
|
||||
std::shared_ptr<PrivateDevice> device = std::make_shared<PrivateDevice>();
|
||||
std::vector<std::shared_ptr<Buffer>> keep;
|
||||
std::array<BufferAddressTable, 4> tables{};
|
||||
Check() { keep.reserve(8); }
|
||||
PrivateBuffer* mapped() { return static_cast<PrivateBuffer*>(pool.chunk.get()); }
|
||||
void run(size_t stage, const FunctionResources& resources, const FunctionInfo& info,
|
||||
bool reuse, const ResolvedFunctionResources* resolved = nullptr, bool previous = true) {
|
||||
auto old = tables[stage];
|
||||
auto used = pool.used, chunks = device->chunks, calls = allocationCalls;
|
||||
auto table = makeBufferAddressTable(resources, info, resolved, device, keep, pool,
|
||||
previous ? &tables[stage] : nullptr);
|
||||
// Only the existing DescriptorScratch overflow and new scratch chunk
|
||||
// may allocate (make_shared control/object plus the chunk byte vector).
|
||||
assert(allocationCalls - calls == (info.bindings.size() > 64 ? 1u : 0u) +
|
||||
(device->chunks - chunks) * 2);
|
||||
assert((table.buffer == old.buffer && table.offset == old.offset && pool.used == used) == reuse);
|
||||
assert(table.range == ((std::count_if(info.bindings.begin(), info.bindings.end(),
|
||||
[](const Binding& b) { return b.type == Iridium::BindingType::Buffer; }) * 8 + 15) & ~size_t(15)));
|
||||
assert(keep.size() == device->chunks);
|
||||
size_t index = 0;
|
||||
for (const auto& binding : info.bindings) {
|
||||
if (binding.type != Iridium::BindingType::Buffer) continue;
|
||||
PrivateBuffer* buffer = nullptr;
|
||||
if (resolved && binding.index < resolved->buffers.size()) buffer = resolved->buffers[binding.index].value;
|
||||
else if (binding.index < resources.buffers.size()) buffer = dynamic_cast<PrivateBuffer*>(resources.buffers[binding.index].first.get());
|
||||
uint64_t expected = buffer ? buffer->gpuAddress() + resources.buffers[binding.index].second : device->fallback;
|
||||
uint64_t actual;
|
||||
memcpy(&actual, mapped()->bytes.data() + table.offset + index++ * 8, 8);
|
||||
assert(actual == expected);
|
||||
}
|
||||
tables[stage] = table;
|
||||
}
|
||||
};
|
||||
FunctionInfo info(size_t count, size_t start = 0) {
|
||||
FunctionInfo result;
|
||||
for (size_t i = 0; i < count; ++i) result.bindings.push_back({Iridium::BindingType::Buffer, start+i});
|
||||
return result;
|
||||
}
|
||||
int main() {
|
||||
// Two 64-key entries: +1080 bytes per pool on the tested 64-bit ABI.
|
||||
static_assert(sizeof(AddressPool) == sizeof(std::shared_ptr<Buffer>) + sizeof(size_t) + 1080);
|
||||
FunctionResources r;
|
||||
auto resource = std::make_shared<PrivateBuffer>(100, 0x1000);
|
||||
for (size_t i = 0; i < 150; ++i) r.buffers.push_back({resource, i*16});
|
||||
Check p;
|
||||
auto f = info(3);
|
||||
p.run(0, r, f, false);
|
||||
auto accesses = p.mapped()->reads;
|
||||
p.run(0, r, f, true); assert(p.mapped()->reads == accesses);
|
||||
r.buffers[100].second++; p.run(0, r, f, true);
|
||||
auto equivalent = std::make_shared<PrivateBuffer>(101, 0x0ff0);
|
||||
r.buffers[0] = {equivalent, 16}; p.run(0, r, f, true);
|
||||
r.buffers[1].second++; p.run(0, r, f, false);
|
||||
auto mixed = f;
|
||||
mixed.bindings.push_back({Iridium::BindingType::Texture, 999});
|
||||
mixed.bindings.push_back({Iridium::BindingType::Sampler, 999});
|
||||
p.run(0, r, mixed, true);
|
||||
p.run(1, r, info(4, 20), false);
|
||||
accesses = p.mapped()->reads;
|
||||
p.run(0, r, f, true); p.run(1, r, info(4, 20), true);
|
||||
assert(p.mapped()->reads == accesses); // both interleaved stages hit CPU keys
|
||||
for (size_t i = 0; i < 5; ++i) {
|
||||
r.buffers[20].second++; p.run(1, r, info(4, 20), false);
|
||||
}
|
||||
accesses = p.mapped()->reads;
|
||||
p.run(0, r, f, true);
|
||||
assert(p.mapped()->reads == accesses + 1); // eviction retains mapped fallback
|
||||
|
||||
Check padding;
|
||||
padding.run(0, r, info(1), false);
|
||||
r.buffers[1] = {resource, uint64_t(0xa5a5a5a5a5a5a5a5ULL - resource->gpuAddress())};
|
||||
accesses = padding.mapped()->reads;
|
||||
padding.run(0, r, info(2), true); // odd -> even reads real padding
|
||||
assert(padding.mapped()->reads == accesses + 1);
|
||||
r.buffers[1].second++; padding.run(0, r, info(2), false);
|
||||
accesses = padding.mapped()->reads;
|
||||
padding.run(0, r, info(1), true); // even -> odd needs only cached prefix
|
||||
assert(padding.mapped()->reads == accesses);
|
||||
padding.run(0, r, info(3), false);
|
||||
|
||||
Check large;
|
||||
for (size_t count : {size_t(64), size_t(65), size_t(150), size_t(32767), size_t(32768)}) {
|
||||
large.run(0, r, info(count), false);
|
||||
large.run(0, r, info(count), true);
|
||||
}
|
||||
r.buffers[64].second++; large.run(0, r, info(65), false);
|
||||
large.run(1, r, info(8), false, nullptr, false);
|
||||
large.run(1, r, info(8), true);
|
||||
|
||||
Check missing;
|
||||
FunctionResources empty;
|
||||
missing.run(0, empty, info(2, 999), false);
|
||||
missing.run(0, empty, info(2, 999), true);
|
||||
missing.device->fallback++; missing.run(0, empty, info(2, 999), false);
|
||||
ResolvedFunctionResources resolved;
|
||||
resolved.buffers.resize(r.buffers.size()); resolved.buffers[0].value = resource.get();
|
||||
missing.run(1, r, info(1), false, &resolved);
|
||||
missing.run(1, r, info(1), true, &resolved);
|
||||
resolved.buffers[0].value = nullptr;
|
||||
missing.run(1, r, info(1), false, &resolved);
|
||||
|
||||
Check roll;
|
||||
roll.run(0, r, info(2), false); roll.run(1, r, info(2, 3), false);
|
||||
auto oldChunk = roll.pool.chunk;
|
||||
roll.pool.used = 256*1024;
|
||||
r.buffers[0].second++; roll.run(0, r, info(2), false);
|
||||
assert(roll.pool.chunk != oldChunk && roll.keep.size() == 2);
|
||||
// Old offset 256 must not supply a stale key for untouched new-chunk bytes.
|
||||
roll.tables[1] = {roll.tables[0].buffer, 256, 16};
|
||||
roll.run(1, r, info(2, 3), false); roll.run(1, r, info(2, 3), true);
|
||||
enabled = false; roll.run(1, r, info(2, 3), false); enabled = true;
|
||||
|
||||
Check guard;
|
||||
guard.run(0, r, info(0), false); guard.run(0, r, info(0), true);
|
||||
auto id = guard.tables[0].buffer;
|
||||
guard.tables[0] = {id, 256*1024, 0}; guard.run(0, r, info(0), true);
|
||||
guard.tables[0] = {id, 256*1024+1, 0}; guard.run(0, r, info(0), false);
|
||||
accesses = guard.mapped()->reads;
|
||||
guard.tables[0] = {id, 256*1024-8, 16}; guard.run(0, r, info(1), false);
|
||||
assert(guard.mapped()->reads == accesses + 1); // write only; no invalid read
|
||||
guard.tables[0].buffer += 100; guard.run(0, r, info(1), false);
|
||||
guard.tables[0].range = 32; guard.run(0, r, info(1), false);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
"""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_none_when_close_preference_is_missing_or_invalid(self):
|
||||
self.write(True)
|
||||
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)
|
||||
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(
|
||||
'lol-user-experience:\n'
|
||||
' data:\n'
|
||||
' potatoModeEnabled: false\n'
|
||||
' closeLeagueClientDuringGame: false\n'
|
||||
' potatoModeEnabled: "true"\n'
|
||||
' closeLeagueClientDuringGame: "true"\n')
|
||||
self.assertEqual(tricks.mode(self.path), 'close')
|
||||
|
||||
def test_keys_outside_the_named_section_are_ignored(self):
|
||||
self.path.write_text(
|
||||
'lol-user-experience:\n'
|
||||
' data:\n'
|
||||
' potatoModeEnabled: true\n'
|
||||
' closeLeagueClientDuringGame: false\n'
|
||||
'other-section:\n'
|
||||
' potatoModeEnabled: true\n'
|
||||
' closeLeagueClientDuringGame: true\n')
|
||||
self.assertEqual(tricks.mode(self.path), 'hide')
|
||||
|
||||
def test_inline_yaml_comments_are_ignored(self):
|
||||
self.path.write_text(
|
||||
'lol-user-experience:\n'
|
||||
' data:\n'
|
||||
' potatoModeEnabled: true # client optimizations\n'
|
||||
' closeLeagueClientDuringGame: "true" # close during matches\n')
|
||||
self.assertEqual(tricks.mode(self.path), 'close')
|
||||
|
||||
def test_second_read_failure_fails_closed(self):
|
||||
self.write(True, False)
|
||||
text = self.path.read_text()
|
||||
with patch.object(Path, 'read_text', side_effect=[text, OSError()]):
|
||||
self.assertEqual(tricks.mode(self.path), 'none')
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,346 @@
|
||||
"""Exercise the in-match client window hide with fake /proc entries and X11."""
|
||||
import importlib.util
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
'hide_in_game', ROOT / 'tools/hide-league-client-in-game.py')
|
||||
hide_in_game = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(hide_in_game)
|
||||
|
||||
GAME = '/Applications/League of Legends.app/Contents/LoL/LeagueofLegends'
|
||||
MAIN = '/Applications/League of Legends.app/Contents/LoL/LeagueClientUx.app/Contents/MacOS/LeagueClientUx'
|
||||
RIOT = '/Applications/Riot Client.app/Contents/MacOS/RiotClientServices'
|
||||
|
||||
|
||||
class FakeX11:
|
||||
def __init__(self):
|
||||
self.unmapped = []
|
||||
self.shown = []
|
||||
|
||||
def unmap(self, window):
|
||||
self.unmapped.append(window)
|
||||
|
||||
def show(self, window):
|
||||
self.shown.append(window)
|
||||
|
||||
|
||||
class HideClientInGameTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.proc = self.root / 'proc'
|
||||
self.proc.mkdir()
|
||||
self.prefix = self.root / '.darling'
|
||||
self.env_patch = patch.dict(os.environ, {'DPREFIX': str(self.prefix)})
|
||||
self.env_patch.start()
|
||||
self.addCleanup(self.env_patch.stop)
|
||||
|
||||
def environment(self, prefix):
|
||||
return b'%s=%s\0' % (
|
||||
hide_in_game.SOCK_VARIABLE,
|
||||
os.fsencode(prefix / hide_in_game.SOCK_NAME),
|
||||
)
|
||||
|
||||
def process(self, pid, executable=GAME, prefix='own', environ=None, args=('--flag',)):
|
||||
folder = self.proc / str(pid)
|
||||
folder.mkdir()
|
||||
(folder / 'cmdline').write_bytes(
|
||||
executable.encode() + b'\0' +
|
||||
b''.join(arg.encode() + b'\0' for arg in args))
|
||||
if environ is None:
|
||||
environ = self.environment(self.prefix if prefix == 'own' else prefix)
|
||||
(folder / 'environ').write_bytes(environ)
|
||||
(folder / 'stat').write_text(f'{pid} (game) S')
|
||||
return folder
|
||||
|
||||
def test_game_running_for_own_prefix(self):
|
||||
self.process(123)
|
||||
self.assertTrue(hide_in_game.game_running(self.prefix, self.proc))
|
||||
|
||||
def test_no_game_is_false(self):
|
||||
self.process(123, executable=MAIN)
|
||||
self.assertFalse(hide_in_game.game_running(self.prefix, self.proc))
|
||||
|
||||
def test_game_of_another_prefix_is_false(self):
|
||||
self.process(123, prefix=self.root / 'other')
|
||||
self.assertFalse(hide_in_game.game_running(self.prefix, self.proc))
|
||||
|
||||
def test_game_without_socket_is_false(self):
|
||||
self.process(123, environ=b'')
|
||||
self.assertFalse(hide_in_game.game_running(self.prefix, self.proc))
|
||||
|
||||
def test_unreadable_environment_is_unknown(self):
|
||||
folder = self.process(123)
|
||||
original = Path.read_bytes
|
||||
|
||||
def read(path):
|
||||
if path.name == 'environ':
|
||||
raise PermissionError()
|
||||
return original(path)
|
||||
|
||||
with patch.object(Path, 'read_bytes', read):
|
||||
self.assertIsNone(hide_in_game.game_running(self.prefix, self.proc))
|
||||
|
||||
def test_unreadable_process_directory_is_unknown(self):
|
||||
class UnreadableProc:
|
||||
def iterdir(self):
|
||||
raise OSError()
|
||||
|
||||
self.assertIsNone(hide_in_game.game_running(self.prefix, UnreadableProc()))
|
||||
|
||||
def test_host_socket_for_guest_uses_nspid(self):
|
||||
host = self.proc / '999'
|
||||
host.mkdir()
|
||||
(host / 'status').write_text('Name:\tCrBrowserMain\nNSpid:\t999\t5877\n')
|
||||
(host / 'environ').write_bytes(self.environment(self.prefix))
|
||||
self.assertEqual(
|
||||
hide_in_game.host_socket_for_guest(5877, self.proc),
|
||||
os.fsencode(self.prefix / hide_in_game.SOCK_NAME),
|
||||
)
|
||||
|
||||
def test_window_belongs_to_prefix(self):
|
||||
socket = os.fsencode(self.prefix / hide_in_game.SOCK_NAME)
|
||||
with patch.object(hide_in_game, 'window_pid', return_value=5877), \
|
||||
patch.object(hide_in_game, 'host_socket_for_guest', return_value=socket):
|
||||
self.assertTrue(hide_in_game.window_belongs_to_prefix('0x10', self.prefix))
|
||||
self.assertFalse(hide_in_game.window_belongs_to_prefix(
|
||||
'0x10', self.root / 'other'))
|
||||
|
||||
def test_once_runs_without_opt_in(self):
|
||||
output = io.StringIO()
|
||||
with patch.dict(os.environ, {
|
||||
'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, 'game_running', return_value=True), \
|
||||
patch.object(hide_in_game.sys, 'stdout', output):
|
||||
self.assertEqual(hide_in_game.main(['--once']), 0)
|
||||
self.assertIn('match=running', output.getvalue())
|
||||
|
||||
def test_target_windows_ignores_game_class(self):
|
||||
with patch.object(hide_in_game, 'xprop_root',
|
||||
return_value=['0x10', '0x20', '0x30']), \
|
||||
patch.object(hide_in_game, 'window_class',
|
||||
side_effect=[
|
||||
'wm_class(string) = "leagueclientux"',
|
||||
'wm_class(string) = "leagueoflegends"',
|
||||
'wm_class(string) = "riot client"',
|
||||
]), \
|
||||
patch.object(hide_in_game, 'window_belongs_to_prefix', return_value=True):
|
||||
self.assertEqual(hide_in_game.target_windows(self.prefix), [0x10, 0x30])
|
||||
|
||||
def test_target_windows_requires_this_prefix(self):
|
||||
with patch.object(hide_in_game, 'xprop_root',
|
||||
return_value=['0x10', '0x20']), \
|
||||
patch.object(hide_in_game, 'window_class',
|
||||
return_value='wm_class(string) = "leagueclientux"'), \
|
||||
patch.object(hide_in_game, 'window_belongs_to_prefix',
|
||||
side_effect=[True, False]):
|
||||
self.assertEqual(hide_in_game.target_windows(self.prefix), [0x10])
|
||||
|
||||
def test_target_windows_caches_non_empty_classes(self):
|
||||
cache = {}
|
||||
with patch.object(hide_in_game, 'xprop_root', return_value=['0x10']), \
|
||||
patch.object(hide_in_game, 'window_class',
|
||||
return_value='wm_class(string) = "leagueclientux"') as classes, \
|
||||
patch.object(hide_in_game, 'window_belongs_to_prefix', return_value=True):
|
||||
self.assertEqual(hide_in_game.target_windows(self.prefix, cache), [0x10])
|
||||
self.assertEqual(hide_in_game.target_windows(self.prefix, cache), [0x10])
|
||||
self.assertEqual(classes.call_count, 1)
|
||||
|
||||
def test_missing_wm_class_is_not_cached_as_a_class(self):
|
||||
with patch.object(hide_in_game, 'xprop_value',
|
||||
return_value='WM_CLASS: not found.'):
|
||||
self.assertEqual(hide_in_game.window_class('0x10'), '')
|
||||
|
||||
def test_unmap_failure_removes_the_window_from_hidden(self):
|
||||
class FailingX11(FakeX11):
|
||||
def unmap(self, window):
|
||||
raise RuntimeError('unmap failed')
|
||||
|
||||
watcher = hide_in_game.Watcher(
|
||||
self.prefix, FailingX11(),
|
||||
window_source=lambda _prefix, _cache: [0xabc],
|
||||
state_source=lambda _prefix: True,
|
||||
sleep=lambda _seconds: None,
|
||||
out=io.StringIO(),
|
||||
)
|
||||
watcher._hide_new()
|
||||
self.assertEqual(watcher.hidden, set())
|
||||
|
||||
def test_interrupted_unmap_keeps_the_window_tracked(self):
|
||||
shown = []
|
||||
|
||||
class InterruptingX11:
|
||||
def unmap(self, window):
|
||||
raise KeyboardInterrupt
|
||||
|
||||
def show(self, window):
|
||||
shown.append(window)
|
||||
|
||||
watcher = hide_in_game.Watcher(
|
||||
self.prefix, InterruptingX11(),
|
||||
window_source=lambda _prefix, _cache: [0xabc],
|
||||
state_source=lambda _prefix: True,
|
||||
sleep=lambda _seconds: None,
|
||||
out=io.StringIO(),
|
||||
)
|
||||
with self.assertRaises(KeyboardInterrupt):
|
||||
watcher._hide_new()
|
||||
self.assertEqual(watcher.hidden, {0xabc})
|
||||
watcher.restore()
|
||||
self.assertEqual(shown, [0xabc])
|
||||
|
||||
def test_log_failure_does_not_discard_an_unmapped_window(self):
|
||||
class BrokenOutput:
|
||||
def write(self, _text):
|
||||
raise BrokenPipeError
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
watcher = hide_in_game.Watcher(
|
||||
self.prefix, FakeX11(),
|
||||
window_source=lambda _prefix, _cache: [0xabc],
|
||||
state_source=lambda _prefix: True,
|
||||
sleep=lambda _seconds: None,
|
||||
out=BrokenOutput(),
|
||||
)
|
||||
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()
|
||||
output = io.StringIO()
|
||||
watcher = hide_in_game.Watcher(
|
||||
self.prefix, x11,
|
||||
window_source=lambda _prefix, _cache: [0xabc],
|
||||
state_source=lambda _prefix: next(states),
|
||||
sleep=lambda _seconds: None,
|
||||
out=output,
|
||||
)
|
||||
watcher.run(max_steps=3)
|
||||
self.assertEqual(x11.unmapped, [0xabc])
|
||||
self.assertEqual(x11.shown, [0xabc])
|
||||
self.assertIn('hidden 0xabc', output.getvalue())
|
||||
self.assertIn('the match ended', output.getvalue())
|
||||
self.assertIn('shown 0xabc', output.getvalue())
|
||||
|
||||
def test_unreadable_state_restores_the_window(self):
|
||||
states = iter([True, None])
|
||||
x11 = FakeX11()
|
||||
output = io.StringIO()
|
||||
watcher = hide_in_game.Watcher(
|
||||
self.prefix, x11,
|
||||
window_source=lambda _prefix, _cache: [0xdef],
|
||||
state_source=lambda _prefix: next(states),
|
||||
sleep=lambda _seconds: None,
|
||||
out=output,
|
||||
)
|
||||
watcher.run(max_steps=2)
|
||||
self.assertEqual(x11.unmapped, [0xdef])
|
||||
self.assertEqual(x11.shown, [0xdef])
|
||||
self.assertIn('unreadable /proc state', output.getvalue())
|
||||
|
||||
def test_no_game_does_not_touch_windows(self):
|
||||
x11 = FakeX11()
|
||||
watcher = hide_in_game.Watcher(
|
||||
self.prefix, x11,
|
||||
window_source=lambda _prefix, _cache: [0xabc],
|
||||
state_source=lambda _prefix: False,
|
||||
sleep=lambda _seconds: None,
|
||||
out=io.StringIO(),
|
||||
)
|
||||
watcher.run(max_steps=1)
|
||||
self.assertEqual(x11.unmapped, [])
|
||||
self.assertEqual(x11.shown, [])
|
||||
|
||||
def test_main_is_disabled_by_default(self):
|
||||
with patch.dict(os.environ, {
|
||||
'RCLOL_HIDE_CLIENT_IN_GAME': '',
|
||||
'RCLOL_CLOSE_CLIENT_IN_GAME': '',
|
||||
}):
|
||||
self.assertEqual(hide_in_game.main([]), 0)
|
||||
|
||||
def test_close_mode_takes_precedence(self):
|
||||
with patch.dict(os.environ, {
|
||||
'RCLOL_HIDE_CLIENT_IN_GAME': '1',
|
||||
'RCLOL_CLOSE_CLIENT_IN_GAME': '1',
|
||||
}):
|
||||
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):
|
||||
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()
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Compile the shipped address-table helper against an inert CPU-only fixture."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
HEADER = 'private-include/indium/command-encoder.private.hpp'
|
||||
|
||||
|
||||
def address_helper():
|
||||
patch = (ROOT / 'patches/metal--deps--indium.patch').read_text()
|
||||
marker = f'diff --git a/{HEADER} b/{HEADER}\n'
|
||||
section = patch.split(marker, 1)[1].split('\ndiff --git ', 1)[0]
|
||||
# Extract each postimage hunk separately: never silently join a source gap.
|
||||
hunks = re.split(r'(?m)^@@ [^\n]*\n', section)[1:]
|
||||
postimages = [''.join(line[1:] for line in hunk.splitlines(keepends=True)
|
||||
if line.startswith((' ', '+'))) for hunk in hunks]
|
||||
|
||||
def region(start, end):
|
||||
matches = [text[text.index(start):text.index(end, text.index(start))]
|
||||
for text in postimages if start in text and end in text]
|
||||
if len(matches) != 1:
|
||||
raise AssertionError('expected one complete patched region: ' + start)
|
||||
return matches[0]
|
||||
|
||||
return (region(' template<class T, size_t capacity = 64> class DescriptorScratch',
|
||||
' // Per-encoder binding tables') +
|
||||
region('\tstruct AddressPool {', '\n\ttemplate<size_t setCount>'))
|
||||
|
||||
|
||||
@unittest.skipUnless(shutil.which('c++'), 'C++ compiler required')
|
||||
class AddressShadowChecks(unittest.TestCase):
|
||||
def test_interleaving_fallback_padding_rollover_large_keys_and_allocations(self):
|
||||
fixture = (ROOT / 'tests/fixtures/indium-address-shadow.cpp').read_text()
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder)
|
||||
source = path / 'check.cpp'
|
||||
source.write_text(fixture.replace('/* ACTUAL_PATCHED_HELPER */', address_helper()))
|
||||
build = subprocess.run(['c++', '-std=c++17', '-O1', str(source), '-o', str(path/'check')],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(build.returncode, 0, build.stderr)
|
||||
result = subprocess.run([str(path/'check')], capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
#!/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 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
|
||||
does not change preferences or files.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SECTION = 'lol-user-experience'
|
||||
CLOSE_KEY = 'closeLeagueClientDuringGame'
|
||||
OPTIMIZATION_KEY = 'potatoModeEnabled'
|
||||
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 invalid.
|
||||
|
||||
OSError/UnicodeError is raised for an unreadable file so callers can fail
|
||||
closed instead of treating a read race as a missing preference.
|
||||
"""
|
||||
lines = Path(path).read_text().splitlines()
|
||||
in_section = False
|
||||
value = None
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith('#'):
|
||||
continue
|
||||
if not line.startswith((' ', '\t')):
|
||||
if stripped == section + ':':
|
||||
in_section = True
|
||||
elif in_section:
|
||||
break
|
||||
continue
|
||||
if not in_section or not stripped.startswith(key + ':'):
|
||||
continue
|
||||
value = parse_bool(stripped.split(':', 1)[1])
|
||||
return value
|
||||
|
||||
|
||||
def mode(path):
|
||||
try:
|
||||
optimization = read_bool(path, OPTIMIZATION_KEY)
|
||||
except (OSError, UnicodeError):
|
||||
return 'none'
|
||||
if optimization is not True:
|
||||
return 'none'
|
||||
try:
|
||||
close = read_bool(path, CLOSE_KEY)
|
||||
except (OSError, UnicodeError):
|
||||
return 'none'
|
||||
if close is True:
|
||||
return 'close'
|
||||
if close is False:
|
||||
return 'hide'
|
||||
return 'none'
|
||||
|
||||
|
||||
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())
|
||||
Executable
+425
@@ -0,0 +1,425 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Opt-in: hide the League client UI while a match is 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
|
||||
default, fails closed, and is intended for X11/Xwayland desktops.
|
||||
|
||||
The game is attributed to this prefix through the loader's
|
||||
`__mldr_sockpath`, because guest processes do not carry `DPREFIX`. If proc,
|
||||
ownership or window information cannot be read, no window is touched.
|
||||
|
||||
Diagnose one observation with:
|
||||
|
||||
python3 tools/hide-league-client-in-game.py --once
|
||||
|
||||
Enable it for a launcher run with:
|
||||
|
||||
RCLOL_HIDE_CLIENT_IN_GAME=1 ./scripts/run.sh
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
GAME_NAME = 'LeagueofLegends'
|
||||
SOCK_VARIABLE = b'__mldr_sockpath'
|
||||
SOCK_NAME = '.darlingserver.sock'
|
||||
POLL_SECONDS = 2.0
|
||||
IDLE_SECONDS = 600
|
||||
TARGET_CLASSES = ('leagueclientux', 'riot client')
|
||||
|
||||
|
||||
def target_prefix():
|
||||
"""The prefix this launcher owns, or None when it cannot be determined."""
|
||||
value = os.environ.get('DPREFIX', str(Path.home() / '.darling'))
|
||||
prefix = Path(value).expanduser()
|
||||
return prefix if prefix.is_absolute() else None
|
||||
|
||||
|
||||
def process_state(proc):
|
||||
try:
|
||||
return (proc / 'stat').read_text().rsplit(')', 1)[1].split()[0]
|
||||
except (OSError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
def command_names(proc):
|
||||
try:
|
||||
raw = (proc / 'cmdline').read_bytes()
|
||||
except OSError:
|
||||
return []
|
||||
return [
|
||||
os.path.basename(part.decode(errors='replace'))
|
||||
for part in raw.split(b'\0')
|
||||
if part
|
||||
]
|
||||
|
||||
|
||||
def game_running(prefix, proc_root=Path('/proc')):
|
||||
"""True/False for a game of this prefix; None when /proc is unreliable.
|
||||
|
||||
The game executable is matched by its exact file name. The process must
|
||||
carry the loader socket of this prefix, so a game from another install is
|
||||
never considered. Any unreadable candidate makes the result None (fail
|
||||
closed).
|
||||
"""
|
||||
owner = (prefix / SOCK_NAME).parent.resolve()
|
||||
try:
|
||||
entries = list(proc_root.iterdir())
|
||||
except OSError:
|
||||
return None
|
||||
for proc in entries:
|
||||
if not proc.name.isdigit() or process_state(proc) in ('Z', None):
|
||||
continue
|
||||
try:
|
||||
if proc.stat().st_uid != os.getuid():
|
||||
continue
|
||||
if GAME_NAME not in command_names(proc):
|
||||
continue
|
||||
environment = dict(
|
||||
item.split(b'=', 1)
|
||||
for item in (proc / 'environ').read_bytes().split(b'\0')
|
||||
if b'=' in item
|
||||
)
|
||||
socket = environment.get(SOCK_VARIABLE)
|
||||
if socket is None:
|
||||
continue
|
||||
if Path(os.fsdecode(socket)).parent.resolve() != owner:
|
||||
continue
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
if proc.exists():
|
||||
return None
|
||||
continue
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
return False
|
||||
|
||||
|
||||
def xprop_root():
|
||||
try:
|
||||
listing = subprocess.check_output(
|
||||
['xprop', '-root', '_NET_CLIENT_LIST'], text=True, timeout=5
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return []
|
||||
return re.findall(r'0x[0-9a-fA-F]+', listing)
|
||||
|
||||
|
||||
def xprop_value(window, prop):
|
||||
try:
|
||||
return subprocess.check_output(
|
||||
['xprop', '-id', window, prop], text=True, timeout=5
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return ''
|
||||
|
||||
|
||||
def window_class(window):
|
||||
value = xprop_value(window, 'WM_CLASS').lower()
|
||||
return value if '=' in value else ''
|
||||
|
||||
|
||||
def window_pid(window):
|
||||
match = re.search(r'=\s*(\d+)', xprop_value(window, '_NET_WM_PID'))
|
||||
return int(match.group(1)) if match else None
|
||||
|
||||
|
||||
def _socket_from_process(proc):
|
||||
try:
|
||||
environment = dict(
|
||||
item.split(b'=', 1)
|
||||
for item in (proc / 'environ').read_bytes().split(b'\0')
|
||||
if b'=' in item
|
||||
)
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
return environment.get(SOCK_VARIABLE)
|
||||
|
||||
|
||||
def host_socket_for_guest(guest_pid, proc_root=Path('/proc')):
|
||||
"""Find the host process for a guest X11 PID and return its loader socket.
|
||||
|
||||
Darling's X11 window stores the guest PID in _NET_WM_PID, while the
|
||||
process visible in /proc is the host loader. NSpid carries both IDs. The
|
||||
direct /proc/<pid> case is checked too for a native process.
|
||||
"""
|
||||
direct = proc_root / str(guest_pid)
|
||||
if direct.exists():
|
||||
socket = _socket_from_process(direct)
|
||||
if socket is not None:
|
||||
return socket
|
||||
sockets = set()
|
||||
try:
|
||||
entries = list(proc_root.iterdir())
|
||||
except OSError:
|
||||
return None
|
||||
for proc in entries:
|
||||
if not proc.name.isdigit():
|
||||
continue
|
||||
try:
|
||||
status = (proc / 'status').read_text()
|
||||
except OSError:
|
||||
continue
|
||||
for line in status.splitlines():
|
||||
if not line.startswith('NSpid:'):
|
||||
continue
|
||||
if str(guest_pid) not in line.split()[1:]:
|
||||
continue
|
||||
socket = _socket_from_process(proc)
|
||||
if socket is not None:
|
||||
sockets.add(socket)
|
||||
return sockets.pop() if len(sockets) == 1 else None
|
||||
|
||||
|
||||
def window_belongs_to_prefix(window, prefix):
|
||||
pid = window_pid(window)
|
||||
if pid is None:
|
||||
return False
|
||||
socket = host_socket_for_guest(pid)
|
||||
if socket is None:
|
||||
return False
|
||||
try:
|
||||
return Path(os.fsdecode(socket)).parent.resolve() == (prefix / SOCK_NAME).parent.resolve()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def target_windows(prefix, class_cache=None):
|
||||
"""Managed client windows for this prefix; the game window is never selected.
|
||||
|
||||
WM_CLASS is cached only when non-empty. The cache is pruned when a window
|
||||
leaves _NET_CLIENT_LIST; empty results are retried because a class can be
|
||||
set after the window appears.
|
||||
"""
|
||||
current = []
|
||||
for raw in xprop_root():
|
||||
try:
|
||||
current.append((int(raw, 16), raw))
|
||||
except ValueError:
|
||||
continue
|
||||
current_ids = {window for window, _raw in current}
|
||||
if class_cache is not None:
|
||||
for stale in set(class_cache) - current_ids:
|
||||
del class_cache[stale]
|
||||
found = []
|
||||
for window, raw in current:
|
||||
classes = class_cache.get(window) if class_cache is not None else None
|
||||
if not classes:
|
||||
classes = window_class(raw)
|
||||
if classes and class_cache is not None:
|
||||
class_cache[window] = classes
|
||||
if not classes or not any(name in classes for name in TARGET_CLASSES):
|
||||
continue
|
||||
if window_belongs_to_prefix(raw, prefix):
|
||||
found.append(window)
|
||||
return sorted(set(found))
|
||||
|
||||
|
||||
class X11:
|
||||
def __init__(self):
|
||||
self.lib = ctypes.CDLL('libX11.so.6')
|
||||
self.lib.XOpenDisplay.restype = ctypes.c_void_p
|
||||
self.lib.XOpenDisplay.argtypes = [ctypes.c_char_p]
|
||||
self.lib.XUnmapWindow.argtypes = [ctypes.c_void_p, ctypes.c_ulong]
|
||||
self.lib.XMapRaised.argtypes = [ctypes.c_void_p, ctypes.c_ulong]
|
||||
self.lib.XSync.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||
self.lib.XCloseDisplay.argtypes = [ctypes.c_void_p]
|
||||
error_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
|
||||
self._error_handler = error_type(lambda _display, _event: 0)
|
||||
self.lib.XSetErrorHandler.argtypes = [error_type]
|
||||
self.lib.XSetErrorHandler.restype = ctypes.c_void_p
|
||||
self.lib.XSetErrorHandler(self._error_handler)
|
||||
self.display = self.lib.XOpenDisplay(None)
|
||||
if not self.display:
|
||||
raise RuntimeError('cannot open DISPLAY')
|
||||
|
||||
def unmap(self, window):
|
||||
self.lib.XUnmapWindow(self.display, window)
|
||||
self.lib.XSync(self.display, 0)
|
||||
|
||||
def show(self, window):
|
||||
self.lib.XMapRaised(self.display, window)
|
||||
self.lib.XSync(self.display, 0)
|
||||
|
||||
def close(self):
|
||||
self.lib.XCloseDisplay(self.display)
|
||||
|
||||
|
||||
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,
|
||||
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
|
||||
self._log(f'hide-league-client-in-game: {reason}; showing the client')
|
||||
for window in sorted(self.hidden):
|
||||
try:
|
||||
self.x11.show(window)
|
||||
except Exception as error:
|
||||
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:
|
||||
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:
|
||||
continue
|
||||
self.hidden.add(window)
|
||||
try:
|
||||
self.x11.unmap(window)
|
||||
except Exception as error:
|
||||
self.hidden.discard(window)
|
||||
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:
|
||||
return bool(self.window_source(self.prefix, self.class_cache))
|
||||
except Exception:
|
||||
# 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
|
||||
self.sleep(POLL_SECONDS)
|
||||
|
||||
def restore(self):
|
||||
self._show_all('exiting')
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument('--detach', action='store_true',
|
||||
help='leave the launcher session so a terminal hang-up cannot end it')
|
||||
parser.add_argument('--once', action='store_true',
|
||||
help='one game observation and exit (no window changes)')
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
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 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:
|
||||
print('hide-league-client-in-game: unusable DPREFIX: no action',
|
||||
file=sys.stderr, flush=True)
|
||||
return 1
|
||||
if args.once:
|
||||
state = game_running(prefix)
|
||||
print(f'hide-league-client-in-game: match={"unreadable" if state is None else "running" if state else "absent"}',
|
||||
flush=True)
|
||||
return 0
|
||||
if args.detach:
|
||||
try:
|
||||
os.setsid()
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
x11 = X11()
|
||||
except Exception as error:
|
||||
print(f'hide-league-client-in-game: cannot open X11: {error}',
|
||||
file=sys.stderr, flush=True)
|
||||
return 1
|
||||
|
||||
watcher = Watcher(prefix, x11)
|
||||
|
||||
def stop(_signum, _frame):
|
||||
raise KeyboardInterrupt
|
||||
|
||||
for signum in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
|
||||
signal.signal(signum, stop)
|
||||
optout = ('RCLOL_CLIENT_IN_GAME_TRICKS=0' if tricks
|
||||
else 'RCLOL_HIDE_CLIENT_IN_GAME=0')
|
||||
print('hide-league-client-in-game: watching %s for a running match; '
|
||||
'opt out with %s' % (prefix, optout), flush=True)
|
||||
try:
|
||||
watcher.run()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
watcher.restore()
|
||||
x11.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -25,13 +25,6 @@ if [ "${RCLOL_CLEAN_STALE_HELPERS:-0}" = 1 ]; then
|
||||
python3 "$runner_dir/clean-stale-helpers.py" --prefix "$prefix_dir" ||
|
||||
echo 'clean-stale-helpers: cleanup failed; continuing' >&2
|
||||
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
|
||||
python3 "$runner_dir/close-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}"
|
||||
@@ -79,6 +72,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