├── bin
└── global-index.js
├── Settings
└── settings.ini
├── run LoLXD.bat
├── package.json
├── README.md
└── lolxd.js
/bin/global-index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const lolxd = require("../lolxd.js")
4 |
5 | lolxd.startGlobal()
--------------------------------------------------------------------------------
/Settings/settings.ini:
--------------------------------------------------------------------------------
1 | enableBetterIngameMute=false
2 | enableToggleMute=false
3 | toggleMuteHotkey=[ctrl][shift][m]
4 |
--------------------------------------------------------------------------------
/run LoLXD.bat:
--------------------------------------------------------------------------------
1 | :: turn off console output
2 | @Echo off
3 |
4 | :: run lolxd
5 | node lolxd.js
6 |
7 | :: show "exiting in [...]" when lolxd finished
8 | echo This window will close in 5 seconds...
9 |
10 | :: exit in 5 seconds
11 | timeout 5 > NUL
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lolxd",
3 | "version": "0.0.1",
4 | "description": "Demo tool to fix bugs and add features in the League of Legends Client",
5 | "main": "lolxd.js",
6 | "bin": {
7 | "lolxd": "./bin/global-index.js"
8 | },
9 | "scripts": {
10 | "test": "echo \"Error: no test specified\" && exit 1",
11 | "start": "node lolxd.js"
12 | },
13 | "author": "LasTech Labs",
14 | "license": "ISC",
15 | "dependencies": {
16 | "cors": "^2.8.5",
17 | "express": "^4.17.1",
18 | "express-ws": "^5.0.2",
19 | "fs-extra": "^10.0.0",
20 | "node-fetch": "^2.6.1"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LoLXD
2 |
3 | A tool to enhance the vanilla League of Legends Client, by adding improvements and unlocking functionality.
4 |
5 |
6 | # THIS TOOL IS NO LONGER AVAILABLE
7 |
8 | Riot Games has rejected the developer application ticket for LoLXD so I have removed the client-interface package from github, this means that the tool will ***NOT BE FUNCTIONAL***.
9 |
10 | The remainder of this Readme is preserved as-is, though attempting to download may produce unexpected results.
11 |
12 | # Demo
13 |
14 |
20 |
21 | **Note:** LoLXD is a demo intended to *demonstrate* what is possible and is NOT intended as anything more. Your account may be banned as a result of use, at Riot's discretion. **Use at your own risk!**
22 |
23 | # What Improvements does LoLXD implement
24 | 1. Language Selection & Localization
25 |
26 | 2. Enable Replay Modes
27 | * Extended Zoom
28 |
29 | * First Person Mode
30 |
31 | 3. Replay Loader
32 |
33 | 4. Autosave Runes
34 |
35 | 5. Improved In-Game Mute Command
36 | * Mute Everything EXCEPT Team Pings
37 |
38 | * Still able to type in chat
39 |
40 | 6. New Voice Comms. Mode
41 |
42 | * Toggle Mute Hotkey
43 |
44 | 7. Open Multiple Clients
45 |
46 | 8. Test Ping Before Game
47 |
48 | 9. Enlarge In-Game HUD Size
49 |
50 | * Increase Chat/Map Size
51 |
52 | 10. Restart UX Process
53 |
54 | # Prerequisites
55 |
56 | *LoLXD will only run on windows currently, it could be adapted to run on mac/linux.
57 |
58 | Having [NodeJS](https://nodejs.org/) installed and added to PATH is essential.
59 |
60 | # How to install
61 |
62 | **Running as a local package:**
63 |
64 | 1. Download/clone this respository
65 | 3. Run `npm install` via command line
66 | 4. Then either:
67 |
68 | 1. Run `npm start` via command line, or
69 | 2. Click the `run LoLXD.bat` file
70 |
71 | The first time you run LoLXD you will have to go through each page and enable those settings/options you prefer to use. After this those settings are saved and will only have to be re-accessed if they are reset. You do not need to keep a browser tab open after first time use.
72 |
73 |
74 | # Note from Riot:
75 | LoLXD isn’t endorsed by Riot Games and doesn’t reflect the views or opinions of Riot Games or anyone officially involved in producing or managing League of Legends. League of Legends™ and Riot Games are trademarks or registered trademarks of Riot Games, Inc.
76 |
--------------------------------------------------------------------------------
/lolxd.js:
--------------------------------------------------------------------------------
1 | const fetch = require("node-fetch");
2 | var cors = require("cors");
3 | const express = require("express");
4 | const fs = require("fs-extra");
5 | const path = require("path");
6 | const { argv } = require("process");
7 | // const childProcess = require("child_process");
8 | let clientInterface = require("lolxd-client-interface");
9 | const app = express();
10 | var expressWs = require("express-ws")(app);
11 | const PORT = 1337;
12 | let HOST = "localhost";
13 | let local_settings_JSON = {};
14 |
15 | if (argv.includes("--network")) {
16 | HOST = "0.0.0.0";
17 | }
18 |
19 | app.use(
20 | cors({
21 | origin: "*",
22 | })
23 | );
24 |
25 | clientInterface.lifecycleEvents.on("connected", function (data) {
26 | let clientState = clientInterface.getClientState();
27 |
28 | let broadcastClients = expressWs.getWss("/ws");
29 | broadcastClients.clients.forEach((client) => {
30 | client.send(
31 | JSON.stringify({
32 | LCUInstanceConnected: true,
33 | LCIInstallDirectory: clientState.installDirectory,
34 | status: "connected",
35 | })
36 | );
37 | client.send(
38 | JSON.stringify({
39 | status: "gamephase update",
40 | gamePhase: clientState.gamePhase,
41 | })
42 | );
43 | });
44 | });
45 |
46 | clientInterface.lifecycleEvents.on("disconnected", function (data) {
47 | let broadcastClients = expressWs.getWss("/ws");
48 | broadcastClients.clients.forEach((client) => {
49 | client.send(
50 | JSON.stringify({
51 | LCUInstanceConnected: false,
52 | status: "disconnected",
53 | })
54 | );
55 | });
56 | });
57 |
58 | clientInterface.lifecycleEvents.on("gameflowphaseUpdate", function (data) {
59 | let clientState = clientInterface.getClientState();
60 |
61 | let broadcastClients = expressWs.getWss("/ws");
62 | broadcastClients.clients.forEach((client) => {
63 | client.send(
64 | JSON.stringify({
65 | status: "gamephase update",
66 | gamePhase: clientState.gamePhase,
67 | })
68 | );
69 | });
70 | });
71 |
72 |
73 | //is server running (ping-pong)
74 | app.get("/ping", (req, res) => {
75 | res.status(200).send("pong");
76 | });
77 |
78 | app.ws("/ws", function (ws, req) {
79 | let clientState = clientInterface.getClientState();
80 |
81 | ws.send(
82 | JSON.stringify({
83 | status: "LoLXD server connection established",
84 | LCUInstanceConnected: clientState.isConnected,
85 | LCIInstallDirectory: clientState.installDirectory,
86 | })
87 | );
88 |
89 | ws.send(
90 | JSON.stringify({
91 | status: "gamephase update",
92 | gamePhase: clientState.gamePhase,
93 | })
94 | );
95 |
96 | ws.on("message", function (msg) {
97 | if (msg === "__PING__") {
98 | ws.send(JSON.stringify({ status: "__PONG__" }));
99 | }
100 | });
101 | });
102 |
103 | app.post("/bugfix/unresponsive/gracefulreset", async (req, res) => {
104 | try {
105 | await clientInterface.restartUx();
106 | res.status(200).json({ code: "SUCCESS" });
107 | } catch (err) {
108 | console.log(err);
109 | res.status(500).json(err);
110 | }
111 | });
112 |
113 | app.post("/bugfix/unresponsive/hardreset", async (req, res) => {
114 | try {
115 | await clientInterface.forceRestartUx();
116 | res.status(200).json({ code: "SUCCESS" });
117 | } catch (err) {
118 | console.log(err);
119 | res.status(500).json(err);
120 | }
121 | });
122 |
123 | app.get("/addedfunction/togglemute/isenabled", async (req, res) => {
124 | try {
125 | let InisUpdated = await clientInterface.getPremadeVoiceSettings();
126 |
127 | if (
128 | InisUpdated.vadSensitivity === 0 &&
129 | InisUpdated.inputMode === "voiceActivity"
130 | ) {
131 | res
132 | .status(200)
133 | .json({ code: "SUCCESS", enabled: true, data: InisUpdated });
134 | } else {
135 | res
136 | .status(200)
137 | .json({ code: "SUCCESS", enabled: false, data: InisUpdated });
138 | }
139 | } catch (err) {
140 | console.log(err);
141 | res.status(500).json(err);
142 | }
143 | });
144 |
145 | app.post("/addedfunction/togglemute/enable", async (req, res) => {
146 | try {
147 | let InisUpdated = await Promise.all(
148 | clientInterface.setPremadeVoiceSettings()
149 | );
150 | InisUpdated = await Promise.all(
151 | InisUpdated.map((x) => {
152 | return x.text();
153 | })
154 | );
155 |
156 | res.status(200).json({ code: "SUCCESS", data: InisUpdated });
157 | } catch (err) {
158 | console.log(err);
159 | res.status(500).json(err);
160 | }
161 | });
162 |
163 | app.post("/addedfunction/togglemute/toggle", async (req, res) => {
164 | try {
165 | await clientInterface.toggleMute();
166 | res.status(200).json({ code: "SUCCESS" });
167 | } catch (err) {
168 | console.log(err);
169 | res.status(500).json(err);
170 | }
171 | });
172 |
173 | app.get("/addedfunction/bettermute", async (req, res) => {
174 | try {
175 | await clientInterface.betterMute();
176 | res.status(200).json({ code: "SUCCESS" });
177 | } catch (err) {
178 | console.log(err);
179 | res.status(500).json(err);
180 | }
181 | });
182 |
183 | app.get("/utility/getlocalsettings/:localsetting", async (req, res) => {
184 | try {
185 | res.status(200).json({
186 | code: "SUCCESS",
187 | [req.params.localsetting]: local_settings_JSON[req.params.localsetting],
188 | });
189 | } catch (err) {
190 | console.log(err);
191 | }
192 | });
193 |
194 | app.get("/utility/getroflpath", async (req, res) => {
195 | try {
196 | let InisUpdated = await clientInterface.getReplaysPath();
197 |
198 | res.status(200).json({ code: "SUCCESS", path: InisUpdated });
199 | } catch (err) {
200 | console.log(err);
201 | res.status(500).json({ code: "ERROR - (probably disconnected LCU)" });
202 | }
203 | });
204 |
205 | app.post(
206 | "/utility/updatelocalsettings/:localsetting/:settingval",
207 | async (req, res) => {
208 | try {
209 | let outcome;
210 | if (
211 | req.params.settingval === "true" ||
212 | req.params.settingval === "false"
213 | ) {
214 | outcome = await clientInterface.updateLocalSettings(
215 | req.params.localsetting,
216 | req.params.settingval === "true"
217 | );
218 | } else {
219 | outcome = await clientInterface.updateLocalSettings(
220 | req.params.localsetting,
221 | req.params.settingval
222 | );
223 | }
224 | local_settings_JSON[outcome.updated] = outcome.updatedValue;
225 | let toWrite = "";
226 | Object.entries(local_settings_JSON).forEach((pair) => {
227 | toWrite = toWrite.concat(`${pair[0]}=${pair[1]}\n`);
228 | });
229 | fs.writeFile(path.join(__dirname, "Settings", "settings.ini"), toWrite);
230 | res.status(200).json({
231 | code: "SUCCESS",
232 | updated: outcome.updated,
233 | updatedVal: outcome.updatedValue,
234 | });
235 | } catch (err) {
236 | console.log(err);
237 | res.status(500).json(err);
238 | }
239 | }
240 | );
241 |
242 | app.post("/addedfunction/changelocale/:newlocale", async (req, res) => {
243 | try {
244 | let a = await clientInterface.restartWithLocale(req.params.newlocale);
245 | res.status(200).json({ code: "SUCCESS" });
246 | } catch (err) {
247 | console.log(err);
248 | res.status(500).json(err);
249 | }
250 | });
251 |
252 | app.post("/addedfunction/spawnMultipleClient", async (req, res) => {
253 | try {
254 | let a = await clientInterface.spawnMultipleClient();
255 | res.status(200).json({ code: "SUCCESS" });
256 | } catch (err) {
257 | console.log(err);
258 | res.status(500).json(err);
259 | }
260 | });
261 |
262 | app.get("/utility/runes/getcurrentpage", async (req, res) => {
263 | try {
264 | let page = await clientInterface.getCurrentRunePage();
265 | page = await page.json();
266 | res.status(200).json({ code: "SUCCESS", data: page });
267 | } catch (err) {
268 | console.log(err);
269 | res.status(500).json(err);
270 | }
271 | });
272 |
273 | app.get("/utility/runes/getpagebyid/:id", async (req, res) => {
274 | try {
275 | let page = await clientInterface.getRunePageById(req.params.id);
276 | page = await page.json();
277 | res.status(200).json({ code: "SUCCESS", data: page });
278 | } catch (err) {
279 | console.log(err);
280 | res.status(500).json(err);
281 | }
282 | });
283 |
284 | app.get("/utility/runes/setpagebyid/:id", async (req, res) => {
285 | try {
286 | let a = await clientInterface.setRunePageById(
287 | req.params.id,
288 | req.query.name
289 | );
290 | a = await a.json();
291 | res.status(200).json({ code: "SUCCESS" });
292 | } catch (err) {
293 | console.log(err);
294 | res.status(500).json(err);
295 | }
296 | });
297 |
298 | app.get("/utility/runes/getallpages", async (req, res) => {
299 | try {
300 | let runepages = await clientInterface.getAllRunePages();
301 | runepages = await runepages.json();
302 | res.status(200).json({ code: "SUCCESS", data: runepages });
303 | } catch (err) {
304 | console.log(err);
305 | res.status(500).json(err);
306 | }
307 | });
308 |
309 | app.post("/utility/runes/update", async (req, res) => {
310 | try {
311 | let runepages = await clientInterface.updateRunes(
312 | req.query.runes,
313 | req.query.primarystyle,
314 | req.query.substyle
315 | );
316 | res.status(200).json({ code: "SUCCESS", data: runepages });
317 | } catch (err) {
318 | console.log(err);
319 | res.status(500).json(err);
320 | }
321 | });
322 |
323 | app.get("/utility/settings/list/:type", async (req, res) => {
324 | try {
325 | let settings = await clientInterface.getGameSettings(req.params.type);
326 | settings = await settings.json();
327 | res.status(200).json({ code: "SUCCESS", data: settings });
328 | } catch (err) {
329 | console.log(err);
330 | res.status(500).json(err);
331 | }
332 | });
333 |
334 | app.post("/utility/settings/set", express.json(), async (req, res) => {
335 | /*
336 | //Example Request - set toggle extended zoom to ctrl shift z
337 | let a = await fetch(`http://localhost:1337/utility/settings/set`, {
338 | method: "POST",
339 | headers: { "Content-Type": "application/json" },
340 | body: JSON.stringify({
341 | input: {
342 | GameEvents: {
343 | evtToggleExtendedZoom: "[Ctrl][Shift][z]",
344 | },
345 | },
346 | }),
347 | });
348 | */
349 |
350 | try {
351 | await clientInterface.setGameSettings(req.body.input, req.body.game);
352 | res.status(200).json({ code: "SUCCESS" });
353 | } catch (err) {
354 | console.log(err);
355 | res.status(500).json(err);
356 | }
357 | });
358 |
359 | app.get("/addedfunction/replayloader/getallreplays", async (req, res) => {
360 | try {
361 | let InisUpdated = await clientInterface.getAllReplays();
362 | InisUpdated = await Promise.all(InisUpdated);
363 | res.status(200).json({ code: "SUCCESS", data: InisUpdated });
364 | } catch (err) {
365 | console.log(err);
366 | res.status(500).json(err);
367 | }
368 | });
369 |
370 | app.post(
371 | "/addedfunction/replayloader/playreplay/:replayfile",
372 | async (req, res) => {
373 | try {
374 | let InisUpdated = await clientInterface.playReplay(
375 | req.params.replayfile,
376 | path.join(__dirname, "Settings")
377 | );
378 |
379 | res.status(200).json({ code: "SUCCESS" });
380 | } catch (err) {
381 | console.log(err);
382 | res.status(500).json(err);
383 | }
384 | }
385 | );
386 |
387 | app.get("/utility/runes/getAllRuneInfo", async (req, res) => {
388 | try {
389 | let InisUpdated = await Promise.all([
390 | clientInterface.getAllRunesStyles(),
391 | clientInterface.getAllRunes(),
392 | ]);
393 | InisUpdated = await Promise.all(
394 | InisUpdated.map((e) => {
395 | return e.json();
396 | })
397 | );
398 |
399 | let output = InisUpdated[0].map((skilltree) => {
400 | return {
401 | skilltreeName: skilltree.name,
402 | skilltreeIcon: skilltree.iconPath,
403 | runes: skilltree.slots.map((skillrow) => {
404 | return skillrow.perks.map((rune) => {
405 | return InisUpdated[1].find((expandedRune) => {
406 | return expandedRune.id === rune;
407 | });
408 | });
409 | }),
410 | };
411 | });
412 |
413 | res.status(200).json({ code: "SUCCESS", data: output });
414 | } catch (err) {
415 | console.log(err);
416 | res.status(500).json(err);
417 | }
418 | });
419 |
420 | app.get("/utility/proxy/*", async (req, res) => {
421 | try {
422 | let InisUpdated = await clientInterface.getRuneIcon(
423 | req.originalUrl.substring(15)
424 | );
425 | img = await InisUpdated.buffer();
426 | res.writeHead(200, {
427 | "Content-Type": "image/png",
428 | "Content-Length": img.length,
429 | });
430 |
431 | res.end(img);
432 | } catch (err) {
433 | console.log(err);
434 | res.status(500).json(err);
435 | }
436 | });
437 |
438 | /*
439 | app.get("/test", async (req, res) => {
440 | try {
441 | let InisUpdated = await clientInterface.getGameSettings("game");
442 |
443 | InisUpdated = await InisUpdated.json();
444 |
445 | console.log(InisUpdated);
446 | res.status(200).json({ code: "SUCCESS", data: InisUpdated });
447 | } catch (err) {
448 | console.log(err);
449 | res.status(500).json(err);
450 | }
451 | });
452 | */
453 | app.use("/bugfix/*", (req, res) => {
454 | res.status(404).json({ code: "ENDPOINT NOT FOUND" });
455 | });
456 |
457 | app.use("/addedfunction/*", (req, res) => {
458 | res.status(404).json({ code: "ENDPOINT NOT FOUND" });
459 | });
460 |
461 | app.use("/utility/*", (req, res) => {
462 | res.status(404).json({ code: "ENDPOINT NOT FOUND" });
463 | });
464 |
465 | app.use(
466 | express.static(path.join(__dirname, "build"), {
467 | index: "index.html",
468 | extensions: ["html"],
469 | redirect: true
470 | })
471 | );
472 |
473 | app.get("*", (req, res) => {
474 | res.redirect("/")
475 | });
476 |
477 | async function startup() {
478 | try {
479 | await fs.ensureDir(path.join(__dirname, "Settings"));
480 | await fs.ensureFile(path.join(__dirname, "Settings", "settings.ini"));
481 | let local_settings = await fs.readFile(
482 | path.join(__dirname, "Settings", "settings.ini"),
483 | "utf-8"
484 | );
485 | local_settings.split("\n").forEach(async (setting) => {
486 | if (setting != "") {
487 | if (
488 | setting.split("=")[1].trim() === "true" ||
489 | setting.split("=")[1].trim() === "false"
490 | ) {
491 | let outcome = await clientInterface.updateLocalSettings(
492 | setting.split("=")[0],
493 | setting.split("=")[1].trim() === "true"
494 | );
495 | local_settings_JSON[outcome.updated] = outcome.updatedValue;
496 | } else {
497 | let outcome = await clientInterface.updateLocalSettings(
498 | setting.split("=")[0],
499 | setting.split("=")[1].trim()
500 | );
501 | local_settings_JSON[outcome.updated] = outcome.updatedValue;
502 | }
503 | }
504 | });
505 | } catch (err) {
506 | console.log(err);
507 | }
508 | try {
509 | let running = await fetch(`http://localhost:1337/ping`);
510 | if (running.status === 200) {
511 | console.log(`\nLoLXD is already running!\nGo to http://localhost:${PORT}\n`);
512 | process.exit();
513 | // childProcess.exec(
514 | // `start chrome.exe -incognito http://localhost:${PORT}`,
515 | // () => {
516 | // process.exit();
517 | // }
518 | // );
519 | }
520 | } catch (err) {
521 | if (err.code === "ECONNREFUSED") {
522 | app.listen(PORT, HOST, () => {});
523 | started_time = Date.now();
524 | clientInterface.start();
525 | console.log(`\nLoLXD running!\nGo to http://localhost:${PORT}\n`);
526 | // childProcess.exec(
527 | // `start chrome.exe -incognito http://localhost:${PORT}`,
528 | // () => {
529 | // }
530 | // );
531 | }
532 | }
533 | }
534 |
535 | startup();
536 |
537 | exports.startGlobal = ()=>{} //In case someone wants to install as a global package. See bin/global-index.js to understand.
--------------------------------------------------------------------------------