From 818e99e973bd8e35af6093379a645114e65ac7bd Mon Sep 17 00:00:00 2001 From: HardiReady Date: Mon, 4 Feb 2019 21:08:37 +0100 Subject: [PATCH 01/30] change scoreboard header row from position fixed to absolute (low res fix) --- package.json | 2 +- .../src/app/statistic/war/scoreboard/scoreboard.component.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 885dd08..66c7f46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opt-cc", - "version": "1.8.6", + "version": "1.8.7", "author": "Florian Hartwich ", "private": true, "scripts": { diff --git a/static/src/app/statistic/war/scoreboard/scoreboard.component.css b/static/src/app/statistic/war/scoreboard/scoreboard.component.css index e7762f8..af48369 100644 --- a/static/src/app/statistic/war/scoreboard/scoreboard.component.css +++ b/static/src/app/statistic/war/scoreboard/scoreboard.component.css @@ -10,7 +10,7 @@ .mat-header-row { width: 1058px; - position: fixed; + position: absolute; z-index: 100; } From b99825b01a8878a5d8adad0539437789109bcd78 Mon Sep 17 00:00:00 2001 From: HardiReady Date: Thu, 7 Feb 2019 21:05:58 +0100 Subject: [PATCH 02/30] Add new server performance stats page --- .gitignore | 2 +- static/src/app/app.component.ts | 1 + static/src/app/statistic/stats.routing.ts | 4 +- .../server-stats/server-stats.component.css | 37 ++++++ .../server-stats/server-stats.component.html | 28 +++++ .../server-stats/server-stats.component.ts | 115 ++++++++++++++++++ .../war/war-header/war-header.component.css | 8 +- .../war/war-header/war-header.component.html | 11 ++ static/src/assets/i18n/statistics/de.json | 1 + static/src/assets/i18n/statistics/en.json | 1 + .../icon/stats/performance-stats-btn.svg | 14 +++ 11 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 static/src/app/statistic/war/server-stats/server-stats.component.css create mode 100644 static/src/app/statistic/war/server-stats/server-stats.component.html create mode 100644 static/src/app/statistic/war/server-stats/server-stats.component.ts create mode 100644 static/src/assets/icon/stats/performance-stats-btn.svg diff --git a/.gitignore b/.gitignore index 952f982..f6b5c0c 100644 --- a/.gitignore +++ b/.gitignore @@ -45,7 +45,7 @@ Thumbs.db .directory # Internal Data -public/ +/public/ mongodb-data/ server/resource/ server/apib/dredd/data/tmp-resource diff --git a/static/src/app/app.component.ts b/static/src/app/app.component.ts index b4e4785..c321351 100644 --- a/static/src/app/app.component.ts +++ b/static/src/app/app.component.ts @@ -52,6 +52,7 @@ export class AppComponent implements OnInit { 'stats-fraction': 'stats/fraction-btn', 'stats-player': 'stats/player-stats-btn', 'stats-scoreboard': 'stats/scoreboard-btn', + 'stats-performance': 'stats/performance-stats-btn', // --SCOREBOARD-- 'death': 'stats/scoreboard/death', 'flagTouch': 'stats/scoreboard/flag-touch', diff --git a/static/src/app/statistic/stats.routing.ts b/static/src/app/statistic/stats.routing.ts index 05bf844..edbe058 100644 --- a/static/src/app/statistic/stats.routing.ts +++ b/static/src/app/statistic/stats.routing.ts @@ -13,6 +13,7 @@ import {WarHeaderComponent} from './war/war-header/war-header.component'; import {LoginGuardMT} from '../login'; import {CampaignNavigationComponent} from './campaign/campaign-navigation/campaign-navigation.component'; import {StatisticOverviewComponent} from './campaign/overview/campaign-overview.component'; +import {ServerStatsComponent} from './war/server-stats/server-stats.component'; export const statsRoutes: Routes = [ @@ -69,5 +70,6 @@ export const statsRouterModule: ModuleWithProviders = RouterModule.forChild(stat export const statsRoutingComponents = [StatisticComponent, StatisticOverviewComponent, StatisticHighScoreComponent, CampaignSubmitComponent, WarListComponent, WarSubmitComponent, WarHeaderComponent, ScoreboardComponent, - FractionStatsComponent, CampaignPlayerDetailComponent, WarItemComponent, CampaignNavigationComponent]; + FractionStatsComponent, CampaignPlayerDetailComponent, WarItemComponent, CampaignNavigationComponent, + ServerStatsComponent]; diff --git a/static/src/app/statistic/war/server-stats/server-stats.component.css b/static/src/app/statistic/war/server-stats/server-stats.component.css new file mode 100644 index 0000000..8b96911 --- /dev/null +++ b/static/src/app/statistic/war/server-stats/server-stats.component.css @@ -0,0 +1,37 @@ +.chart-select-group { + display: flex; + width: fit-content; + margin: auto; +} + +:host /deep/ mat-button-toggle { + color: #666666; + background: #e7e7e7; +} + +:host /deep/ mat-button-toggle:hover { + background: #afafaf; +} + +:host /deep/ mat-button-toggle.mat-button-toggle-checked { + background: #ffffff; +} + +:host /deep/ label.mat-button-toggle-label { + margin: 2px 0; +} + +:host /deep/ div.mat-button-toggle-label-content { + line-height: 25px; + margin-bottom: 0; + font-weight: normal; +} + +.chart-container { + width: 95%; + margin: 2%; + min-width: 900px; + height: 50vh; + padding: 15px; + float: left; +} diff --git a/static/src/app/statistic/war/server-stats/server-stats.component.html b/static/src/app/statistic/war/server-stats/server-stats.component.html new file mode 100644 index 0000000..452c16c --- /dev/null +++ b/static/src/app/statistic/war/server-stats/server-stats.component.html @@ -0,0 +1,28 @@ +
+ + + + {{label | translate}} + + + +
+ + +
+
diff --git a/static/src/app/statistic/war/server-stats/server-stats.component.ts b/static/src/app/statistic/war/server-stats/server-stats.component.ts new file mode 100644 index 0000000..ea131ba --- /dev/null +++ b/static/src/app/statistic/war/server-stats/server-stats.component.ts @@ -0,0 +1,115 @@ +import {Component, ElementRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild} from '@angular/core'; +import {ChartUtils} from '../../../utils/chart-utils'; +import {War} from '../../../models/model-interfaces'; +import {TranslateService} from '@ngx-translate/core'; + + +@Component({ + selector: 'cc-server-statistics', + templateUrl: './server-stats.component.html', + styleUrls: ['./server-stats.component.css', '../../../style/list-entry.css', '../../../style/hide-scrollbar.css'] +}) +export class ServerStatsComponent implements OnInit, OnChanges { + + @ViewChild('overview') private overviewContainer: ElementRef; + + @Input() war: War; + + @Input() performanceData: any; + + startDateObj: Date; + + initialized: any; + + public activeChartSelect: string; + + lineChartData: any[] = []; + + tmpPointData; + tmpBudgetData; + tmpKillData; + tmpFrienlyFireData; + tmpVehicleData; + tmpTransportData; + tmpReviveData; + tmpStabilizeData; + tmpFlagCaptureData; + + readonly labels = { + points: 'stats.fraction.select.points', + budget: 'stats.fraction.select.budget', + kill: 'stats.fraction.select.kills', + friendlyFire: 'stats.fraction.select.friendly.fire', + vehicle: 'stats.fraction.select.vehicle.kills', + }; + readonly labelsAsString = Object.keys(this.labels) + .map((key) => this.labels[key]); + + lineChartLabel: string; + + gradient = false; + yAxis = true; + xAxis = true; + legend = false; + legendTitle = false; + showXAxisLabel = false; + showYAxisLabel = true; + autoscale = true; + timeline = false; + roundDomains = true; + + constructor(private translate: TranslateService) { + } + + ngOnInit() { + this.setLineChartLabel(this.labels.points); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.war || changes.performanceData) { + this.initialized = { + budget: false, + kill: false, + revive: false, + transport: false, + flag: false + }; + Object.assign(this, [this.lineChartData]); + this.activeChartSelect = this.labels.points; + + this.startDateObj = new Date(this.war.date); + this.startDateObj.setHours(0); + this.startDateObj.setMinutes(1); + } + } + + selectChart(newSelection) { + this.activeChartSelect = newSelection; + this.setLineChartLabel(this.activeChartSelect); + console.log('############### apply data ##################'); + } + + addFinalTimeData(tmpCollection) { + const endDate = new Date(this.war.endDate); + if (tmpCollection === this.tmpBudgetData) { + this.tmpBudgetData[0].series.push(ChartUtils.getSeriesEntry(endDate, this.war.endBudgetBlufor)); + this.tmpBudgetData[1].series.push(ChartUtils.getSeriesEntry(endDate, this.war.endBudgetOpfor)); + } else { + for (let j = 0; j < tmpCollection.length; j++) { + // mayBe check is needed for logs that are simply not existent in older wars, i.e. vehicleKills + const maybeLast = tmpCollection[j].series[tmpCollection[j].series.length - 1]; + if (maybeLast && maybeLast.name < endDate) { + tmpCollection[j].series.push( + ChartUtils.getSeriesEntry(endDate, tmpCollection[j].series[tmpCollection[j].series.length - 1].value) + ); + } + } + } + } + + setLineChartLabel(i18n: string) { + this.translate.get(i18n).subscribe((translated) => { + this.lineChartLabel = translated; + }); + } +} diff --git a/static/src/app/statistic/war/war-header/war-header.component.css b/static/src/app/statistic/war/war-header/war-header.component.css index 7623793..605fbc3 100644 --- a/static/src/app/statistic/war/war-header/war-header.component.css +++ b/static/src/app/statistic/war/war-header/war-header.component.css @@ -16,7 +16,7 @@ form.tab-control { } span.tab-control { - margin: 4px 67px; + margin: 4px 30px 4px 55px; padding: 4px 16px; } @@ -39,8 +39,12 @@ span.tab-control { vertical-align: middle; } +:host/deep/.mat-icon-stats-performance g { + stroke: #666666; +} + .nav-tabs { - width: 920px; + width: 1000px; margin: auto; clear: both; border-bottom: 0; diff --git a/static/src/app/statistic/war/war-header/war-header.component.html b/static/src/app/statistic/war/war-header/war-header.component.html index 81e1d7b..e617a1b 100644 --- a/static/src/app/statistic/war/war-header/war-header.component.html +++ b/static/src/app/statistic/war/war-header/war-header.component.html @@ -48,6 +48,12 @@ {{'stats.scoreboard.tab.player' | translate}} +
  • @@ -100,6 +106,11 @@ [playerName]="playerDetailName" (switchTab)="switchTab($event)"> + +
    diff --git a/static/src/assets/i18n/statistics/de.json b/static/src/assets/i18n/statistics/de.json index beae2a7..5282f8a 100644 --- a/static/src/assets/i18n/statistics/de.json +++ b/static/src/assets/i18n/statistics/de.json @@ -41,6 +41,7 @@ "stats.scoreboard.tab.scoreboard": "Scoreboard", "stats.scoreboard.tab.fractions": "Fraktionen", "stats.scoreboard.tab.player": "Spieler", + "stats.scoreboard.tab.performance": "Performance", "stats.scoreboard.fraction.filter.all": "Alle", "stats.scoreboard.header.player": "Spieler", "stats.scoreboard.header.fraction": "Fraktion", diff --git a/static/src/assets/i18n/statistics/en.json b/static/src/assets/i18n/statistics/en.json index f412529..cc4509e 100644 --- a/static/src/assets/i18n/statistics/en.json +++ b/static/src/assets/i18n/statistics/en.json @@ -49,6 +49,7 @@ "stats.scoreboard.tab.scoreboard": "Scoreboard", "stats.scoreboard.tab.fractions": "Fractions", "stats.scoreboard.tab.player": "Player", + "stats.scoreboard.tab.performance": "Performance", "stats.scoreboard.fraction.filter.all": "All", "stats.scoreboard.header.player": "Player", "stats.scoreboard.header.fraction": "Fraction", diff --git a/static/src/assets/icon/stats/performance-stats-btn.svg b/static/src/assets/icon/stats/performance-stats-btn.svg new file mode 100644 index 0000000..550d130 --- /dev/null +++ b/static/src/assets/icon/stats/performance-stats-btn.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + From 2d64c325982a0537b6921c7226036c09f44a4dc0 Mon Sep 17 00:00:00 2001 From: HardiReady Date: Thu, 7 Feb 2019 21:44:25 +0100 Subject: [PATCH 03/30] Add chart select label names --- .../war/server-stats/server-stats.component.ts | 14 +++++++------- static/src/assets/i18n/statistics/de.json | 6 ++++++ static/src/assets/i18n/statistics/en.json | 6 ++++++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/static/src/app/statistic/war/server-stats/server-stats.component.ts b/static/src/app/statistic/war/server-stats/server-stats.component.ts index ea131ba..6ed098d 100644 --- a/static/src/app/statistic/war/server-stats/server-stats.component.ts +++ b/static/src/app/statistic/war/server-stats/server-stats.component.ts @@ -36,11 +36,11 @@ export class ServerStatsComponent implements OnInit, OnChanges { tmpFlagCaptureData; readonly labels = { - points: 'stats.fraction.select.points', - budget: 'stats.fraction.select.budget', - kill: 'stats.fraction.select.kills', - friendlyFire: 'stats.fraction.select.friendly.fire', - vehicle: 'stats.fraction.select.vehicle.kills', + singleAvg: 'stats.performance.select.single.avg', + singleMin: 'stats.performance.select.single.min', + avgTimeline: 'stats.performance.select.timeline.avg', + minTimeline: 'stats.performance.select.timeline.min', + serverFps: 'stats.performance.select.timeline.server', }; readonly labelsAsString = Object.keys(this.labels) .map((key) => this.labels[key]); @@ -62,7 +62,7 @@ export class ServerStatsComponent implements OnInit, OnChanges { } ngOnInit() { - this.setLineChartLabel(this.labels.points); + this.setLineChartLabel(this.labels.singleAvg); } ngOnChanges(changes: SimpleChanges) { @@ -75,7 +75,7 @@ export class ServerStatsComponent implements OnInit, OnChanges { flag: false }; Object.assign(this, [this.lineChartData]); - this.activeChartSelect = this.labels.points; + this.activeChartSelect = this.labels.singleAvg; this.startDateObj = new Date(this.war.date); this.startDateObj.setHours(0); diff --git a/static/src/assets/i18n/statistics/de.json b/static/src/assets/i18n/statistics/de.json index 5282f8a..dd3c09a 100644 --- a/static/src/assets/i18n/statistics/de.json +++ b/static/src/assets/i18n/statistics/de.json @@ -28,6 +28,12 @@ "stats.fraction.select.stabilize": "Stabilisiert", "stats.fraction.select.flag": "Flaggenbesitz", + "stats.performance.select.single.avg": "Spieler FPS Durchschnitt", + "stats.performance.select.single.min": "Spieler FPS Minimum", + "stats.performance.select.timeline.avg": "Verlauf Gesamt FPS", + "stats.performance.select.timeline.min": "Verlauf Minimum FPS", + "stats.performance.select.timeline.server": "Server FPS", + "stats.player.detail.headline": "Kampagnendetails - {{name}}", "stats.player.detail.button.back": "Zurück", "stats.player.detail.kill.death.ratio": "Kill/Death", diff --git a/static/src/assets/i18n/statistics/en.json b/static/src/assets/i18n/statistics/en.json index cc4509e..7634312 100644 --- a/static/src/assets/i18n/statistics/en.json +++ b/static/src/assets/i18n/statistics/en.json @@ -36,6 +36,12 @@ "stats.fraction.select.stabilize": "Stabilized", "stats.fraction.select.flag": "Flag Possession", + "stats.performance.select.single.avg": "Player FPS Average", + "stats.performance.select.single.min": "Player FPS Minimum", + "stats.performance.select.timeline.avg": "Timeline Overall FPS", + "stats.performance.select.timeline.min": "Timeline Minimum FPS", + "stats.performance.select.timeline.server": "Server FPS", + "stats.player.detail.headline": "Campaign Details - {{name}}", "stats.player.detail.button.back": "Back", "stats.player.detail.kill.death.ratio": "Kill/Death", From 712071865f51e566ee04c9296ebf0f7b4af6c8cf Mon Sep 17 00:00:00 2001 From: HardiReady Date: Fri, 8 Feb 2019 22:34:50 +0100 Subject: [PATCH 04/30] Prepare fraction stats page select buttons for alternating data availability --- .../fraction-stats.component.html | 4 +- .../fraction-stats.component.ts | 70 +++++++++++++++---- static/src/assets/i18n/statistics/de.json | 5 +- static/src/assets/i18n/statistics/en.json | 5 +- 4 files changed, 65 insertions(+), 19 deletions(-) diff --git a/static/src/app/statistic/war/fraction-stats/fraction-stats.component.html b/static/src/app/statistic/war/fraction-stats/fraction-stats.component.html index dc1e762..54285f1 100644 --- a/static/src/app/statistic/war/fraction-stats/fraction-stats.component.html +++ b/static/src/app/statistic/war/fraction-stats/fraction-stats.component.html @@ -4,8 +4,8 @@ #group="matButtonToggleGroup" [(ngModel)]="activeChartSelect" (change)="selectChart(group.value)"> - - {{label | translate}} + + {{labels[labelKey] | translate}} diff --git a/static/src/app/statistic/war/fraction-stats/fraction-stats.component.ts b/static/src/app/statistic/war/fraction-stats/fraction-stats.component.ts index b86a3f0..2460707 100644 --- a/static/src/app/statistic/war/fraction-stats/fraction-stats.component.ts +++ b/static/src/app/statistic/war/fraction-stats/fraction-stats.component.ts @@ -39,24 +39,15 @@ export class FractionStatsComponent implements OnInit, OnChanges { tmpReviveData; tmpStabilizeData; tmpFlagCaptureData; + tmpPlayerCountData; colorScheme = { domain: [Fraction.COLOR_BLUFOR, Fraction.COLOR_OPFOR, Fraction.COLOR_BLUFOR_LIGHT, Fraction.COLOR_OPFOR_LIGHT, Fraction.COLOR_BLUFOR_DARK, Fraction.COLOR_OPFOR_DARK, Fraction.COLOR_BLUFOR_GREY, Fraction.COLOR_OPFOR_GREY] }; - readonly labels = { - points: 'stats.fraction.select.points', - budget: 'stats.fraction.select.budget', - kill: 'stats.fraction.select.kills', - friendlyFire: 'stats.fraction.select.friendly.fire', - vehicle: 'stats.fraction.select.vehicle.kills', - transport: 'stats.fraction.select.air.transport', - revive: 'stats.fraction.select.revive', - stabilize: 'stats.fraction.select.stabilize', - flag: 'stats.fraction.select.flag' - }; - readonly labelsAsString = Object.keys(this.labels).map((key) => this.labels[key]); + labels; + labelsKeys; lineChartLabel: string; @@ -87,8 +78,12 @@ export class FractionStatsComponent implements OnInit, OnChanges { kill: false, revive: false, transport: false, - flag: false + flag: false, + playerCount: false }; + + this.initializeToggleButtons(); + Object.assign(this, [this.lineChartData, this.areaChartData]); this.activeChartSelect = this.labels.points; @@ -99,6 +94,42 @@ export class FractionStatsComponent implements OnInit, OnChanges { } } + /** + * show only labels for for data that is available, + * to not end up with empty charts in old battles + */ + initializeToggleButtons() { + const newLabels = {}; + if (this.logData.points && this.logData.points.length > 0) { + newLabels['points'] = 'stats.fraction.select.points'; + } + if (this.logData.budget && this.logData.budget.length > 0) { + newLabels['budget'] = 'stats.fraction.select.budget'; + } + if (this.logData.kill && this.logData.kill.length > 0) { + newLabels['kill'] = 'stats.fraction.select.kills'; + newLabels['friendlyFire'] = 'stats.fraction.select.friendly.fire'; + } + if (this.logData.vehicle && this.logData.vehicle.length > 0) { + newLabels['vehicle'] = 'stats.fraction.select.vehicle.kills'; + } + if (this.logData.transport && this.logData.transport.length > 0) { + newLabels['transport'] = 'stats.fraction.select.air.transport'; + } + if (this.logData.revive && this.logData.revive.length > 0) { + newLabels['revive'] = 'stats.fraction.select.revive'; + newLabels['stabilize'] = 'stats.fraction.select.stabilize'; + } + if (this.logData.flag && this.logData.flag.length > 0) { + newLabels['flag'] = 'stats.fraction.select.flag'; + } + if (this.logData.playerCount && this.logData.playerCount.length > 0) { + newLabels['playerCount'] = 'stats.fraction.select.player.count'; + } + this.labels = newLabels; + this.labelsKeys = Object.keys(this.labels); + } + selectChart(newSelection) { this.activeChartSelect = newSelection; if (this.activeChartSelect !== this.labels.flag) { @@ -136,6 +167,10 @@ export class FractionStatsComponent implements OnInit, OnChanges { this.initTransportData(); this.lineChartData = this.tmpTransportData; break; + case this.labels.playerCount: + this.initPlayerCountData(); + this.lineChartData = this.tmpPlayerCountData; + break; } } else { this.initFlagHoldData(); @@ -347,6 +382,15 @@ export class FractionStatsComponent implements OnInit, OnChanges { this.initialized.transport = true; } + + initPlayerCountData() { + if (this.initialized.playerCount) { + return; + } + this.addFinalTimeData(this.tmpPlayerCountData); + this.initialized.playerCount = true; + } + initFlagHoldData() { if (this.initialized.flag) { return; diff --git a/static/src/assets/i18n/statistics/de.json b/static/src/assets/i18n/statistics/de.json index dd3c09a..509e70f 100644 --- a/static/src/assets/i18n/statistics/de.json +++ b/static/src/assets/i18n/statistics/de.json @@ -27,11 +27,12 @@ "stats.fraction.select.revive": "Revive", "stats.fraction.select.stabilize": "Stabilisiert", "stats.fraction.select.flag": "Flaggenbesitz", + "stats.fraction.select.player.count": "Spieleranzahl", "stats.performance.select.single.avg": "Spieler FPS Durchschnitt", "stats.performance.select.single.min": "Spieler FPS Minimum", - "stats.performance.select.timeline.avg": "Verlauf Gesamt FPS", - "stats.performance.select.timeline.min": "Verlauf Minimum FPS", + "stats.performance.select.timeline.avg": "Verlauf FPS Durchschnitt", + "stats.performance.select.timeline.min": "Verlauf FPS Minimum", "stats.performance.select.timeline.server": "Server FPS", "stats.player.detail.headline": "Kampagnendetails - {{name}}", diff --git a/static/src/assets/i18n/statistics/en.json b/static/src/assets/i18n/statistics/en.json index 7634312..e45df1e 100644 --- a/static/src/assets/i18n/statistics/en.json +++ b/static/src/assets/i18n/statistics/en.json @@ -35,11 +35,12 @@ "stats.fraction.select.revive": "Revive", "stats.fraction.select.stabilize": "Stabilized", "stats.fraction.select.flag": "Flag Possession", + "stats.fraction.select.player.count": "Player Count", "stats.performance.select.single.avg": "Player FPS Average", "stats.performance.select.single.min": "Player FPS Minimum", - "stats.performance.select.timeline.avg": "Timeline Overall FPS", - "stats.performance.select.timeline.min": "Timeline Minimum FPS", + "stats.performance.select.timeline.avg": "Timeline FPS Average", + "stats.performance.select.timeline.min": "Timeline FPS Minimum", "stats.performance.select.timeline.server": "Server FPS", "stats.player.detail.headline": "Campaign Details - {{name}}", From 2538f1838ab79939f58d34e41f6adb0c98119b28 Mon Sep 17 00:00:00 2001 From: HardiReady Date: Sat, 9 Feb 2019 11:54:16 +0100 Subject: [PATCH 05/30] Add player count for log parsing --- server/models/logs/player-count.js | 34 ++++++++++++++++++++++ server/routes/logs.js | 3 ++ server/routes/wars.js | 45 +++++++++++++++++------------- server/tools/log-parse-tool.js | 20 +++++++++++++ 4 files changed, 82 insertions(+), 20 deletions(-) create mode 100644 server/models/logs/player-count.js diff --git a/server/models/logs/player-count.js b/server/models/logs/player-count.js new file mode 100644 index 0000000..ee17850 --- /dev/null +++ b/server/models/logs/player-count.js @@ -0,0 +1,34 @@ +'use strict'; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const LogPlayerCountSchema = new Schema({ + war: { + type: mongoose.Schema.Types.ObjectId, + ref: 'War', + required: true, + }, + time: { + type: Date, + required: true, + }, + countBlufor: { + type: Number, + get: (v) => Math.round(v), + set: (v) => Math.round(v), + required: true, + }, + countOpfor: { + type: Number, + get: (v) => Math.round(v), + set: (v) => Math.round(v), + required: true, + }, +}, { + collection: 'logPlayerCount', +}); +// optional more indices +LogPlayerCountSchema.index({war: 1}); + +module.exports = mongoose.model('LogPlayerCount', LogPlayerCountSchema); diff --git a/server/routes/logs.js b/server/routes/logs.js index e277a41..6727365 100644 --- a/server/routes/logs.js +++ b/server/routes/logs.js @@ -15,6 +15,7 @@ const LogVehicleModel = require('../models/logs/vehicle'); const LogTransportModel = require('../models/logs/transport'); const LogFlagModel = require('../models/logs/flag'); const LogPointsModel = require('../models/logs/points'); +const LogPlayerCountModel = require('../models/logs/player-count'); const logsRouter = new express.Router(); @@ -45,6 +46,7 @@ logsRouter.route('/:warId') const vehicleObjects = LogVehicleModel.find(filter, {}, sort); const transportObjects = LogTransportModel.find(filter, {}, sort); const flagObjects = LogFlagModel.find(filter, {}, sort); + const playerCountObjects = LogPlayerCountModel.find(filter, {}, sort); const resources = { points: pointsObjects.exec.bind(pointsObjects), budget: budgetObjects.exec.bind(budgetObjects), @@ -54,6 +56,7 @@ logsRouter.route('/:warId') vehicle: killObjects.exec.bind(vehicleObjects), transport: transportObjects.exec.bind(transportObjects), flag: flagObjects.exec.bind(flagObjects), + playerCount: playerCountObjects.exec.bind(playerCountObjects), }; async.parallel(resources, (error, results) => { diff --git a/server/routes/wars.js b/server/routes/wars.js index 692e37f..993c99f 100644 --- a/server/routes/wars.js +++ b/server/routes/wars.js @@ -33,6 +33,7 @@ const LogTransportModel = require('../models/logs/transport'); const LogFlagModel = require('../models/logs/flag'); const LogBudgetModel = require('../models/logs/budget'); const LogPointsModel = require('../models/logs/points'); +const LogPlayerCountModel = require('../models/logs/player-count'); // util const genericPatch = require('./_generic').genericPatch; @@ -76,6 +77,8 @@ wars.route('/') if (err) { return next(err); } + + console.log(statsResult.playerCount) LogKillModel.create(statsResult.kills, () => { LogVehicleKillModel.create(statsResult.vehicles, () => { LogRespawnModel.create(statsResult.respawn, () => { @@ -83,28 +86,30 @@ wars.route('/') LogFlagModel.create(statsResult.flag, () => { LogBudgetModel.create(statsResult.budget, () => { LogTransportModel.create(statsResult.transport, () => { - LogPointsModel.create(statsResult.points, () => { - const folderName = resourceLocation.concat(war._id); - mkdirp(folderName, (err) => { - if (err) return next(err); + LogPlayerCountModel.create(statsResult.playerCount, () => { + LogPointsModel.create(statsResult.points, () => { + const folderName = resourceLocation.concat(war._id); + mkdirp(folderName, (err) => { + if (err) return next(err); - // save clean log file - const cleanFile = fs.createWriteStream(folderName + '/clean.log'); - statsResult.clean.forEach((cleanLine) => { - cleanFile.write(cleanLine + '\n\n'); + // save clean log file + const cleanFile = fs.createWriteStream(folderName + '/clean.log'); + statsResult.clean.forEach((cleanLine) => { + cleanFile.write(cleanLine + '\n\n'); + }); + cleanFile.end(); + + // save raw log file + const rawFile = fs.createWriteStream(folderName + '/war.log'); + lineArray.forEach((rawLine) => { + rawFile.write(rawLine + '\n'); + }); + rawFile.end(); + + res.status(codes.created); + res.locals.items = war; + next(); }); - cleanFile.end(); - - // save raw log file - const rawFile = fs.createWriteStream(folderName + '/war.log'); - lineArray.forEach((rawLine) => { - rawFile.write(rawLine + '\n'); - }); - rawFile.end(); - - res.status(codes.created); - res.locals.items = war; - next(); }); }); }); diff --git a/server/tools/log-parse-tool.js b/server/tools/log-parse-tool.js index 2152ab3..bdaaf63 100644 --- a/server/tools/log-parse-tool.js +++ b/server/tools/log-parse-tool.js @@ -23,6 +23,12 @@ const vehicleRegex = /(vehicle:\s(.*?)\))/; const categoryRegex = /(category:\s(.*?)\))/; +const bluforPlayerCountRegex = /NATO\s(\d*)/; + +const opforPlayerCountRegex = /CSAT\s(\d*)/; + +const timestampRegex= /LOG:\s(\d*:\d*:\d*)\s---/; + const parseWarLog = (lineArray, war) => { let flagBlufor = true; let flagOpfor = true; @@ -41,6 +47,7 @@ const parseWarLog = (lineArray, war) => { flag: [], transport: [], players: [], + playerCount: [], }; const VEHICLE_BLACKLIST = [ @@ -245,6 +252,19 @@ const parseWarLog = (lineArray, war) => { fraction: driver ? driver.fraction : 'NONE', distance: distance, }); + } else if (line.includes('(Spieleranzahl)')) { + stats.clean.push(line); + + const timestamp = (timestampRegex.exec(line))[1]; + const countBlufor = (bluforPlayerCountRegex.exec(line))[1]; + const countOpfor = (opforPlayerCountRegex.exec(line))[1]; + + stats.playerCount.push({ + war: war.id, + time: getFullTimeDate(war.date, timestamp), + countBlufor: countBlufor, + countOpfor: countOpfor, + }); } else if (line.includes('(Fraktionsuebersicht)') || line.includes('Fraktionsübersicht')) { /** * PLAYERS From f34a748d7f173dc3dd0d84c7a5e56a042f0ffa07 Mon Sep 17 00:00:00 2001 From: HardiReady Date: Sat, 9 Feb 2019 12:07:09 +0100 Subject: [PATCH 06/30] Add player count graph print in FE --- .../war/fraction-stats/fraction-stats.component.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/static/src/app/statistic/war/fraction-stats/fraction-stats.component.ts b/static/src/app/statistic/war/fraction-stats/fraction-stats.component.ts index 2460707..2625b39 100644 --- a/static/src/app/statistic/war/fraction-stats/fraction-stats.component.ts +++ b/static/src/app/statistic/war/fraction-stats/fraction-stats.component.ts @@ -382,13 +382,18 @@ export class FractionStatsComponent implements OnInit, OnChanges { this.initialized.transport = true; } - initPlayerCountData() { if (this.initialized.playerCount) { return; } - this.addFinalTimeData(this.tmpPlayerCountData); + this.logData.playerCount.forEach(playerCountEntry => { + this.tmpPlayerCountData[0].series.push( + ChartUtils.getSeriesEntry(new Date(playerCountEntry.time), playerCountEntry.countBlufor)); + this.tmpPlayerCountData[1].series.push( + ChartUtils.getSeriesEntry(new Date(playerCountEntry.time), playerCountEntry.countOpfor)); + }); this.initialized.playerCount = true; + this.addFinalTimeData(this.tmpPlayerCountData); } initFlagHoldData() { @@ -431,6 +436,7 @@ export class FractionStatsComponent implements OnInit, OnChanges { this.tmpReviveData = ChartUtils.getMultiDataArray(Fraction.BLUFOR, Fraction.OPFOR); this.tmpStabilizeData = ChartUtils.getMultiDataArray(Fraction.BLUFOR, Fraction.OPFOR); this.tmpFlagCaptureData = ChartUtils.getMultiDataArray(Fraction.BLUFOR, Fraction.OPFOR); + this.tmpPlayerCountData = ChartUtils.getMultiDataArray(Fraction.BLUFOR, Fraction.OPFOR); [this.tmpKillData, this.tmpFrienlyFireData, this.tmpVehicleData, this.tmpReviveData, this.tmpStabilizeData, this.tmpTransportData].forEach(tmp => { From a3665b547630ba32f649d69d59909379d7118ce3 Mon Sep 17 00:00:00 2001 From: HardiReady Date: Sat, 9 Feb 2019 12:41:02 +0100 Subject: [PATCH 07/30] Add delet db entries player count on delete war --- server/routes/wars.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routes/wars.js b/server/routes/wars.js index 993c99f..9bb1fac 100644 --- a/server/routes/wars.js +++ b/server/routes/wars.js @@ -78,7 +78,6 @@ wars.route('/') return next(err); } - console.log(statsResult.playerCount) LogKillModel.create(statsResult.kills, () => { LogVehicleKillModel.create(statsResult.vehicles, () => { LogRespawnModel.create(statsResult.respawn, () => { @@ -182,6 +181,7 @@ wars.route('/:id') LogBudgetModel.find({war: item._id}).remove().exec(); LogTransportModel.find({war: item._id}).remove().exec(); LogPointsModel.find({war: item._id}).remove().exec(); + LogPlayerCountModel.find({war: item._id}).remove().exec(); // check if logfiles exist and delete from fs const warDir = resourceLocation + req.params.id; From 82f3dc923fa05f4957630258e2de2213bb3f2e60 Mon Sep 17 00:00:00 2001 From: HardiReady Date: Sat, 9 Feb 2019 13:16:41 +0100 Subject: [PATCH 08/30] Delete campaign war resources and logs on campaign delete (CC-78) --- server/routes/campaigns.js | 71 ++++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/server/routes/campaigns.js b/server/routes/campaigns.js index e292624..37b8135 100644 --- a/server/routes/campaigns.js +++ b/server/routes/campaigns.js @@ -12,9 +12,22 @@ const checkMT = require('../middleware/permission-check').checkMT; const routerHandling = require('../middleware/router-handling'); const idValidator = require('../middleware/validators').idValidator; +const fs = require('fs'); +const resourceLocation = require('../middleware/resource-location').resourceLocation().concat('/logs/'); + // Mongoose Model using mongoDB const CampaignModel = require('../models/campaign'); const WarModel = require('../models/war'); +const PlayerModel = require('../models/player'); +const LogKillModel = require('../models/logs/kill'); +const LogVehicleKillModel = require('../models/logs/vehicle'); +const LogRespawnModel = require('../models/logs/respawn'); +const LogReviveModel = require('../models/logs/revive'); +const LogTransportModel = require('../models/logs/transport'); +const LogFlagModel = require('../models/logs/flag'); +const LogBudgetModel = require('../models/logs/budget'); +const LogPointsModel = require('../models/logs/points'); +const LogPlayerCountModel = require('../models/logs/player-count'); // util const genericGetById = require('./_generic').genericGetById; @@ -88,20 +101,50 @@ campaigns.route('/:id') }) .delete((req, res, next) => { - CampaignModel.findByIdAndRemove(req.params.id, (err, item) => { - if (err) { - err.status = codes.wrongrequest; - return next(err); - } else if (!item) { - err = new Error('item not found'); - err.status = codes.notfound; - return next(err); - } - WarModel.find({campaign: req.params.id}).remove().exec(); - // TODO: remove all the war logs from fs here!!! - // TODO: remove all LOG entries from DB!!! - res.locals.processed = true; - next(); + WarModel.find({campaign: req.params.id}, (err, wars) => { + + wars.forEach((item) => { + // delete linked appearances + PlayerModel.find({warId: item._id}).remove().exec(); + LogKillModel.find({war: item._id}).remove().exec(); + LogVehicleKillModel.find({war: item._id}).remove().exec(); + LogRespawnModel.find({war: item._id}).remove().exec(); + LogReviveModel.find({war: item._id}).remove().exec(); + LogFlagModel.find({war: item._id}).remove().exec(); + LogBudgetModel.find({war: item._id}).remove().exec(); + LogTransportModel.find({war: item._id}).remove().exec(); + LogPointsModel.find({war: item._id}).remove().exec(); + LogPlayerCountModel.find({war: item._id}).remove().exec(); + + // check if logfiles exist and delete from fs + const warDir = resourceLocation + item.id; + if (fs.existsSync(warDir)) { + const cleanLog = warDir + '/clean.log'; + if (fs.existsSync(cleanLog)) { + fs.unlink(cleanLog, (err) => { + }); + } + const sourceLog = warDir + '/war.log'; + if (fs.existsSync(sourceLog)) { + fs.unlink(sourceLog, (err) => { + }); + } + fs.rmdir(warDir, (err) => { + }); + } + }); + CampaignModel.findByIdAndRemove(req.params.id, (err, item) => { + if (err) { + err.status = codes.wrongrequest; + return next(err); + } else if (!item) { + err = new Error('item not found'); + err.status = codes.notfound; + return next(err); + } + res.locals.processed = true; + next(); + }); }); }) From 9e6c52648b180f60df41491edb6b55651382a3fb Mon Sep 17 00:00:00 2001 From: HardiReady Date: Sat, 9 Feb 2019 13:17:32 +0100 Subject: [PATCH 09/30] Fix lint --- server/routes/campaigns.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/routes/campaigns.js b/server/routes/campaigns.js index 37b8135..bf72959 100644 --- a/server/routes/campaigns.js +++ b/server/routes/campaigns.js @@ -102,7 +102,6 @@ campaigns.route('/:id') .delete((req, res, next) => { WarModel.find({campaign: req.params.id}, (err, wars) => { - wars.forEach((item) => { // delete linked appearances PlayerModel.find({warId: item._id}).remove().exec(); From 3c02c353e730773082297158676f7b03c4306946 Mon Sep 17 00:00:00 2001 From: HardiReady Date: Sat, 9 Feb 2019 13:18:22 +0100 Subject: [PATCH 10/30] Update version label --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 66c7f46..7309a5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opt-cc", - "version": "1.8.7", + "version": "1.9.0", "author": "Florian Hartwich ", "private": true, "scripts": { From de05260298ec68a20d95de50406c68685bb1f5ea Mon Sep 17 00:00:00 2001 From: HardiReady Date: Sat, 9 Feb 2019 14:24:14 +0100 Subject: [PATCH 11/30] remove version key from db log entries --- docs/mongo-db-schema.odg | Bin 15869 -> 16309 bytes server/models/logs/budget.js | 1 + server/models/logs/flag.js | 1 + server/models/logs/kill.js | 1 + server/models/logs/player-count.js | 1 + server/models/logs/points.js | 1 + server/models/logs/respawn.js | 1 + server/models/logs/revive.js | 1 + server/models/logs/transport.js | 1 + server/models/logs/vehicle.js | 1 + 10 files changed, 9 insertions(+) diff --git a/docs/mongo-db-schema.odg b/docs/mongo-db-schema.odg index 0287454c3ab1cbecb10f0dff32445c2952faa328..c7ab0b07c47324ba22aa53fb71e956474c4880b7 100644 GIT binary patch delta 14195 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 z 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 f|5E2a-xaL?0%>r@pmO8HWC) Date: Sat, 9 Feb 2019 18:51:01 +0100 Subject: [PATCH 12/30] Update schema docs --- .gitignore | 2 -- docs/mongo-db-schema.odg | Bin 16309 -> 16432 bytes server/models/logs/server-fps.js | 35 +++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 server/models/logs/server-fps.js diff --git a/.gitignore b/.gitignore index f6b5c0c..9e2a547 100644 --- a/.gitignore +++ b/.gitignore @@ -46,7 +46,6 @@ Thumbs.db # Internal Data /public/ -mongodb-data/ server/resource/ server/apib/dredd/data/tmp-resource backup/ @@ -59,4 +58,3 @@ backup/ .cache/motd.legal-displayed .profile .ssh/ - diff --git a/docs/mongo-db-schema.odg b/docs/mongo-db-schema.odg index c7ab0b07c47324ba22aa53fb71e956474c4880b7..26737ec27baa5749636652a6110e00c17cb026d1 100644 GIT binary patch delta 14734 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 Ls4%anHgecW@d`<&$s(_|Myfi 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 diff --git a/server/models/logs/server-fps.js b/server/models/logs/server-fps.js new file mode 100644 index 0000000..99aa0ae --- /dev/null +++ b/server/models/logs/server-fps.js @@ -0,0 +1,35 @@ +'use strict'; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const LogServerFpsSchema = new Schema({ + war: { + type: mongoose.Schema.Types.ObjectId, + ref: 'War', + required: true, + }, + entityName: { + type: String, + required: true, + }, + avgFps: [{ + type: Number, + get: (v) => Math.round(v), + set: (v) => Math.round(v), + required: true, + }], + minFps: [{ + type: Number, + get: (v) => Math.round(v), + set: (v) => Math.round(v), + required: true, + }], +}, { + collection: 'logServerFps', + versionKey: false +}); +// optional more indices +LogServerFpsSchema.index({war: 1}); + +module.exports = mongoose.model('LogServerFpsSchema', LogServerFpsSchema); From 03073f5436c95ca6fee54b5544d593628656f1b2 Mon Sep 17 00:00:00 2001 From: HardiReady Date: Sat, 9 Feb 2019 20:42:24 +0100 Subject: [PATCH 13/30] Add parsing and persisting for FPS logs (CC-80) --- docs/mongo-db-schema.odg | Bin 16432 -> 16708 bytes server/models/logs/budget.js | 2 +- server/models/logs/flag.js | 2 +- server/models/logs/kill.js | 2 +- server/models/logs/player-count.js | 2 +- server/models/logs/points.js | 2 +- server/models/logs/respawn.js | 2 +- server/models/logs/revive.js | 2 +- server/models/logs/server-fps.js | 14 ++++- server/models/logs/transport.js | 2 +- server/models/logs/vehicle.js | 2 +- server/models/player.js | 5 ++ server/routes/wars.js | 84 ++++++++++++++++------------- server/tools/log-parse-tool.js | 48 ++++++++++++++++- 14 files changed, 122 insertions(+), 47 deletions(-) diff --git a/docs/mongo-db-schema.odg b/docs/mongo-db-schema.odg index 26737ec27baa5749636652a6110e00c17cb026d1..90557e0221037d82e3cd992ccdb9bc98c22eefba 100644 GIT binary patch delta 12620 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 delta 12353 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 Math.round(v), + set: (v) => Math.round(v), + required: true, + }, + singleMinFps: { + type: Number, + get: (v) => Math.round(v), + set: (v) => Math.round(v), + required: true, + }, avgFps: [{ type: Number, get: (v) => Math.round(v), @@ -27,7 +39,7 @@ const LogServerFpsSchema = new Schema({ }], }, { collection: 'logServerFps', - versionKey: false + versionKey: false, }); // optional more indices LogServerFpsSchema.index({war: 1}); diff --git a/server/models/logs/transport.js b/server/models/logs/transport.js index ccf4d89..6bddc4d 100644 --- a/server/models/logs/transport.js +++ b/server/models/logs/transport.js @@ -34,7 +34,7 @@ const LogTransportSchema = new Schema({ }, }, { collection: 'logTransport', - versionKey: false + versionKey: false, }); // optional more indices LogTransportSchema.index({war: 1, driver: 1}); diff --git a/server/models/logs/vehicle.js b/server/models/logs/vehicle.js index c898cfa..d796636 100644 --- a/server/models/logs/vehicle.js +++ b/server/models/logs/vehicle.js @@ -41,7 +41,7 @@ const LogVehicleKillSchema = new Schema({ }, }, { collection: 'logVehicle', - versionKey: false + versionKey: false, }); // optional more indices LogVehicleKillSchema.index({war: 1, shooter: 1, target: 1}); diff --git a/server/models/player.js b/server/models/player.js index a407a8a..85bd6f3 100644 --- a/server/models/player.js +++ b/server/models/player.js @@ -82,6 +82,11 @@ const PlayerSchema = new Schema({ get: (v) => Math.round(v), set: (v) => Math.round(v), }, + performance: { + type: mongoose.Schema.Types.ObjectId, + ref: 'LogServerFpsSchema', + required: true, + }, }, { collection: 'player', timestamps: {createdAt: 'timestamp'}, diff --git a/server/routes/wars.js b/server/routes/wars.js index 9bb1fac..62984b4 100644 --- a/server/routes/wars.js +++ b/server/routes/wars.js @@ -34,6 +34,7 @@ const LogFlagModel = require('../models/logs/flag'); const LogBudgetModel = require('../models/logs/budget'); const LogPointsModel = require('../models/logs/points'); const LogPlayerCountModel = require('../models/logs/player-count'); +const LogServerFpsModel = require('../models/logs/server-fps'); // util const genericPatch = require('./_generic').genericPatch; @@ -59,25 +60,20 @@ wars.route('/') }) .post(apiAuthenticationMiddleware, checkMT, upload.single('log'), (req, res, next) => { - const body = req.body; - const warBody = new WarModel(body); + const body = req.body; + const warBody = new WarModel(body); - if (req.file) { - fs.readFile(req.file.buffer, (file, err) => { - if (err) { - return next(err); - } - const lineArray = file.toString().split('\n'); - const statsResult = parseWarLog(lineArray, warBody); - statsResult.war.save((err, war) => { + if (req.file) { + fs.readFile(req.file.buffer, (file, err) => { if (err) { return next(err); } - PlayerModel.create(statsResult.players, (err) => { + const lineArray = file.toString().split('\n'); + const statsResult = parseWarLog(lineArray, warBody); + statsResult.war.save((err, war) => { if (err) { return next(err); } - LogKillModel.create(statsResult.kills, () => { LogVehicleKillModel.create(statsResult.vehicles, () => { LogRespawnModel.create(statsResult.respawn, () => { @@ -87,27 +83,43 @@ wars.route('/') LogTransportModel.create(statsResult.transport, () => { LogPlayerCountModel.create(statsResult.playerCount, () => { LogPointsModel.create(statsResult.points, () => { - const folderName = resourceLocation.concat(war._id); - mkdirp(folderName, (err) => { - if (err) return next(err); - - // save clean log file - const cleanFile = fs.createWriteStream(folderName + '/clean.log'); - statsResult.clean.forEach((cleanLine) => { - cleanFile.write(cleanLine + '\n\n'); + LogServerFpsModel.create(statsResult.serverFps, (err, serverPerformanceEntries) => { + serverPerformanceEntries.forEach((entry) => { + const idx = statsResult.players + .findIndex((player) => player.name === entry.entityName); + if (idx !== -1) { + const player = statsResult.players[idx]; + player['performance'] = entry._id; + statsResult.players[idx] = player; + } }); - cleanFile.end(); + PlayerModel.create(statsResult.players, (err) => { + if (err) { + return next(err); + } + const folderName = resourceLocation.concat(war._id); + mkdirp(folderName, (err) => { + if (err) return next(err); - // save raw log file - const rawFile = fs.createWriteStream(folderName + '/war.log'); - lineArray.forEach((rawLine) => { - rawFile.write(rawLine + '\n'); + // save clean log file + const cleanFile = fs.createWriteStream(folderName + '/clean.log'); + statsResult.clean.forEach((cleanLine) => { + cleanFile.write(cleanLine + '\n\n'); + }); + cleanFile.end(); + + // save raw log file + const rawFile = fs.createWriteStream(folderName + '/war.log'); + lineArray.forEach((rawLine) => { + rawFile.write(rawLine + '\n'); + }); + rawFile.end(); + + res.status(codes.created); + res.locals.items = war; + next(); + }); }); - rawFile.end(); - - res.status(codes.created); - res.locals.items = war; - next(); }); }); }); @@ -120,13 +132,13 @@ wars.route('/') }); }); }); - }); - } else { - const err = new Error('no Logfile provided'); - err.status = codes.wrongmediasend; - next(err); + } else { + const err = new Error('no Logfile provided'); + err.status = codes.wrongmediasend; + next(err); + } } - }) + ) .all( routerHandling.httpMethodNotAllowed diff --git a/server/tools/log-parse-tool.js b/server/tools/log-parse-tool.js index bdaaf63..e51ae68 100644 --- a/server/tools/log-parse-tool.js +++ b/server/tools/log-parse-tool.js @@ -27,7 +27,15 @@ const bluforPlayerCountRegex = /NATO\s(\d*)/; const opforPlayerCountRegex = /CSAT\s(\d*)/; -const timestampRegex= /LOG:\s(\d*:\d*:\d*)\s---/; +const timestampRegex = /LOG:\s(\d*:\d*:\d*)\s---/; + +const singleMinFpsRegex = /Single min\. FPS for (.*):\s(\d*.\d*)"/; // group1 = entity name, group2 = value + +const singleAvgFpsRegex = /Single avg\. FPS for (.*):\s(\d*.\d*)"/; // group1 = entity name, group2 = value + +const minFpsRegex = /Min\. FPS for (.*):\s\[(.*)\]"/; // group1 = entity name, group2 = comma separated values + +const avgFpsRegex = /Avg\. FPS for (.*):\s\[(.*)\]"/; // group1 = entity name, group2 = comma separated values const parseWarLog = (lineArray, war) => { let flagBlufor = true; @@ -48,6 +56,7 @@ const parseWarLog = (lineArray, war) => { transport: [], players: [], playerCount: [], + serverFps: [], }; const VEHICLE_BLACKLIST = [ @@ -265,6 +274,43 @@ const parseWarLog = (lineArray, war) => { countBlufor: countBlufor, countOpfor: countOpfor, }); + } else if (line.includes('(FPS)')) { + stats.clean.push(line); + + const updateServerFpsLogEntry = (entityName, addKey, addValue) => { + const idx = stats.serverFps.findIndex((entry) => entry.entityName === entityName); + if (idx === -1) { + stats.serverFps.push({ + war: war.id, + entityName: entityName, + [addKey]: addValue, + }); + return; + } + const entity = stats.serverFps[idx]; + entity[addKey] = addValue; + stats.serverFps[idx] = entity; + }; + + const singleMinMatch = singleMinFpsRegex.exec(line); + if (singleMinMatch) { + updateServerFpsLogEntry(singleMinMatch[1], 'singleMinFps', singleMinMatch[2]); + } else { + const singleAvgMatch = singleAvgFpsRegex.exec(line); + if (singleAvgMatch) { + updateServerFpsLogEntry(singleAvgMatch[1], 'singleAvgFps', singleAvgMatch[2]); + } else { + const minCollectionMatch = minFpsRegex.exec(line); + if (minCollectionMatch) { + updateServerFpsLogEntry(minCollectionMatch[1], 'minFps', minCollectionMatch[2].split(',')); + } else { + const avgCollectionMatch = avgFpsRegex.exec(line); + if (avgCollectionMatch) { + updateServerFpsLogEntry(avgCollectionMatch[1], 'avgFps', avgCollectionMatch[2].split(',')); + } + } + } + } } else if (line.includes('(Fraktionsuebersicht)') || line.includes('Fraktionsübersicht')) { /** * PLAYERS From 9c6f74c14f14533c878d18e8c99db355ceefdb06 Mon Sep 17 00:00:00 2001 From: HardiReady Date: Sat, 9 Feb 2019 21:55:13 +0100 Subject: [PATCH 14/30] Add bar chart per player fps visualization (CC-80) --- server/routes/logs.js | 24 ++++- static/src/app/models/model-interfaces.ts | 1 + static/src/app/services/logs/logs.service.ts | 4 + .../server-stats/server-stats.component.html | 28 ++++-- .../server-stats/server-stats.component.ts | 98 +++++++++++-------- .../war/war-header/war-header.component.html | 4 +- .../war/war-header/war-header.component.ts | 14 ++- 7 files changed, 123 insertions(+), 50 deletions(-) diff --git a/server/routes/logs.js b/server/routes/logs.js index 6727365..16ffc1b 100644 --- a/server/routes/logs.js +++ b/server/routes/logs.js @@ -4,6 +4,9 @@ const express = require('express'); const async = require('async'); +// HTTP status codes by name +const codes = require('./http-codes'); + const routerHandling = require('../middleware/router-handling'); // Mongoose Model using mongoDB @@ -16,6 +19,7 @@ const LogTransportModel = require('../models/logs/transport'); const LogFlagModel = require('../models/logs/flag'); const LogPointsModel = require('../models/logs/points'); const LogPlayerCountModel = require('../models/logs/player-count'); +const LogServerFpsModel = require('../models/logs/server-fps'); const logsRouter = new express.Router(); @@ -24,7 +28,7 @@ const processLogRequest = (model, filter, res, next) => { if (err) return next(err); if (!log || log.length === 0) { const err = new Error('No logs found'); - err.status = require('./http-codes').notfound; + err.status = codes.notfound; return next(err); } res.locals.items = log; @@ -154,6 +158,24 @@ logsRouter.route('/:warId/points') routerHandling.httpMethodNotAllowed ); +logsRouter.route('/:warId/performance') + .get((req, res, next) => { + const filter = {war: req.params.warId}; + LogServerFpsModel.find(filter, (err, items) => { + if (err) return next(err); + if (!items) { + const err = new Error('No logs found'); + err.status = codes.notfound; + return next(err); + } + res.locals.items = items; + next(); + }); + }) + .all( + routerHandling.httpMethodNotAllowed + ); + logsRouter.use(routerHandling.emptyResponse); module.exports = logsRouter; diff --git a/static/src/app/models/model-interfaces.ts b/static/src/app/models/model-interfaces.ts index cea28a5..4bb6b4b 100644 --- a/static/src/app/models/model-interfaces.ts +++ b/static/src/app/models/model-interfaces.ts @@ -35,6 +35,7 @@ export interface Player { revive?: number; respawn?: number; flagTouch?: number; + performance?: string; } export interface CampaignPlayer { diff --git a/static/src/app/services/logs/logs.service.ts b/static/src/app/services/logs/logs.service.ts index cfda5c2..35bc80f 100644 --- a/static/src/app/services/logs/logs.service.ts +++ b/static/src/app/services/logs/logs.service.ts @@ -68,4 +68,8 @@ export class LogsService { params.append('defend', defendOnly ? 'true' : ''); return this.httpGateway.get(this.config.apiLogsPath + '/' + warId + '/flag', params); } + + getPerformanceLogs(warId: string) { + return this.httpGateway.get(this.config.apiLogsPath + '/' + warId + '/performance'); + } } diff --git a/static/src/app/statistic/war/server-stats/server-stats.component.html b/static/src/app/statistic/war/server-stats/server-stats.component.html index 452c16c..09192f3 100644 --- a/static/src/app/statistic/war/server-stats/server-stats.component.html +++ b/static/src/app/statistic/war/server-stats/server-stats.component.html @@ -10,8 +10,8 @@
    - - +
    + + + + + + + + + + + + + + + + + diff --git a/static/src/app/statistic/war/server-stats/server-stats.component.ts b/static/src/app/statistic/war/server-stats/server-stats.component.ts index 6ed098d..68724e4 100644 --- a/static/src/app/statistic/war/server-stats/server-stats.component.ts +++ b/static/src/app/statistic/war/server-stats/server-stats.component.ts @@ -1,5 +1,4 @@ import {Component, ElementRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild} from '@angular/core'; -import {ChartUtils} from '../../../utils/chart-utils'; import {War} from '../../../models/model-interfaces'; import {TranslateService} from '@ngx-translate/core'; @@ -19,21 +18,19 @@ export class ServerStatsComponent implements OnInit, OnChanges { startDateObj: Date; - initialized: any; - public activeChartSelect: string; + barChartData: any[] = []; + lineChartData: any[] = []; - tmpPointData; - tmpBudgetData; - tmpKillData; - tmpFrienlyFireData; - tmpVehicleData; - tmpTransportData; - tmpReviveData; - tmpStabilizeData; - tmpFlagCaptureData; + showBarChart = true; + + tmpSingleAvg; + tmpSingleMin; + tmpAvgTimeline; + tmpMinTimeline; + tmpServerTimeline; readonly labels = { singleAvg: 'stats.performance.select.single.avg', @@ -45,6 +42,7 @@ export class ServerStatsComponent implements OnInit, OnChanges { readonly labelsAsString = Object.keys(this.labels) .map((key) => this.labels[key]); + barChartLabel: string; lineChartLabel: string; gradient = false; @@ -62,21 +60,14 @@ export class ServerStatsComponent implements OnInit, OnChanges { } ngOnInit() { - this.setLineChartLabel(this.labels.singleAvg); + this.setBarChartLabel(this.labels.singleAvg); } ngOnChanges(changes: SimpleChanges) { if (changes.war || changes.performanceData) { - this.initialized = { - budget: false, - kill: false, - revive: false, - transport: false, - flag: false - }; - Object.assign(this, [this.lineChartData]); + this.initializeChartData(); + Object.assign(this, [this.barChartData]); this.activeChartSelect = this.labels.singleAvg; - this.startDateObj = new Date(this.war.date); this.startDateObj.setHours(0); this.startDateObj.setMinutes(1); @@ -85,31 +76,60 @@ export class ServerStatsComponent implements OnInit, OnChanges { selectChart(newSelection) { this.activeChartSelect = newSelection; - this.setLineChartLabel(this.activeChartSelect); - console.log('############### apply data ##################'); - } - - addFinalTimeData(tmpCollection) { - const endDate = new Date(this.war.endDate); - if (tmpCollection === this.tmpBudgetData) { - this.tmpBudgetData[0].series.push(ChartUtils.getSeriesEntry(endDate, this.war.endBudgetBlufor)); - this.tmpBudgetData[1].series.push(ChartUtils.getSeriesEntry(endDate, this.war.endBudgetOpfor)); + if (this.activeChartSelect !== this.labels.serverFps) { + this.showBarChart = true; + this.setBarChartLabel(this.activeChartSelect); + switch (this.activeChartSelect) { + case this.labels.singleAvg: + this.barChartData = this.tmpSingleAvg; + break; + case this.labels.singleMin: + this.barChartData = this.tmpSingleMin; + break; + } } else { - for (let j = 0; j < tmpCollection.length; j++) { - // mayBe check is needed for logs that are simply not existent in older wars, i.e. vehicleKills - const maybeLast = tmpCollection[j].series[tmpCollection[j].series.length - 1]; - if (maybeLast && maybeLast.name < endDate) { - tmpCollection[j].series.push( - ChartUtils.getSeriesEntry(endDate, tmpCollection[j].series[tmpCollection[j].series.length - 1].value) - ); - } + this.showBarChart = false; + this.setLineChartLabel(this.activeChartSelect); + switch (this.activeChartSelect) { + case this.labels.avgTimeline: + this.lineChartData = this.tmpAvgTimeline; + break; + case this.labels.minTimeline: + this.lineChartData = this.tmpMinTimeline; + break; + case this.labels.serverFps: + this.lineChartData = this.tmpServerTimeline; } } } + initializeChartData() { + this.tmpSingleAvg = []; + this.tmpSingleMin = []; + this.performanceData.forEach((entry) => { + this.tmpSingleAvg.push({ + name: entry.entityName, + value: entry.singleAvgFps + }); + this.tmpSingleMin.push({ + name: entry.entityName, + value: entry.singleMinFps + }); + }); + this.tmpSingleAvg.sort((a, b) => a.value - b.value); + this.tmpSingleMin.sort((a, b) => a.value - b.value); + this.barChartData = this.tmpSingleAvg; + } + setLineChartLabel(i18n: string) { this.translate.get(i18n).subscribe((translated) => { this.lineChartLabel = translated; }); } + + setBarChartLabel(i18n: string) { + this.translate.get(i18n).subscribe((translated) => { + this.barChartLabel = translated; + }); + } } diff --git a/static/src/app/statistic/war/war-header/war-header.component.html b/static/src/app/statistic/war/war-header/war-header.component.html index e617a1b..e922d3d 100644 --- a/static/src/app/statistic/war/war-header/war-header.component.html +++ b/static/src/app/statistic/war/war-header/war-header.component.html @@ -48,7 +48,7 @@ {{'stats.scoreboard.tab.player' | translate}}
  • - - -