6 min read

Mod client LMHT và nhiều hơn thế

cover
Table of Contents

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-inremote.

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

  1. 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
  1. Khởi động Pengu Loader
  2. 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);
});