Bạn thấy cái giao diện của client game quá nhàm chán? Bạn muốn tích hợp vài tính năng để nâng cao trải nghiệm client? Vậy bài viết này là dành cho bạn 😁
Lịch sử League Client
Khoảng chục năm trước, cái thời mà Client được viết bằng C++, UI thì sử dụng Adobe AIR
Riot nhận thấy rằng AIR đã dần lỗi thời. Cùng với sự phát triển của nền tảng web, tiêu biểu là sự ra mắt của HTML5, họ quyết định xây dựng lại một diện mạo mới cho client bằng HTML và Javascript.
Nổi bật khi ấy là node.js. Nhưng Riot không muốn phụ thuộc vào node.js mà xây dựng một nền tảng tương tự. Họ sử dụng RTMP kèm bộ xử lý responses bất đồng bộ, thay vào đó là xây dựng UI bằng CEF.
CEF là viết tắt của Chromium Embedded Framework, một C++ framework hỗ trợ nhúng bộ vi xử lý của trình duyệt Chromium vào ứng dụng desktop. Project này được viết từ source của nhân Chromium và gần như tận dụng mọi thứ từ nó.
Bạn có thể tìm hiểu thêm về cách Riot xây dựng client mới tại đây:
Đến 2016, Riot tung ra bản alpha của client mới, lấy tên gọi là Hextech UI. Và cũng chính là client hiện tại.
Bạn có thể đọc thêm tại: Pick-lock Yasuo tốc độ bàn thờ
Pengu Loader
Pengu Loader (trước đây là League Loader) là một plugin loader được thiết kế dành riêng cho League Client.
Với Pengu Loader, bạn có thể load các plugin JavaScript vào Client dưới dạng dependency, giúp bạn cá nhân hóa giao diện của Client, thêm tính năng mới và cải thiện trải nghiệm tổng thể của bạn. Nó cũng cho phép bạn xây dựng một Client phù hợp với nhu cầu và sở thích của bạn.
Các tính năng chính:
- Tùy chỉnh Client với plugin
- Sơn màu Client theo sở thích
- Hỗ trợ JavaScript hiện đại
- Có hỗ trợ các built-in API và devtools
- Dễ dàng làm việc với LCU API
Cách hoạt động
Ý tưởng ban đầu là từ Mecha
Họ sử dụng Image File Execution để inject payload vào LeagueClientUx.exe
Injector của họ sử dụng MS Detours để khởi động tiến trình Ux với flag DEBUG_ONLY_THIS_PROCESS
. Tuy nhiên việc bật chế độ Debug có thể ảnh hưởng đến performance.
Cũng có nhiều trường hợp được ghi nhận đã bị Riot ban vĩnh viễn tài khoản vì sử dụng Mecha
Tất cả những thứ trái với điều khoản dịch vụ của Riot đều dẫn đến đình chỉ tài khoản, chúng tôi đã ghi nhận vài trường hợp bị cấm vĩnh viễn vì sử dụng Mecha. Từ patch 11.7, League Client update CEF từ 74 -> 91, do vậy tất cả các phiên bản Mecha trước đó đều không sử dụng được nữa.
Hooking
Pengu Loader sử dụng cách khác so với Mecha, “passive injection” thông qua proxy D3D9.
File D3d9.dll
gốc là Direct3D 9 Runtime – một dependency của libcef.dll
. DLL của Pengu là một proxy để load các API D3D9 cần thiết và hook ngược lại libcef.dll
d3d9.dll
> LeagueClientUx.exe (browser process)
+ Load CEF functions
+ Hook cef_initialize(), cef_browser_host_create_browser()
+ Modify command line/settings
+ Provide DevTools
> LeagueClientUxRender.exe (rerderer process, arg: --type=renderer)
+ Load CEF functions
+ Hook cef_execute_process()
+ Register extension
+ Load plugins
LeagueClientUx.exe là browser process cho việc render đồ họa, UI, IO,… Và LeagueClientUxRender.exe renderer process cho V8 và Blink.
Cấu hình
Pengu cung cấp GUI Winform để cài đặt loader và điều khiển League Client từ xa.
Ở phiên bản đầu tiên, Pengu copy d3d9.dll
vào thư mục League Client và đặt đường dẫn thư mục loader trong env. Tuy nhiên cách tiếp cận này quá chậm nếu bạn đang mở nhiều tác vụ. Vì vậy từ những phiên bản tiếp theo Pengu tạo symlink trong thư mục League Client trỏ đến thư mục loader.
DevTools
Pengu hỗ trợ 2 loại DevTools: built-in
và remote
.
URL DevTools remote có thể được truy xuất thông qua localhost:REMOTE_DEBUGGING_PORT/json
nếu --remote-debugging-port
được bật trong CEF.
Để mở DevTools tích hợp bằng JavaScript, Pengu invoke built-in
method trong process browser thông qua CreateRemoteThread
.
Thực hiện
Lí thuyết thế là đủ rồi, giờ bắt tay vào thực hành nhé 😁
Cài đạt Pengu Loader
- Tải Pengu tại đây
- Có 2 phiên bản, file exe setup và file portable chỉ cần giải nén ra là dùng được (ZIP/7z)
- Với bản portable, bạn nên giải nén nó ra một chỗ cố định
- Khởi động Pengu Loader
- Nhấn vào cái công tắc Status để bật nó
Tạo Plugin
Việc tạo plugin đòi hỏi kiến thức cơ bản về JavaScript, và CSS nếu bạn muốn tạo theme. Nếu bạn đã có kiến thức về lập trình web thì điều này sẽ khá dễ dàng.
Trong bài viết này mình sẽ hướng dẫn các bạn tạo 1 theme cơ bản.
Một plugin được coi là một theme nếu nó nhằm mục đích thay đổi style mặc định của League Client, thay vì thêm chức năng cho Client.
Cấu trúc của theme như sau:
your-plugin/
|__index.js <- plugin entry
|__theme.css <- theme
Từ index.js
, sử dụng import
để thêm CSS:
import "./theme.css";
Vậy là xong, từ bây giờ bạn sẽ thêm CSS để thay đổi tùy theo ý muốn của mình.
Sử dụng tính năng Inspect của DevTools để tìm các element và viết đè css lên nó.
Ví dụ nếu như bạn muốn ẩn cái bảng 18+ này
F12 để mở DevTools, sau đó ấn vào cái này để sử dụng Inspect
Di chuột vào cái bảng, sau đó click chuột trái. Lúc này trong DevTools sẽ hiện ra element của cái bảng với class vng-age-rating
Để ẩn nó, chỉ cần đè CSS lên như sau:
.vng-age-rating {
display: none !important;
}
Thay ảnh nền:
#rcp-fe-viewport-root {
background-image: url("bg.jpg") !important;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
#rcp-fe-viewport-root > .rcp-fe-viewport-full-screen {
background-color: transparent;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
-webkit-user-select: none;
box-shadow: inset 0px 0px 40px rgba(255, 255, 255, 0.2);
}
Hầu hết các element trong Client bạn có thể ghi đè bằng CSS để custom, nhưng một số lại khác. Bạn sẽ thấy một vài element sẽ nằm trong cái shadow-root
như thế này:
Đối với loại này bạn không thể đè CSS lên nó như bình thường, phải sử dụng JavaScript mới có thể modify:
window.addEventListener("load", () => {
// Wait for viewport root
const interval = setInterval(() => {
const manager = document.getElementById("rcp-fe-viewport-root");
if (manager) {
clearInterval(interval);
new MutationObserver((mutations) => {
document
.querySelector(".regalia-loaded")
.shadowRoot.querySelector(
".regalia-profile-banner-backdrop.regalia-banner-loaded",
)
.setAttribute(
"style",
"opacity: 0.5; filter: grayscale(100%) brightness(3);",
);
}).observe(manager, {
childList: true,
subtree: true,
});
}
}, 500);
});