diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml.bak similarity index 100% rename from .github/workflows/build.yml rename to .github/workflows/build.yml.bak diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml.bak similarity index 100% rename from .github/workflows/codeql-analysis.yml rename to .github/workflows/codeql-analysis.yml.bak diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e69de29..0000000 diff --git a/contributing.md b/contributing.md new file mode 100644 index 0000000..990d97e --- /dev/null +++ b/contributing.md @@ -0,0 +1,34 @@ +# Development +### Install Dependencies +``` +npm i +``` +### Running the Application +#### Serving Angular Live +This builds the Angular and Electron portions of the code and runs the Electron application. The Angular portion of the code is served live (i.e. from a server). Changes to both the Angular portion and the Electron portion of the code are watched and will trigger the code to be recompiled. When the Angular portion of the code is recompiled, any open Electron windows will be reloaded. When the Electron portion of the code is recompiled, a new Electron process will be spawned. +``` +npm run start +``` +#### Serving Angular Locally +This builds the Angular and Electron portions of the code and runs the Electron application. The Angular portion of the code is served locally (i.e. from a file). This is how the application would be ran once built into an executable. +``` +npm run build[:] +npm run electron -- ./ +``` +#### Skipping Angular Build +If the Angular portion of the code has not changed, or if the Angular window is not being displayed, then (re)compiling the Angular portion may be skipped. +``` +npm run electron:build +npm run electron -- ./ +``` +Alternatively, changes to the Electron portion of the code can be watched. When the Electron portion of the code is recompiled, a new Electron process will be spawned. +``` +npm run electron:watch +``` +### Building Executable +``` +npm run package +``` + +### CI/CD Pipeline +Commits trigger CodeQL analysis that build the application and add a status check to pull requests. Commits to master with a version tag (format v#.#.#) trigger the release job to build and create a release draft. The release draft requires manual review. Disabled atm \ No newline at end of file diff --git a/images/OpenBuildIn-1.2.1.png b/images/OpenBuildIn-1.2.1.png new file mode 100644 index 0000000..43efec0 Binary files /dev/null and b/images/OpenBuildIn-1.2.1.png differ diff --git a/images/RecentlyPlayed-1.2.1.png b/images/RecentlyPlayed-1.2.1.png new file mode 100644 index 0000000..753d929 Binary files /dev/null and b/images/RecentlyPlayed-1.2.1.png differ diff --git a/images/SpoofProfileIcon-1.2.1.png b/images/SpoofProfileIcon-1.2.1.png new file mode 100644 index 0000000..ebbc593 Binary files /dev/null and b/images/SpoofProfileIcon-1.2.1.png differ diff --git a/images/SpoofProfileRank-1.2.1.png b/images/SpoofProfileRank-1.2.1.png new file mode 100644 index 0000000..fd9c674 Binary files /dev/null and b/images/SpoofProfileRank-1.2.1.png differ diff --git a/package.json b/package.json index 3dd5ef9..a18baeb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lcu-enhancement-suite", - "version": "1.2.0", + "version": "1.2.1", "description": "Enhancements for the League of Legends client.", "author": "xadamxk", "contributors": [ diff --git a/readme.md b/readme.md index 89ffdc5..c8a7031 100644 --- a/readme.md +++ b/readme.md @@ -6,13 +6,18 @@ Running as a background application, found in your system tray, LCU Enhancement ![Feature Highlight GIF](https://github.com/xadamxk/LCU-Enhancement-Suite/blob/master/images/LCU-Enhancement-Suite-1.0.3.gif?raw=true) +## Download +[Download Link](https://github.com/xadamxk/LCU-Enhancement-Suite/releases/latest) + ## Features -- Open recommended build in browser during champion select (ARAM ONLY - SR coming soon) for the following providers: +- Open recommended build in browser during champion select (SR & ARAM ONLY) for the following providers: - Blitz.gg - Lolalytics - Mobalytics - Op.gg -- Quick invite recently played players + - U.gg +
Screenshot
+- Quick invite recently played players
Screenshot
- Quick invite players by friend group - Advanced loot disenchantment for owned & unowned resources including: - Champion Capsules @@ -27,46 +32,11 @@ Running as a background application, found in your system tray, LCU Enhancement - Availability (Friends List) - Icon (Friends List) - Rank (Friends List) +
Screenshot
- Auto accept queue prompt -## Download -[Download Link](https://github.com/xadamxk/LCU-Enhancement-Suite/releases/latest) - -# Development -### Install Dependencies -``` -npm i -``` -### Running the Application -#### Serving Angular Live -This builds the Angular and Electron portions of the code and runs the Electron application. The Angular portion of the code is served live (i.e. from a server). Changes to both the Angular portion and the Electron portion of the code are watched and will trigger the code to be recompiled. When the Angular portion of the code is recompiled, any open Electron windows will be reloaded. When the Electron portion of the code is recompiled, a new Electron process will be spawned. -``` -npm run start -``` -#### Serving Angular Locally -This builds the Angular and Electron portions of the code and runs the Electron application. The Angular portion of the code is served locally (i.e. from a file). This is how the application would be ran once built into an executable. -``` -npm run build[:] -npm run electron -- ./ -``` -#### Skipping Angular Build -If the Angular portion of the code has not changed, or if the Angular window is not being displayed, then (re)compiling the Angular portion may be skipped. -``` -npm run electron:build -npm run electron -- ./ -``` -Alternatively, changes to the Electron portion of the code can be watched. When the Electron portion of the code is recompiled, a new Electron process will be spawned. -``` -npm run electron:watch -``` -### Building Executable -``` -npm run package -``` -### CI/CD Pipeline -Commits trigger CodeQL analysis that build the application and add a status check to pull requests. Commits to master with a version tag (format v#.#.#) trigger the release job to build and create a release draft. The release draft requires manual review. ## Riot Games diff --git a/src/electron/core/connection.ts b/src/electron/core/connection.ts index 3f3b747..05715ae 100644 --- a/src/electron/core/connection.ts +++ b/src/electron/core/connection.ts @@ -32,6 +32,7 @@ declare module '../../connector' { getCurrentVersion(): Promise; getChampionIcon(championId: number): Promise; getChampionSelectChampions(): Promise; + getLobbyV2Lobby(): Promise; } } @@ -63,11 +64,11 @@ LeagueConnection.prototype.forgeLoot = async function(this, recipeName, componen return await this.post(`${Endpoints.LOOT_RECIPES}/${recipeName}/craft?repeat=${repeatCount}`, componentLootIds); }; -LeagueConnection.prototype.getBalance = async function(this) : Promise { +LeagueConnection.prototype.getBalance = async function(this): Promise { return await this.get(Endpoints.WALLET); }; -LeagueConnection.prototype.getFriends = async function(this) : Promise { +LeagueConnection.prototype.getFriends = async function(this): Promise { return await this.get(Endpoints.FRIENDS); }; @@ -98,3 +99,7 @@ LeagueConnection.prototype.getChampionIcon = async function(championId): Promise LeagueConnection.prototype.getChampionSelectChampions = async function(): Promise { return await this.get(Endpoints.CHAMPION_SELECT_ALL_CHAMPS); }; + +LeagueConnection.prototype.getLobbyV2Lobby = async function(): Promise { + return await this.get(Endpoints.LOBBY); +}; diff --git a/src/electron/enums/endpoints.ts b/src/electron/enums/endpoints.ts index abbada4..b6925a0 100644 --- a/src/electron/enums/endpoints.ts +++ b/src/electron/enums/endpoints.ts @@ -5,6 +5,7 @@ export enum Endpoints { TEAM_BUILDER_CURRENT_CHAMPION = '/lol-lobby-team-builder/champ-select/v1/current-champion', END_OF_GAME_STATS = '/lol-end-of-game/v1/eog-stats-block', INVITATIONS = '/lol-lobby/v2/lobby/invitations', + LOBBY = '/lol-lobby/v2/lobby', FRIEND_GROUPS = '/lol-chat/v1/friend-groups', FRIENDS = '/lol-chat/v1/friends', FRIEND_REQUESTS = '/lol-chat/v1/friend-requests', diff --git a/src/electron/enums/game-modes.ts b/src/electron/enums/game-modes.ts new file mode 100644 index 0000000..8c39de7 --- /dev/null +++ b/src/electron/enums/game-modes.ts @@ -0,0 +1,5 @@ +// Source: https://static.developer.riotgames.com/docs/lol/gameModes.json +export enum GameModes { + ARAM = 'ARAM', + CLASSIC = 'CLASSIC' +} diff --git a/src/electron/enums/index.ts b/src/electron/enums/index.ts index 4adb0be..35e319c 100644 --- a/src/electron/enums/index.ts +++ b/src/electron/enums/index.ts @@ -5,3 +5,4 @@ export * from './loot-item-status'; export * from './loot-redeemable-status'; export * from './loot-types'; export * from './player-response'; +export * from './game-modes'; diff --git a/src/electron/models/index.ts b/src/electron/models/index.ts index 1dae2d0..8c58dee 100644 --- a/src/electron/models/index.ts +++ b/src/electron/models/index.ts @@ -12,3 +12,4 @@ export * from './eog-player'; export * from './eog-stats'; export * from './eog-team'; export * from './cs-champion'; +export * from './lollobby-lobby'; diff --git a/src/electron/models/lollobby-lobby.ts b/src/electron/models/lollobby-lobby.ts new file mode 100644 index 0000000..75da9f0 --- /dev/null +++ b/src/electron/models/lollobby-lobby.ts @@ -0,0 +1,151 @@ +import { Model } from '../api'; + +export class LOLLobbyV2Lobby extends Model { + canStartActivity: boolean; + gameConfig: GameConfig; + invitations: any[]; + localMember: LocalMember; + members: Member[]; + mucJwtDto: MucJwtDto; + multiUserChatId: string; + multiUserChatPassword: string; + partyId: string; + partyType: string; + restrictions: any; + scarcePositions: any[]; + warnings: any; +} + +export interface GameConfig { + allowablePremadeSizes: any[] + customLobbyName: string + customMutatorName: string + customRewardsDisabledReasons: any[] + customSpectatorPolicy: string + customSpectators: any[] + customTeam100: CustomTeam[] + customTeam200: CustomTeam[] + gameMode: string + isCustom: boolean + isLobbyFull: boolean + isTeamBuilderManaged: boolean + mapId: number + maxHumanPlayers: number + maxLobbySize: number + maxTeamSize: number + pickType: string + premadeSizeAllowed: boolean + queueId: number + shouldForceScarcePositionSelection: boolean + showPositionSelector: boolean + showQuickPlaySlotSelection: boolean +} + +export interface CustomTeam { + allowedChangeActivity: boolean + allowedInviteOthers: boolean + allowedKickOthers: boolean + allowedStartActivity: boolean + allowedToggleInvite: boolean + autoFillEligible: boolean + autoFillProtectedForPromos: boolean + autoFillProtectedForSoloing: boolean + autoFillProtectedForStreaking: boolean + botChampionId: number + botDifficulty: string + botId: string + firstPositionPreference: string + intraSubteamPosition: any + isBot: boolean + isLeader: boolean + isSpectator: boolean + playerSlots: any[] + puuid: string + quickplayPlayerState: any + ready: boolean + secondPositionPreference: string + showGhostedBanner: boolean + subteamIndex: any + summonerIconId: number + summonerId: number + summonerInternalName: string + summonerLevel: number + summonerName: string + teamId: number + tftNPEQueueBypass: any +} + +export interface LocalMember { + allowedChangeActivity: boolean + allowedInviteOthers: boolean + allowedKickOthers: boolean + allowedStartActivity: boolean + allowedToggleInvite: boolean + autoFillEligible: boolean + autoFillProtectedForPromos: boolean + autoFillProtectedForSoloing: boolean + autoFillProtectedForStreaking: boolean + botChampionId: number + botDifficulty: string + botId: string + firstPositionPreference: string + intraSubteamPosition: any + isBot: boolean + isLeader: boolean + isSpectator: boolean + playerSlots: any[] + puuid: string + quickplayPlayerState: any + ready: boolean + secondPositionPreference: string + showGhostedBanner: boolean + subteamIndex: any + summonerIconId: number + summonerId: number + summonerInternalName: string + summonerLevel: number + summonerName: string + teamId: number + tftNPEQueueBypass: any +} + +export interface Member { + allowedChangeActivity: boolean + allowedInviteOthers: boolean + allowedKickOthers: boolean + allowedStartActivity: boolean + allowedToggleInvite: boolean + autoFillEligible: boolean + autoFillProtectedForPromos: boolean + autoFillProtectedForSoloing: boolean + autoFillProtectedForStreaking: boolean + botChampionId: number + botDifficulty: string + botId: string + firstPositionPreference: string + intraSubteamPosition: any + isBot: boolean + isLeader: boolean + isSpectator: boolean + playerSlots: any[] + puuid: string + quickplayPlayerState: any + ready: boolean + secondPositionPreference: string + showGhostedBanner: boolean + subteamIndex: any + summonerIconId: number + summonerId: number + summonerInternalName: string + summonerLevel: number + summonerName: string + teamId: number + tftNPEQueueBypass: any +} + +export interface MucJwtDto { + channelClaim: string + domain: string + jwt: string + targetRegion: string +} diff --git a/src/electron/modules/auto-accept-queue.ts b/src/electron/modules/auto-accept-queue.ts index afc7abc..c16146d 100644 --- a/src/electron/modules/auto-accept-queue.ts +++ b/src/electron/modules/auto-accept-queue.ts @@ -6,24 +6,28 @@ import { Endpoints, PlayerResponse } from '../enums'; import { ReadyCheck } from '../models'; import { ReadyCheckSubscription } from '../subscriptions'; +const SETTINGS_KEY = 'isAutoAcceptQueueEnabled'; + export class AutoAcceptQueueModule extends WebSocketModule { id = 'AutoAcceptQueue'; - checked = this.storage.get('checked', true); + enabled = this.storage.get(SETTINGS_KEY, false); async register(): Promise { connection.addSubscription( new ReadyCheckSubscription((event) => { - this.refresh(event); + if (this.storage.get(SETTINGS_KEY)) { + this.refresh(event); + } }) ); const menuItem = new MenuItem({ label: 'Auto-Accept Queue', type: 'checkbox', - checked: this.checked, + checked: this.enabled, click: (menuItem) => { - this.checked = menuItem.checked = !this.checked; - this.storage.set('checked', this.checked); + this.enabled = menuItem.checked = !this.enabled; + this.storage.set(SETTINGS_KEY, this.enabled); } }); @@ -34,10 +38,7 @@ export class AutoAcceptQueueModule extends WebSocketModule { const readyCheck = new ReadyCheck(event.data); if (readyCheck.playerResponse === PlayerResponse.NONE) { - const response = await connection.post(Endpoints.READY_CHECK_ACCEPT); - console.log(response.status); - const json = await response.json(); - console.log(json); + await connection.post(Endpoints.READY_CHECK_ACCEPT); } } } diff --git a/src/electron/modules/index.ts b/src/electron/modules/index.ts index fad8a6e..4496d33 100644 --- a/src/electron/modules/index.ts +++ b/src/electron/modules/index.ts @@ -21,8 +21,8 @@ const modules: Module[] = [ new DisenchantLootModule(), new FriendsListModule(), new SpoofProfileModule(), - new AutoAcceptQueueModule(), - new OpenBuildInBrowserModule() + new OpenBuildInBrowserModule(), + new AutoAcceptQueueModule() ]; export { modules }; diff --git a/src/electron/modules/open-build-in-browser/open-build-in-browser.ts b/src/electron/modules/open-build-in-browser/open-build-in-browser.ts index 800a9dc..98f7527 100644 --- a/src/electron/modules/open-build-in-browser/open-build-in-browser.ts +++ b/src/electron/modules/open-build-in-browser/open-build-in-browser.ts @@ -3,6 +3,8 @@ import { WebSocketModule } from '../../api'; import { connection } from '../../core'; import { CSCurrentChampionSubscription, TBCurrentChampionSubscription } from '../../subscriptions'; import { BlitzProvider, IProvider, LolalyticsProvider, MobalyticsProvider, OPGGProvider, PROVIDERS, UGGProvider } from './providers'; +import { GameModes } from '../../enums'; +import { LOLLobbyV2Lobby } from '../../models'; const SETTINGS_KEY = 'openIn'; @@ -76,24 +78,28 @@ export class OpenBuildInBrowserModule extends WebSocketModule { const openInKey = this.storage.get(SETTINGS_KEY, ''); if (openInKey === '') return; + // Get game mode from lobby + const lobbyData: LOLLobbyV2Lobby = await (await connection.getLobbyV2Lobby()).json(); + const isAram = lobbyData.gameConfig.gameMode === GameModes.ARAM; + // Initialize provider let provider: IProvider; switch (openInKey) { default: case PROVIDERS.Mobalytics: - provider = new MobalyticsProvider(connection, true); + provider = new MobalyticsProvider(connection, isAram); break; case PROVIDERS.Blitz: - provider = new BlitzProvider(connection, true); + provider = new BlitzProvider(connection, isAram); break; case PROVIDERS.OPGG: - provider = new OPGGProvider(connection, true); + provider = new OPGGProvider(connection, isAram); break; case PROVIDERS.Lolalytics: - provider = new LolalyticsProvider(connection, true); + provider = new LolalyticsProvider(connection, isAram); break; case PROVIDERS.UGG: - provider = new UGGProvider(connection, true); + provider = new UGGProvider(connection, isAram); break; } diff --git a/src/electron/modules/recently-played.ts b/src/electron/modules/recently-played.ts index 012a2a6..eae027b 100644 --- a/src/electron/modules/recently-played.ts +++ b/src/electron/modules/recently-played.ts @@ -8,7 +8,7 @@ import { GameflowPhaseSubscription } from '../subscriptions'; export class RecentlyPlayedModule extends WebSocketModule { id = 'RecentlyPlayed'; - recentSummonerLimit = 20; + recentSummonerLimit = 18; async register(): Promise { // TODO: Add subscription for call when client initially loads friends (ie. signing into different account) @@ -58,22 +58,43 @@ export class RecentlyPlayedModule extends WebSocketModule { // reverse order of games to show most recent game first Object.keys(summonersByGame).reverse().forEach((gameId, index, reversedGameIds) => { + const summoners: RecentlyPlayedSummoner[] = summonersByGame[gameId]; + submenu.append(new MenuItem({ + label: `Game #${gameId}`, + sublabel: `${summoners[0].gameCreationDate.fromNow()}`, + enabled: false + })); + let previousTeamId = -1; // append each unique player in game - summonersByGame[gameId].forEach((summoner: RecentlyPlayedSummoner) => { + const sortedSummoners = summoners.sort((summonerA: RecentlyPlayedSummoner, summonerB: RecentlyPlayedSummoner) => summonerA.teamId - summonerB.teamId); + sortedSummoners.forEach((summoner: RecentlyPlayedSummoner) => { const championNameMatch = champions.filter((champion: CSChampion) => { return champion.id == summoner.championId; }); + if (previousTeamId !== summoner.teamId) { + previousTeamId = summoner.teamId; + const currentSummonerTeamId = summoner.teamId; + const currentSummonerTeammates = sortedSummoners.filter((summoner: RecentlyPlayedSummoner) => summoner.teamId === currentSummonerTeamId); + const teamLabel = currentSummonerTeammates.length === 5 ? 'Enemy' : 'Ally'; + const sideName = summoner.teamId === 100 ? 'Blue' : 'Red'; + submenu.append(new MenuItem({ + label: `${teamLabel} Team`, + sublabel: `${sideName} Side`, + click: async() => { + for (const teammate of currentSummonerTeammates) { + await connection.inviteSummoners(teammate.summonerId); + } + } + })); + } const championName = championNameMatch ? championNameMatch[0].name : 'N/A'; submenu.append(new MenuItem({ - label: `${summoner.summonerName} (${championName})`, - sublabel: `Played: ${summoner.gameCreationDate.fromNow()}`, + label: ` ${championName}`, + sublabel: ` ${summoner.summonerName}`, click: async() => { - const response = await connection.inviteSummoners(summoner.summonerId); - console.log(response.status); - const json = await response.json(); - console.log(json); + await connection.inviteSummoners(summoner.summonerId); } })); });