├── 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 |
15 | 16 | Watch the video 17 |
Click to watch
18 |
19 |
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. --------------------------------------------------------------------------------