Skip to content

Commit

Permalink
Bug fix (#197)
Browse files Browse the repository at this point in the history
* First implementation of twitch extension settings

* Fix merge conflicts

* Improvements for twitch extension in app

* Fix reject in password hashing

* App send mapstats to twitch extension

* Fix some playercards missing lastmatchdate cause NaN error
  • Loading branch information
JohannesMerkt authored Mar 27, 2022
1 parent 2d1a550 commit ce0d1a7
Show file tree
Hide file tree
Showing 20 changed files with 837 additions and 160 deletions.
4 changes: 3 additions & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@types/react-dom": "^17.0.11",
"@types/react-redux": "^7.1.22",
"@types/tcp-port-used": "^1.0.0",
"@types/uuid": "^8.3.4",
"@vercel/webpack-asset-relocator-loader": "1.7.0",
"css-loader": "^6.0.0",
"electron": "16.0.6",
Expand Down Expand Up @@ -63,6 +64,7 @@
"react-redux": "^7.2.6",
"reverse-line-reader": "^0.2.6",
"socket.io": "^4.4.1",
"tcp-port-used": "^1.0.2"
"tcp-port-used": "^1.0.2",
"uuid": "^8.3.2"
}
}
12 changes: 6 additions & 6 deletions packages/app/src/main/applicationManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Unsubscribe } from "@reduxjs/toolkit";
import { app, BrowserWindow, ipcMain, Menu, nativeTheme, shell, Tray } from "electron";
import config from "./config";
import { ApplicationStore } from "./applicationStore";
Expand All @@ -7,6 +6,7 @@ import { ApplicationWindows, WindowState } from "../redux/state";
import { actions } from "../redux/slice";
import { events } from "./mixpanel";
import { ApplicationWindow, WindowCloseHandlerCreator } from "./applicationWindow";
import { Unsubscribe } from "@reduxjs/toolkit";

// This allows TypeScript to pick up the magic constant that's auto-generated by Forge's Webpack
// plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on
Expand All @@ -19,17 +19,18 @@ declare const ABOUT_WINDOW_WEBPACK_ENTRY: string;
declare const ABOUT_WINDOW_PRELOAD_WEBPACK_ENTRY: string;

export class ApplicationManager {
applicationStore: ApplicationStore;
windows: Record<ApplicationWindows, ApplicationWindow>;
tray: Tray;
inTrayMode: boolean;
isQuitting: boolean;
unsubscriber: Unsubscribe;
startTime: number;
applicationStore: ApplicationStore;
unsubscriber: Unsubscribe;

constructor(applicationStore: ApplicationStore) {
this.isQuitting = false;
this.applicationStore = applicationStore;
this.unsubscriber = this.applicationStore.runtimeStore.subscribe(this.runtimeStoreSubscriber);
this.isQuitting = false;
const settings = this.applicationStore.getState().settings;
// launch tray if in tray mode
if (settings.runInTray) {
Expand All @@ -38,7 +39,6 @@ export class ApplicationManager {
} else {
this.inTrayMode = false;
}
this.unsubscriber = this.applicationStore.runtimeStore.subscribe(this.runtimeStoreSubscriber);
// initialize windows
this.windows = {
main: new ApplicationWindow("main", {
Expand Down Expand Up @@ -250,7 +250,7 @@ export class ApplicationManager {
);
};

destroy(): void {
public destroy(): void {
this.unsubscriber();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import {
ApplicationSettings,
ApplicationState,
WindowStates,
} from "../redux/state";
import { actions, ReduxStore } from "../redux/slice";
import { configureMainStore } from "../redux/configureStoreMain";
} from "../../redux/state";
import { actions, ReduxStore } from "../../redux/slice";
import { configureMainStore } from "../../redux/configureStoreMain";
import { AnyAction, Unsubscribe } from "@reduxjs/toolkit";
import { app, dialog, nativeTheme } from "electron";
import { defaultSettings, defaultWindowStates, startupGameData } from "../redux/defaultState";
import { defaultSettings, defaultWindowStates, startupGameData } from "../../redux/defaultState";
import axios from "axios";
import config from "./config";
import { events } from "./mixpanel";
import config from "../config";
import { events } from "../mixpanel";

export class ApplicationStore {
runtimeStore: ReduxStore;
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/main/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const config = {
checkCurrentVersionURL: "https://coh2stats.com/electron-app-version.json",
checkCurrentVersionLocalDevURL: "http://localhost:3000/electron-app-version.json",
mixpanelProjectToken: "40dec2eae51580f28e7ee646014b95cc",
twitchExtensionUpdateURL:
"https://us-east4-coh2-ladders-prod.cloudfunctions.net/updateTwitchExtData",
};

export default config;
15 changes: 10 additions & 5 deletions packages/app/src/main/gameWatcher/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@ import path from "path";
import fs from "fs";
import { ApplicationStore } from "../applicationStore";
import { actions } from "../../redux/slice";
import { Unsubscribe } from "@reduxjs/toolkit";
import { notifyGameFound } from "./notification";
import { locateWarningsFile } from "./locateWarningsDialog";
import { parseLogFileReverse } from "./parseLogFile";
import { refineLogFileData } from "./refineLogFileData";
import { events } from "../mixpanel";
import { Unsubscribe } from "@reduxjs/toolkit";

export class GameWatcher {
applicationStore: ApplicationStore;
currentIntervalTime: number;
nodeInterval: NodeJS.Timer;
lastGameId: string;
isFirstScan: boolean;
applicationStore: ApplicationStore;
unsubscriber: Unsubscribe;

constructor(applicationStore: ApplicationStore) {
this.isFirstScan = true;
this.applicationStore = applicationStore;
this.unsubscriber = this.applicationStore.runtimeStore.subscribe(this.runtimeStoreSubscriber);
this.isFirstScan = true;
const settings = this.applicationStore.getState().settings;
if (!settings.coh2LogFileFound) {
// check for warnings.log file in expected folder
Expand Down Expand Up @@ -89,7 +90,11 @@ export class GameWatcher {
notifyGameFound();
}
this.isFirstScan = false;
refineLogFileData(result.game).then(
refineLogFileData(
result.game,
result.newGameId,
this.applicationStore.getState().cache.mapStats,
).then(
(gameData) => {
events.new_match_found(gameData.map);
this.applicationStore.dispatch(actions.setGameData(gameData));
Expand Down Expand Up @@ -119,7 +124,7 @@ export class GameWatcher {
this.setInterval(newCheckInterval);
}

destroy(): void {
public destroy(): void {
this.unsubscriber();
clearInterval(this.nodeInterval);
}
Expand Down
200 changes: 196 additions & 4 deletions packages/app/src/main/gameWatcher/refineLogFileData.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import axios, { AxiosResponse } from "axios";
import { GameData, SideData, LadderStats, Member, TeamSide } from "../../redux/state";
import {
GameData,
SideData,
LadderStats,
Member,
TeamSide,
MapStatCache,
Factions,
} from "../../redux/state";
import { LogFileGameData, LogFilePlayerData, LogFileTeamData } from "./parseLogFile";
import {
LeaderboardStat,
Expand Down Expand Up @@ -29,21 +37,53 @@ const teamLeaderboardIdsLookupTable: Record<TeamSide, number[]> = {
* @param logFileGameData Game data from reading the warnings.log file
* @returns Promise with refined game data using the relic api if relic request was successful
*/
export const refineLogFileData = (logFileGameData: LogFileGameData): Promise<GameData> => {
export const refineLogFileData = (
logFileGameData: LogFileGameData,
uniqueId: string,
mapStatCache?: MapStatCache,
): Promise<GameData> => {
return new Promise((resolve, reject) => {
fetchDataFromRelicAPI(logFileGameData).then(
(response: AxiosResponse<PersonalStatResponse>) => {
const apiData = response.data;
if (response.status === 200 && apiData.result.code === 0) {
resolve({
const initialGameData: GameData = {
found: true,
uniqueId: uniqueId,
state: logFileGameData.state,
type: logFileGameData.type,
map: logFileGameData.map,
winCondition: logFileGameData.winCondition,
left: parseSideData(logFileGameData.left, apiData),
right: parseSideData(logFileGameData.right, apiData),
});
mapWinRatioLeft: undefined,
winProbabilityLeft: undefined,
};
addBalanceStats(initialGameData, mapStatCache);
/*if (mapStatCache) {
const mapData = findMapInApiData(mapStatCache.data, initialGameData);
// found map?
if (mapData) {
const factionMatrix = getFactionMatrix(initialGameData);
const winLosses = mapData["factionMatrix"][factionMatrix];
if (winLosses) {
const totalMapCompositionDataPoints = winLosses.wins + winLosses.losses;
if (totalMapCompositionDataPoints > 5) {
const axisMapWinRatio = (winLosses.wins / totalMapCompositionDataPoints) * 100;
const alliesMapWinRatio =
(winLosses.losses / totalMapCompositionDataPoints) * 100;
initialGameData.mapWinRatioLeft = initialGameData.left.side === "axis" ? axisMapWinRatio : alliesMapWinRatio;
if (initialGameData.left.averageLevel && initialGameData.left.averageWinRatio && initialGameData.right.averageLevel && initialGameData.right.averageWinRatio) {
const leftStrength = initialGameData.left.averageLevel * initialGameData.left.averageWinRatio;
const rightStrength = initialGameData.right.averageLevel * initialGameData.right.averageWinRatio;
const leftStrengthRatio = (leftStrength / (leftStrength + rightStrength)) * 100;
initialGameData.winProbabilityLeft = (leftStrengthRatio + initialGameData.mapWinRatioLeft) / 2;
}
}
}
}
}*/
resolve(addBalanceStats(initialGameData, mapStatCache));
} else {
reject();
}
Expand All @@ -55,6 +95,90 @@ export const refineLogFileData = (logFileGameData: LogFileGameData): Promise<Gam
});
};

const addBalanceStats = (game: GameData, mapStatCache?: MapStatCache): GameData => {
if (mapStatCache) {
const mapData = findMapInApiData(mapStatCache.data, game);
// found map?
if (mapData) {
const factionMatrix = getFactionMatrix(game);
const winLosses = mapData["factionMatrix"][factionMatrix];
if (winLosses) {
const totalMapCompositionDataPoints = winLosses.wins + winLosses.losses;
if (totalMapCompositionDataPoints > 5) {
const axisMapWinRatio = (winLosses.wins / totalMapCompositionDataPoints) * 100;
const alliesMapWinRatio = (winLosses.losses / totalMapCompositionDataPoints) * 100;
game.mapWinRatioLeft = game.left.side === "axis" ? axisMapWinRatio : alliesMapWinRatio;
if (
game.left.averageLevel &&
game.left.averageWinRatio &&
game.right.averageLevel &&
game.right.averageWinRatio
) {
const leftStrength = game.left.averageLevel * game.left.averageWinRatio;
const rightStrength = game.right.averageLevel * game.right.averageWinRatio;
const leftStrengthRatio = (leftStrength / (leftStrength + rightStrength)) * 100;
game.winProbabilityLeft = (leftStrengthRatio + game.mapWinRatioLeft) / 2;
}
}
}
}
}
return game;
};

const factionLetterLookupTable: Record<Factions, string> = {
german: "O",
west_german: "W",
british: "B",
soviet: "S",
aef: "U",
};

const getFactionMatrix = (gameData: GameData): string => {
let axis = gameData.left;
let allies = gameData.right;
if (gameData.left.side === "allies") {
axis = gameData.right;
allies = gameData.left;
}
let factionMatrixString = "";
factionMatrixString += axis.solo
.map((stats) => factionLetterLookupTable[stats.members[0].faction])
.sort((a, b) => a.localeCompare(b))
.join("");
factionMatrixString += "x";
factionMatrixString += allies.solo
.map((stats) => factionLetterLookupTable[stats.members[0].faction])
.sort((a, b) => a.localeCompare(b))
.join("");
return factionMatrixString;
};

const getBiggestTeamSize = (gameData: GameData) => {
if (gameData.left.solo.length > gameData.right.solo.length) {
return gameData.left.solo.length;
}
return gameData.right.solo.length;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const findMapInApiData = (apiData: any, gameData: GameData): any | undefined => {
const minMapSize = getBiggestTeamSize(gameData);
for (let i = minMapSize; i <= 4; i++) {
const mapCategory = i + "v" + i;
if (mapCategory in apiData) {
for (const [mapName, mapData] of Object.entries(apiData[mapCategory])) {
if (mapName.replace(/\s/g, "") === gameData.map.replace(/\s/g, "")) {
return mapData;
}
}
} else {
console.error("Unexpected error: Did not find the map category " + mapCategory + " in api");
}
}
return undefined;
};

const parseSideData = (logFileTeam: LogFileTeamData, apiData: PersonalStatResponse): SideData => {
const { statGroups, leaderboardStats } = apiData;
const soloData: LadderStats[] = new Array(logFileTeam.players.length);
Expand Down Expand Up @@ -115,13 +239,81 @@ const parseSideData = (logFileTeam: LogFileTeamData, apiData: PersonalStatRespon
(a, b) => (b.members.length - a.members.length) * 100 + (b.ranklevel - a.ranklevel),
);
findTeamRankingForSoloStats(soloData, sortedTeamData);
// find actual ranking for the game
const countingRanks = findAllStatsForEachPlayerInSide({
side: logFileTeam.side,
solo: soloData,
teams: sortedTeamData,
averageLevel: undefined,
averageWinRatio: undefined,
});
if (allPlayersInSideHaveRanking(countingRanks)) {
// calculate average level
const averageLevel = GetAverageTeamValue(countingRanks, (stats) => stats.ranklevel);
// calculate average win ratio
const averageRatio = GetAverageTeamValue(
countingRanks,
(stats) => (stats.wins / (stats.wins + stats.losses)) * 100,
);
return {
side: logFileTeam.side,
solo: soloData,
teams: sortedTeamData,
averageLevel: averageLevel,
averageWinRatio: averageRatio,
};
}
return {
side: logFileTeam.side,
solo: soloData,
teams: sortedTeamData,
averageLevel: undefined,
averageWinRatio: undefined,
};
};

const allPlayersInSideHaveRanking = (statsMatrix: LadderStats[][]): boolean => {
let allHaveARanking = true;
statsMatrix.forEach((stats) => {
if (stats.length === 0) {
allHaveARanking = false;
}
});
return allHaveARanking;
};

const findAllStatsForEachPlayerInSide = (side: SideData): LadderStats[][] => {
const result: LadderStats[][] = new Array(side.solo.length);
side.solo.forEach((soloStat, index) => {
result[index] = [];
// only include stats with a ranking
if (soloStat.rank > 0) {
result[index].push(soloStat);
}
side.teams.forEach((teamStat) => {
if (teamStat.rank > 0) {
teamStat.members.forEach((teamMember) => {
if (teamMember.relicID === soloStat.members[0].relicID) {
result[index].push(teamStat);
}
});
}
});
});
return result;
};

const GetAverageTeamValue = (
statsMatrix: LadderStats[][],
mapFunc: (stats: LadderStats) => number,
): number => {
const flatLadderStatsArray = statsMatrix.flat(1);
return (
flatLadderStatsArray.map(mapFunc).reduce((a, b) => a + b, 0) / flatLadderStatsArray.length ||
0
);
};

/**
* Looks if players belong to a team and adds the team ranking and level to the players ladder stats
* @param soloData Array of ladder data for each player on one side that will be extended with teamdata
Expand Down
Loading

0 comments on commit ce0d1a7

Please sign in to comment.