Compare commits

...

121 commits

Author SHA1 Message Date
Anthony Mineo 8165e35d10 release: api v0.0.26 2023-03-01 19:10:06 -05:00
Anthony Mineo 912a7c676a chore: pin nest cli version 2023-03-01 19:09:39 -05:00
Anthony Mineo f803963015 chore: pin node version with volta 2023-03-01 19:09:09 -05:00
Anthony Mineo 928bf1812c fix: change games service to return list ordered by datestamp desc 2023-03-01 17:52:45 -05:00
Anthony Mineo 248f8a4687 config: ignore *.dump 2023-03-01 17:51:12 -05:00
Anthony Mineo eea261af1a * [api] v0.0.25 -- Adds query caching support 2021-09-27 12:13:05 -04:00
Anthony Mineo a615d4524b Remove some cruft 2021-09-27 12:11:43 -04:00
Anthony Mineo 86e13db165 * Add query caching for lthe long running leaderboard queries -- 2hr ttl 2021-09-27 11:56:17 -04:00
Anthony Mineo 4df3a1b594 * [t2-stats-api]: v0.0.24 2021-09-26 09:44:39 -04:00
Anthony Mineo 7f1f34364e
Merge pull request #16 from exogen/feature/leaderboard-time-period
Add time period support to leaderboard queries
2021-09-26 09:36:45 -04:00
Brian Beck 08010956a8 Default timePeriod to null instead of undefined 2021-09-26 00:50:59 -07:00
Brian Beck 3114913c85 Cast string to interval correctly 2021-09-26 00:46:51 -07:00
Brian Beck edd3076b54 Add time period support to leaderboard queries 2021-09-26 00:25:26 -07:00
Anthony Mineo 79a8d4eeac
Merge pull request #9 from exogen/accuracy-leaderboard-grenade-launcher
Add grenade launcher stats to accuracy leaderboard
2021-09-25 18:54:04 -04:00
Brian Beck 01c2b8b37d Add grenade launcher stats to accuracy leaderboard 2021-09-25 14:36:43 -07:00
Anthony Mineo c42b05e7de * API: v0.0.21 2021-06-01 18:10:32 -04:00
Anthony Mineo 3bbdb99253 * Update format for new attributes (teamOneCapTimesGame, teamTwoCapTimesGame) -- formatArrayOfNumbers: Pass in string and return a number array"
"
2021-06-01 18:10:03 -04:00
Anthony Mineo 89fbe7cf85
Merge pull request #8 from petrifiedroadkill/master
Cap Time Stats
2021-06-01 17:50:40 -04:00
Anthony Mineo 70306791c3
Merge pull request #7 from exogen/feature/lower-team-threshold-and-improve-detection
Lower team participation threshold from 80% to 67%, improve team score accuracy
2021-06-01 17:45:47 -04:00
petrifiedroadkill 321047c38e Cap Time Stats 2021-06-01 13:14:01 -05:00
Brian Beck efdf44d7ba Also include LCTF games 2021-05-31 23:44:13 -07:00
Brian Beck e55c6ed9da Lower team participation threshold from 80% to 67%, improve team score accuracy 2021-05-31 22:10:55 -07:00
Anthony Mineo e01179fd72 * API: v0.0.20 2021-05-31 18:31:35 -04:00
Anthony Mineo 2e8b8458a6
Merge pull request #6 from exogen/bugfix/teamScoreGame-mode
Use mode() instead of avg() to get most likely teamScoreGame
2021-05-31 18:30:19 -04:00
Brian Beck 7585564018 Use mode() instead of avg() to get most likely teamScoreGame 2021-05-31 00:20:42 -07:00
Anthony Mineo 9be3e79372 * API v0.0.19 2021-05-30 08:35:48 -04:00
Anthony Mineo ab028e52fd
Merge pull request #5 from exogen/feature/top-winning-percentage
Add API endpoint for top win-loss CTF records
2021-05-30 08:30:21 -04:00
Brian Beck 1949494098 Add API endpoint for top win-loss CTF records 2021-05-30 01:57:00 -07:00
Anthony Mineo 6f37071ba6 api v0.0.18 2021-05-05 16:27:04 -04:00
Anthony Mineo e7e064ae55
Merge pull request #4 from petrifiedroadkill/master
Team Time Stats
2021-05-05 16:22:01 -04:00
petrifiedroadkill 4cfeab4264 Update formatStats.ts 2021-05-05 12:50:13 -05:00
Anthony Mineo c8402074b4 Disable PGHero service 2021-04-30 22:02:11 -04:00
Anthony Mineo 59d0cc198d api v0.0.17 2021-04-30 21:54:14 -04:00
Anthony Mineo d0f963911f
Merge pull request #3 from exogen/bugfix/null-discjumps
Fix null discjumps and potential divide by zero
2021-04-30 21:53:24 -04:00
Brian Beck 05fa275f85 Fix null discjumps and potential divide by zero 2021-04-30 18:43:18 -07:00
Anthony Mineo 4bb29a22ff * [pghero] New Service: Add pghero for dev env tooling 2021-04-30 21:17:37 -04:00
Anthony Mineo 53c66be747
Merge pull request #2 from exogen/feature/top-players-endpoint
Add accuracy leaderboard: players/top/accuracy endpoint
2021-04-30 21:09:11 -04:00
Brian Beck 7504d1497c Add accuracy leaderboard: players/top/accuracy endpoint 2021-04-29 18:04:53 -07:00
Anthony Mineo 1015e4b8d4 * [SSL] Adjustment: Lower cert lifetime. Browsers max lifespan only allow for 398 days now 2021-04-28 19:51:10 -04:00
Anthony Mineo 9466af400d Add additional vars that were previously set in docker-compose 2021-04-28 19:47:41 -04:00
Anthony Mineo 1a56501db4 Remove legacy Adonis 2021-04-28 19:46:39 -04:00
Anthony Mineo 1adcdf1d0e API v0.0.15 2021-04-20 19:02:04 -04:00
Anthony Mineo ebb72ff01f Add new stat keys for number conversion 2021-04-20 19:00:51 -04:00
Anthony Mineo 0bbfa32ef6 swap out wget for lftp 2021-03-16 20:10:59 -04:00
Anthony Mineo fbff090ba5 Refactor FTP command 2021-03-15 17:19:58 -04:00
Anthony Mineo ddf8c112f5 Add FTP Host path as env 2021-03-13 11:38:56 -05:00
Anthony Mineo 9222b8f0d6 Update ftp parser paths 2021-03-13 11:13:26 -05:00
Anthony Mineo a14fec7355 Use new teamScoreGame key to display team v team score. Fall back to old tally method if its not available 2020-09-30 16:20:16 -04:00
Anthony Mineo c1fcb61731 Deprecated webapp (old stats stack) 2020-09-29 14:43:26 -04:00
Anthony Mineo aed14743e6 api v0.0.13 2020-09-27 14:22:34 -04:00
Anthony Mineo 2f3f61fd62 Sort player's games properly by gameID 2020-09-27 14:21:31 -04:00
Anthony Mineo 98affddbff tweak return details 2020-09-23 15:03:48 -04:00
Anthony Mineo c10cd44615 Update return formats 2020-09-23 14:39:29 -04:00
Anthony Mineo 67eb8db0c8 parser update, non-restart - node cron 2020-09-23 10:57:09 -04:00
Anthony Mineo ef3909b8ac return player statTotals 2020-09-22 10:50:22 -04:00
Anthony Mineo fdb446714a v0.0.7 2020-09-21 14:28:51 -04:00
Anthony Mineo cc1c71c769 A few more stats... 2020-09-21 14:25:20 -04:00
Anthony Mineo f70b95ab5e Additional stats 2020-09-21 14:11:10 -04:00
Anthony Mineo 1a51c95b82 api v0.0.5 2020-09-21 13:36:11 -04:00
Anthony Mineo 9e40b300f5 Tweak return limits 2020-09-21 12:09:21 -04:00
Anthony Mineo 8e5cf83d4d Majorly improve stat formatting :) 2020-09-21 11:53:06 -04:00
Anthony Mineo 52f87b1fc3 v0.0.4 2020-09-17 16:59:42 -04:00
Anthony Mineo 72ac49e4f9 Update deps 2020-09-17 16:59:17 -04:00
Anthony Mineo 69f250871a init new api endpoint for returning games by game type with summaries 2020-09-17 16:57:45 -04:00
Anthony Mineo 80056903c6 Set API https 2020-08-31 17:12:04 -04:00
Anthony Mineo 87bfab307d Set API Healthcheck 2020-08-31 16:38:28 -04:00
Anthony Mineo b3925a8fc6 Setup stats API container: 2020-08-31 16:27:31 -04:00
Anthony Mineo 9176889086 Update port 2020-08-31 15:45:46 -04:00
Anthony Mineo ef32d90030 Return formatted stats based off other gameTypes 2020-08-30 10:00:33 -04:00
Anthony Mineo 96e15be86d Setup return games with details endpoint 2020-08-29 21:05:20 -04:00
Anthony Mineo f240ccd306 Enable CORS 2020-08-29 19:35:28 -04:00
Anthony Mineo 86c531d61e Enable cors 2020-08-29 19:32:50 -04:00
Anthony Mineo d85896dcaa Better return error messages 2020-08-29 19:26:53 -04:00
Anthony Mineo 2c3c9a0532 Setup Swagger 2020-08-29 19:20:44 -04:00
Anthony Mineo a1b2f2cd78 Enable swagger 2020-08-29 19:10:50 -04:00
Anthony Mineo 75f6e64a1f Update games service to include find by gametype 2020-08-29 18:36:24 -04:00
Anthony Mineo 38fb66da06 Calculated CTFGame return type stats 2020-08-29 14:44:46 -04:00
Anthony Mineo 2fcfd8cb3b upgrade packages 2020-08-29 11:17:14 -04:00
Anthony Mineo 0db8a7f439 Finish API services 2020-08-29 11:11:21 -04:00
Anthony Mineo 2abedc99f9 Setup game module 2020-08-23 11:04:35 -04:00
Anthony Mineo 2d90f9dde8 Add find by endpoints 2020-08-23 10:35:50 -04:00
Anthony Mineo 3b5f41f933 init nest (wip) 2020-08-22 10:19:50 -04:00
Anthony Mineo 1a211e261e ignore notes 2020-08-17 17:08:31 -04:00
Anthony Mineo f1e93e9d29 New stat format images 2020-08-15 12:03:11 -04:00
Anthony Mineo 0b5aceca7a Update to ingest/display new stats format; QoL updates soon... 2020-08-15 12:02:05 -04:00
Anthony Mineo bb179b82f8 web-rc5 2020-04-26 11:18:08 -04:00
Anthony Mineo 43c15e1649 init coors for playt2.com 2020-04-26 11:14:05 -04:00
Anthony Mineo b1bf08267e stats rc4 2020-04-16 17:22:24 -04:00
Anthony Mineo 71c3cad722 Add header for playt2.com origin 2020-04-16 17:21:56 -04:00
Anthony Mineo 15ac59d6e9 rc3 2020-04-14 20:50:05 -04:00
Anthony Mineo a484d94254 Display all games for player even if their score is 0 2020-04-14 20:49:51 -04:00
Anthony Mineo 093636fc6d display tweaks 2020-04-14 20:43:43 -04:00
Anthony Mineo 8fc23cc6e4 rc1 2020-04-14 18:22:44 -04:00
Anthony Mineo 2cf1a508d5 Adjust limits for performance 2020-04-14 18:22:26 -04:00
Anthony Mineo 56878c47d6 update pg pool settings 2020-04-14 09:11:56 -04:00
Anthony Mineo 77880c9df2 update 2020-04-13 17:38:52 -04:00
Anthony Mineo 74798d239b basic concept ready 2020-04-12 14:47:23 -04:00
Anthony Mineo a57800e3a4 Basic game stub 2020-04-10 16:32:05 -04:00
Anthony Mineo 5394982541 stubbed out Game 2020-04-10 15:03:00 -04:00
Anthony Mineo 063f6ba55b Display a 'No Data' label on radar chart if there isnt any data 2020-04-10 10:56:56 -04:00
Anthony Mineo e601fe8e91 bump parser and db 2020-04-08 16:57:42 -04:00
Anthony Mineo 825159ea56 Major logic updates - accounts for dynamic length stat arrays 2020-04-08 16:56:15 -04:00
Anthony Mineo 77a564125a Dont display players with 0 totalGames 2020-04-05 11:39:03 -04:00
Anthony Mineo 15b96d197e init base game view/controller 2020-04-05 11:38:41 -04:00
Anthony Mineo 31d927bbe2 Setup weapon chart 2020-04-04 12:29:04 -04:00
Anthony Mineo 47c6017ea1 add recharts 2020-04-04 11:20:33 -04:00
Anthony Mineo 354ea453e0 add some docs 2020-04-01 20:15:45 -04:00
Anthony Mineo cdbcbe19e3 Calculate stat totals 2020-04-01 18:06:39 -04:00
Anthony Mineo 229ce10466 cleanup 2020-03-30 16:34:31 -04:00
Anthony Mineo 982d1c429c Refine queries 2020-03-30 16:34:20 -04:00
Anthony Mineo cfa645120d init base frontend 2020-03-29 11:31:16 -04:00
Anthony Mineo ade7f7e288 v0.1.3 2020-03-15 14:53:11 -04:00
Anthony Mineo 915b396bc5 Output completion time of last parse 2020-03-15 14:49:00 -04:00
Anthony Mineo 15d89ae5e1 Use cron to the keep the container alive 2020-03-15 14:47:48 -04:00
Anthony Mineo e596b0ca7c Add support to parse and store gameID 2020-03-15 13:59:33 -04:00
Anthony Mineo 9518a8a6b8 Add new column for game_id 2020-03-15 13:58:31 -04:00
Anthony Mineo 9c8a60e626 Update cron to run at 9:30am est 2020-03-08 15:47:58 -04:00
Anthony Mineo 9071887407 v0.1.2 2020-03-08 14:26:55 -04:00
Anthony Mineo 15294ad2e5 Use env user 2020-03-08 14:26:17 -04:00
Anthony Mineo 788cc09aec Prep deploy directives 2020-03-08 14:16:48 -04:00
Anthony Mineo cc1053eff0 Ignore docker-compose stack deploy 2020-03-08 14:16:19 -04:00
62 changed files with 16334 additions and 65 deletions

View file

@ -7,3 +7,14 @@ POSTGRES_PASSWORD="dev"
FTP_HOST="127.0.0.1" FTP_HOST="127.0.0.1"
FTP_USER="user" FTP_USER="user"
FTP_PW="pw" FTP_PW="pw"
FTP_PATH="/path/to/stats"
# API DB Connection
DATABASE_HOST="db"
DATABASE_PORT="5432"
DATABASE_USER="dev"
DATABASE_PASSWORD="dev"
DATABASE_NAME="t2_stats"
NODE_ENV="development"
APP_NAME="T2StatsAPI"

11
.gitignore vendored
View file

@ -1,4 +1,15 @@
.env .env
node_modules
notes.md notes.md
app/t2-stat-parser/serverStats/stats app/t2-stat-parser/serverStats/stats
app/t2-stat-parser/serverStats/mlData
app/t2-stat-parser/serverStats/lData
app/t2-stat-parser/serverStats/__notes
docker-compose.deploy.yml
traefik.yml
docker-secrets/adonis.appkey.*
*.dump

72
app/api/.eslintrc.js Normal file
View file

@ -0,0 +1,72 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module'
},
plugins: [ '@typescript-eslint/eslint-plugin' ],
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'prettier/@typescript-eslint'
],
root: true,
env: {
node: true,
jest: true
},
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'default',
filter: {
regex: '^(created|updated)_at$',
match: false
},
format: [ 'camelCase' ],
leadingUnderscore: 'allow',
trailingUnderscore: 'allow'
},
{
selector: 'property',
filter: {
regex: '^(created|updated)_at$',
match: false
},
format: [ 'camelCase', 'UPPER_CASE' ],
leadingUnderscore: 'allow',
trailingUnderscore: 'allow'
},
{
selector: 'enumMember',
format: [ 'UPPER_CASE' ],
leadingUnderscore: 'allow',
trailingUnderscore: 'allow'
},
{
selector: 'variable',
format: [ 'camelCase', 'UPPER_CASE' ],
types: [ 'boolean', 'string', 'number', 'array' ],
leadingUnderscore: 'allow',
trailingUnderscore: 'allow'
},
{
selector: 'variable',
format: [ 'camelCase', 'PascalCase' ],
types: [ 'function' ],
leadingUnderscore: 'allow',
trailingUnderscore: 'allow'
},
{
selector: 'typeLike',
format: [ 'PascalCase' ]
}
]
}
};

37
app/api/.gitignore vendored Normal file
View file

@ -0,0 +1,37 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# ENV
*.env

5
app/api/.prettierrc Normal file
View file

@ -0,0 +1,5 @@
{
"singleQuote": true,
"trailingComma": "all",
"useTabs": true
}

75
app/api/README.md Normal file
View file

@ -0,0 +1,75 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo_text.svg" width="320" alt="Nest Logo" /></a>
</p>
[travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master
[travis-url]: https://travis-ci.org/nestjs/nest
[linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
[linux-url]: https://travis-ci.org/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="blank">Node.js</a> framework for building efficient and scalable server-side applications, heavily inspired by <a href="https://angular.io" target="blank">Angular</a>.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/dm/@nestjs/core.svg" alt="NPM Downloads" /></a>
<a href="https://travis-ci.org/nestjs/nest"><img src="https://api.travis-ci.org/nestjs/nest.svg?branch=master" alt="Travis" /></a>
<a href="https://travis-ci.org/nestjs/nest"><img src="https://img.shields.io/travis/nestjs/nest/master.svg?label=linux" alt="Linux" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#5" alt="Coverage" /></a>
<a href="https://gitter.im/nestjs/nestjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge"><img src="https://badges.gitter.im/nestjs/nestjs.svg" alt="Gitter" /></a>
<a href="https://opencollective.com/nest#backer"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec"><img src="https://img.shields.io/badge/Donate-PayPal-dc3d53.svg"/></a>
<a href="https://twitter.com/nestframework"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

4
app/api/nest-cli.json Normal file
View file

@ -0,0 +1,4 @@
{
"collection": "@nestjs/schematics",
"sourceRoot": "src"
}

12695
app/api/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

87
app/api/package.json Normal file
View file

@ -0,0 +1,87 @@
{
"name": "playt2-stats-api",
"version": "0.0.1",
"description": "Open-API powering stats.playt2.com",
"authors": {
"name": "Anthony Mineo",
"email": "anthonymineo@gmail.com",
"url": "https://anthonymineo.com"
},
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^8.0.7",
"@nestjs/config": "^1.0.1",
"@nestjs/core": "^8.0.7",
"@nestjs/mapped-types": "^1.0.0",
"@nestjs/platform-express": "^8.0.7",
"@nestjs/swagger": "^5.0.9",
"@nestjs/typeorm": "^8.0.2",
"cache-manager": "^3.4.4",
"class-transformer": "^0.3.1",
"class-validator": "^0.12.2",
"joi": "^17.4.2",
"pg": "^8.7.1",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.3.0",
"swagger-ui-express": "^4.1.6",
"typeorm": "^0.2.37"
},
"devDependencies": {
"@nestjs/cli": "^8.1.1",
"@nestjs/schematics": "^8.0.3",
"@nestjs/testing": "^8.0.7",
"@types/cache-manager": "^3.4.2",
"@types/express": "^4.17.13",
"@types/hapi__joi": "^17.1.7",
"@types/jest": "^27.0.2",
"@types/node": "^16.10.1",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^3.10.1",
"@typescript-eslint/parser": "^3.10.1",
"eslint": "^7.9.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"jest": "^26.4.2",
"prettier": "^2.1.2",
"supertest": "^4.0.2",
"ts-jest": "^26.3.0",
"ts-loader": "^8.0.3",
"ts-node": "^9.0.0",
"tsconfig-paths": "^3.9.0",
"typescript": "^4.4.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"volta": {
"node": "12.18"
}
}

View file

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [ AppController ],
providers: [ AppService ]
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Healthy!"', () => {
expect(appController.getHello()).toBe('Healthy!');
});
});
});

View file

@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

39
app/api/src/app.module.ts Normal file
View file

@ -0,0 +1,39 @@
import * as Joi from 'joi';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GamesModule } from './games/games.module';
import { GameModule } from './game/game.module';
import { PlayersModule } from './players/players.module';
import { PlayerModule } from './player/player.module';
@Module({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
DATABASE_HOST: Joi.required(),
DATABASE_PORT: Joi.number().default(5432)
})
}),
TypeOrmModule.forRoot({
type: 'postgres', // type of our database
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true // models will be loaded automatically (you don't have to explicitly specify the entities: [] array)
}),
GamesModule,
GameModule,
PlayersModule,
PlayerModule
],
controllers: [ AppController ],
providers: [ AppService ]
})
export class AppModule {}

View file

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Healthy!';
}
}

View file

@ -0,0 +1,11 @@
import { IsOptional, IsPositive } from 'class-validator';
export class PaginationQueryDto {
@IsOptional()
@IsPositive()
limit: number;
@IsOptional()
@IsPositive()
offset: number;
}

View file

@ -0,0 +1,60 @@
import { IsOptional, IsPositive, IsNotEmpty, IsIn, Max } from 'class-validator';
const filterableGameTypes = ['CTFGame', 'LakRabbitGame'] as const;
type FilterableGameType = typeof filterableGameTypes[number];
const hitStats = [
'discHitsTG',
'discDmgHitsTG',
'discMATG',
'laserHitsTG',
'laserMATG',
'cgHitsTG',
'shockHitsTG',
'grenadeHitsTG',
'grenadeDmgHitsTG',
'grenadeMATG',
] as const;
type Stat = typeof hitStats[number];
export class TopAccuracyQueryDto {
@IsNotEmpty()
@IsIn(hitStats as any)
stat: Stat;
@IsOptional()
@IsIn(filterableGameTypes as any)
gameType: FilterableGameType;
@IsOptional()
@IsPositive()
minGames: number;
@IsOptional()
@IsPositive()
minShots: number;
@IsOptional()
@IsPositive()
@Max(100)
limit: number;
@IsOptional()
timePeriod: string;
}
export class TopWinsQueryDto {
@IsOptional()
@IsPositive()
minGames: number;
@IsOptional()
@IsPositive()
@Max(100)
limit: number;
@IsOptional()
timePeriod: string;
}

View file

@ -0,0 +1,703 @@
/* eslint-disable */
// function formatArrayOfNumbers(arr: string): number[] {
// return arr ? arr.split(',').map(Number) : [];
// };
function formatArrayOfNumbers(arr){
return arr;
};
// Fixup player stat line types
function formatPlayerStats(statObj: any) {
return {
...statObj.stats,
aaTurretDeathsTG: Number(statObj.stats.aaTurretDeathsTG),
aaTurretKillsTG: Number(statObj.stats.aaTurretKillsTG),
airTimeAvg: Number(statObj.stats.airTimeAvg),
airTimeTG: Number(statObj.stats.airTimeTG),
armorHDTG: Number(statObj.stats.armorHDTG),
armorHHDTG: Number(statObj.stats.armorHHDTG),
armorHHTG: Number(statObj.stats.armorHHTG),
armorHLDTG: Number(statObj.stats.armorHLDTG),
armorHLTG: Number(statObj.stats.armorHLTG),
armorHMDTG: Number(statObj.stats.armorHMDTG),
armorHMTG: Number(statObj.stats.armorHMTG),
armorHTG: Number(statObj.stats.armorHTG),
armorLDTG: Number(statObj.stats.armorLDTG),
armorLHDTG: Number(statObj.stats.armorLHDTG),
armorLHTG: Number(statObj.stats.armorLHTG),
armorLLDTG: Number(statObj.stats.armorLLDTG),
armorLLTG: Number(statObj.stats.armorLLTG),
armorLMDTG: Number(statObj.stats.armorLMDTG),
armorLMTG: Number(statObj.stats.armorLMTG),
armorLTG: Number(statObj.stats.armorLTG),
armorMDTG: Number(statObj.stats.armorMDTG),
armorMHDTG: Number(statObj.stats.armorMHDTG),
armorMHTG: Number(statObj.stats.armorMHTG),
armorMLDTG: Number(statObj.stats.armorMLDTG),
armorMLTG: Number(statObj.stats.armorMLTG),
armorMMDTG: Number(statObj.stats.armorMMDTG),
armorMMTG: Number(statObj.stats.armorMMTG),
armorMTG: Number(statObj.stats.armorMTG),
assaultRDTG: Number(statObj.stats.assaultRDTG),
assaultRKTG: Number(statObj.stats.assaultRKTG),
assistTG: Number(statObj.stats.assistTG),
avgSpeedAvg: Number(statObj.stats.avgSpeedAvg),
bellyTurretDeathsTG: Number(statObj.stats.bellyTurretDeathsTG),
bellyTurretKillsTG: Number(statObj.stats.bellyTurretKillsTG),
blasterACCAvg: Number(statObj.stats.blasterACCAvg),
blasterComTG: Number(statObj.stats.blasterComTG),
blasterDeathAirAirTG: Number(statObj.stats.blasterDeathAirAirTG),
blasterDeathAirGroundTG: Number(statObj.stats.blasterDeathAirGroundTG),
blasterDeathAirTG: Number(statObj.stats.blasterDeathAirTG),
blasterDeathGroundAirTG: Number(statObj.stats.blasterDeathGroundAirTG),
blasterDeathGroundGroundTG: Number(statObj.stats.blasterDeathGroundGroundTG),
blasterDeathGroundTG: Number(statObj.stats.blasterDeathGroundTG),
blasterDeathsTG: Number(statObj.stats.blasterDeathsTG),
blasterDmgTG: Number(statObj.stats.blasterDmgTG),
blasterHitDistMax: Number(statObj.stats.blasterHitDistMax),
blasterHitsTG: Number(statObj.stats.blasterHitsTG),
blasterHitSVMax: Number(statObj.stats.blasterHitSVMax),
blasterHitVVMax: Number(statObj.stats.blasterHitVVMax),
blasterKillAirAirTG: Number(statObj.stats.blasterKillAirAirTG),
blasterKillAirGroundTG: Number(statObj.stats.blasterKillAirGroundTG),
blasterKillAirTG: Number(statObj.stats.blasterKillAirTG),
blasterKillDistMax: Number(statObj.stats.blasterKillDistMax),
blasterKillGroundAirTG: Number(statObj.stats.blasterKillGroundAirTG),
blasterKillGroundGroundTG: Number(statObj.stats.blasterKillGroundGroundTG),
blasterKillGroundTG: Number(statObj.stats.blasterKillGroundTG),
blasterKillsTG: Number(statObj.stats.blasterKillsTG),
blasterKillSVMax: Number(statObj.stats.blasterKillSVMax),
blasterKillVVMax: Number(statObj.stats.blasterKillVVMax),
blasterMADistMax: Number(statObj.stats.blasterMADistMax),
blasterMAHitDistMax: Number(statObj.stats.blasterMAHitDistMax),
blasterMATG: Number(statObj.stats.blasterMATG),
blasterReflectHitTG: Number(statObj.stats.blasterReflectHitTG),
blasterReflectKillTG: Number(statObj.stats.blasterReflectKillTG),
blasterScoreTG: Number(statObj.stats.blasterScoreTG),
blasterShotsFiredTG: Number(statObj.stats.blasterShotsFiredTG),
bomberBombsDeathsTG: Number(statObj.stats.bomberBombsDeathsTG),
bomberBombsKillsTG: Number(statObj.stats.bomberBombsKillsTG),
bomberFlyerRDTG: Number(statObj.stats.bomberFlyerRDTG),
bomberFlyerRKTG: Number(statObj.stats.bomberFlyerRKTG),
capEfficiencyAvg: Number(statObj.stats.capEfficiencyAvg),
carrierKillsTG: Number(statObj.stats.carrierKillsTG),
cgACCAvg: Number(statObj.stats.cgACCAvg),
cgComTG: Number(statObj.stats.cgComTG),
cgDeathAirAirTG: Number(statObj.stats.cgDeathAirAirTG),
cgDeathAirGroundTG: Number(statObj.stats.cgDeathAirGroundTG),
cgDeathAirTG: Number(statObj.stats.cgDeathAirTG),
cgDeathGroundAirTG: Number(statObj.stats.cgDeathGroundAirTG),
cgDeathGroundGroundTG: Number(statObj.stats.cgDeathGroundGroundTG),
cgDeathGroundTG: Number(statObj.stats.cgDeathGroundTG),
cgDeathsTG: Number(statObj.stats.cgDeathsTG),
cgDmgTG: Number(statObj.stats.cgDmgTG),
cgHitDistMax: Number(statObj.stats.cgHitDistMax),
cgHitsTG: Number(statObj.stats.cgHitsTG),
cgHitSVMax: Number(statObj.stats.cgHitSVMax),
cgHitVVMax: Number(statObj.stats.cgHitVVMax),
cgKillAirAirTG: Number(statObj.stats.cgKillAirAirTG),
cgKillAirGroundTG: Number(statObj.stats.cgKillAirGroundTG),
cgKillAirTG: Number(statObj.stats.cgKillAirTG),
cgKillDistMax: Number(statObj.stats.cgKillDistMax),
cgKillGroundAirTG: Number(statObj.stats.cgKillGroundAirTG),
cgKillGroundGroundTG: Number(statObj.stats.cgKillGroundGroundTG),
cgKillGroundTG: Number(statObj.stats.cgKillGroundTG),
cgKillsTG: Number(statObj.stats.cgKillsTG),
cgKillSVMax: Number(statObj.stats.cgKillSVMax),
cgKillVVMax: Number(statObj.stats.cgKillVVMax),
cgMADistMax: Number(statObj.stats.cgMADistMax),
cgMAHitDistMax: Number(statObj.stats.cgMAHitDistMax),
cgMATG: Number(statObj.stats.cgMATG),
cgScoreTG: Number(statObj.stats.cgScoreTG),
cgShotsFiredTG: Number(statObj.stats.cgShotsFiredTG),
chainKillTG: Number(statObj.stats.chainKillTG),
comboCountTG: Number(statObj.stats.comboCountTG),
concussFlagTG: Number(statObj.stats.concussFlagTG),
concussHitTG: Number(statObj.stats.concussHitTG),
concussTakenTG: Number(statObj.stats.concussTakenTG),
crashDeathsTG: Number(statObj.stats.crashDeathsTG),
crashKillsTG: Number(statObj.stats.crashKillsTG),
ctrlKKillsTG: Number(statObj.stats.ctrlKKillsTG),
dayStamp: Number(statObj.stats.dayStamp),
deadDistMax: Number(statObj.stats.deadDistMax),
deathAirAirTG: Number(statObj.stats.deathAirAirTG),
deathAirGroundTG: Number(statObj.stats.deathAirGroundTG),
deathAirTG: Number(statObj.stats.deathAirTG),
deathGroundAirTG: Number(statObj.stats.deathGroundAirTG),
deathGroundGroundTG: Number(statObj.stats.deathGroundGroundTG),
deathGroundTG: Number(statObj.stats.deathGroundTG),
deathKillsTG: Number(statObj.stats.deathKillsTG),
deathsTG: Number(statObj.stats.deathsTG),
decupleChainKillTG: Number(statObj.stats.decupleChainKillTG),
decupleKillTG: Number(statObj.stats.decupleKillTG),
defenseScoreTG: Number(statObj.stats.defenseScoreTG),
depInvRepairsTG: Number(statObj.stats.depInvRepairsTG),
depInvyUseTG: Number(statObj.stats.depInvyUseTG),
depSensorDestroysTG: Number(statObj.stats.depSensorDestroysTG),
depSensorRepairsTG: Number(statObj.stats.depSensorRepairsTG),
depStationDestroysTG: Number(statObj.stats.depStationDestroysTG),
depTurretDestroysTG: Number(statObj.stats.depTurretDestroysTG),
depTurretRepairsTG: Number(statObj.stats.depTurretRepairsTG),
destructionTG: Number(statObj.stats.destructionTG),
discACCAvg: Number(statObj.stats.discACCAvg),
discAoeMATG: Number(statObj.stats.discAoeMATG),
discComTG: Number(statObj.stats.discComTG),
discDeathAirAirTG: Number(statObj.stats.discDeathAirAirTG),
discDeathAirGroundTG: Number(statObj.stats.discDeathAirGroundTG),
discDeathAirTG: Number(statObj.stats.discDeathAirTG),
discDeathGroundAirTG: Number(statObj.stats.discDeathGroundAirTG),
discDeathGroundGroundTG: Number(statObj.stats.discDeathGroundGroundTG),
discDeathGroundTG: Number(statObj.stats.discDeathGroundTG),
discDeathsTG: Number(statObj.stats.discDeathsTG),
discDmgACCAvg: Number(statObj.stats.discDmgACCAvg),
discDmgHitsTG: Number(statObj.stats.discDmgHitsTG),
discDmgTG: Number(statObj.stats.discDmgTG),
discHitDistMax: Number(statObj.stats.discHitDistMax),
discHitsTG: Number(statObj.stats.discHitsTG),
discHitSVMax: Number(statObj.stats.discHitSVMax),
discHitVVMax: Number(statObj.stats.discHitVVMax),
discJumpTG: Number(statObj.stats.discJumpTG),
discKillAirAirTG: Number(statObj.stats.discKillAirAirTG),
discKillAirGroundTG: Number(statObj.stats.discKillAirGroundTG),
discKillAirTG: Number(statObj.stats.discKillAirTG),
discKillDistMax: Number(statObj.stats.discKillDistMax),
discKillGroundAirTG: Number(statObj.stats.discKillGroundAirTG),
discKillGroundGroundTG: Number(statObj.stats.discKillGroundGroundTG),
discKillGroundTG: Number(statObj.stats.discKillGroundTG),
discKillsTG: Number(statObj.stats.discKillsTG),
discKillSVMax: Number(statObj.stats.discKillSVMax),
discKillVVMax: Number(statObj.stats.discKillVVMax),
discMADistMax: Number(statObj.stats.discMADistMax),
discMAHitDistMax: Number(statObj.stats.discMAHitDistMax),
discMATG: Number(statObj.stats.discMATG),
discReflectHitTG: Number(statObj.stats.discReflectHitTG),
discReflectKillTG: Number(statObj.stats.discReflectKillTG),
discScoreTG: Number(statObj.stats.discScoreTG),
discShotsFiredTG: Number(statObj.stats.discShotsFiredTG),
distMovTG: Number(statObj.stats.distMovTG),
doubleChainKillTG: Number(statObj.stats.doubleChainKillTG),
doubleKillTG: Number(statObj.stats.doubleKillTG),
dtTeamGame: Number(statObj.stats.dtTeamGame),
dtTurretKillsTG: Number(statObj.stats.dtTurretKillsTG),
elfShotsFiredTG: Number(statObj.stats.elfShotsFiredTG),
elfTurretDeathsTG: Number(statObj.stats.elfTurretDeathsTG),
elfTurretKillsTG: Number(statObj.stats.elfTurretKillsTG),
escortAssistsTG: Number(statObj.stats.escortAssistsTG),
EVDeathsTG: Number(statObj.stats.EVDeathsTG),
EVHitWepTG: Number(statObj.stats.EVHitWepTG),
EVKillsTG: Number(statObj.stats.EVKillsTG),
EVMAHitTG: Number(statObj.stats.EVMAHitTG),
explosionDeathsTG: Number(statObj.stats.explosionDeathsTG),
explosionKillsTG: Number(statObj.stats.explosionKillsTG),
firstKillTG: Number(statObj.stats.firstKillTG),
flagCapsTG: Number(statObj.stats.flagCapsTG),
flagCatchSpeedMax: Number(statObj.stats.flagCatchSpeedMax),
flagCatchTG: Number(statObj.stats.flagCatchTG),
flagDefendsTG: Number(statObj.stats.flagDefendsTG),
flagGrabsTG: Number(statObj.stats.flagGrabsTG),
flagReturnsTG: Number(statObj.stats.flagReturnsTG),
flagTimeMinTG: Number(statObj.stats.flagTimeMinTG),
flagTossCatchTG: Number(statObj.stats.flagTossCatchTG),
flagTossTG: Number(statObj.stats.flagTossTG),
flareHitTG: Number(statObj.stats.flareHitTG),
flareKillTG: Number(statObj.stats.flareKillTG),
forceFieldPowerUpDeathsTG: Number(statObj.stats.forceFieldPowerUpDeathsTG),
forceFieldPowerUpKillsTG: Number(statObj.stats.forceFieldPowerUpKillsTG),
friendlyFireTG: Number(statObj.stats.friendlyFireTG),
fullSet: Number(statObj.stats.fullSet),
gamePCT: Number(statObj.stats.gamePCT),
startPCTGame: Number(statObj.stats.startPCTGame),
endPCTGame: Number(statObj.stats.endPCTGame),
teamOneCapTimesGame: formatArrayOfNumbers(statObj.stats.teamOneCapTimesGame),
teamTwoCapTimesGame: formatArrayOfNumbers(statObj.stats.teamTwoCapTimesGame),
mapSkipGame: Number(statObj.stats.mapSkipGame),
clientQuitGame: Number(statObj.stats.clientQuitGame),
genDefendsTG: Number(statObj.stats.genDefendsTG),
genDestroysTG: Number(statObj.stats.genDestroysTG),
genRepairsTG: Number(statObj.stats.genRepairsTG),
grabSpeedAvg: Number(statObj.stats.grabSpeedAvg),
grabSpeedLowMax: Number(statObj.stats.grabSpeedLowMax),
grabSpeedMax: Number(statObj.stats.grabSpeedMax),
grenadeACCAvg: Number(statObj.stats.grenadeACCAvg),
grenadeAoeMATG: Number(statObj.stats.grenadeAoeMATG),
grenadeComTG: Number(statObj.stats.grenadeComTG),
grenadeDeathAirAirTG: Number(statObj.stats.grenadeDeathAirAirTG),
grenadeDeathAirGroundTG: Number(statObj.stats.grenadeDeathAirGroundTG),
grenadeDeathAirTG: Number(statObj.stats.grenadeDeathAirTG),
grenadeDeathGroundAirTG: Number(statObj.stats.grenadeDeathGroundAirTG),
grenadeDeathGroundGroundTG: Number(statObj.stats.grenadeDeathGroundGroundTG),
grenadeDeathGroundTG: Number(statObj.stats.grenadeDeathGroundTG),
grenadeDeathsTG: Number(statObj.stats.grenadeDeathsTG),
grenadeDmgACCAvg: Number(statObj.stats.grenadeDmgACCAvg),
grenadeDmgHitsTG: Number(statObj.stats.grenadeDmgHitsTG),
grenadeDmgTG: Number(statObj.stats.grenadeDmgTG),
grenadeHitDistMax: Number(statObj.stats.grenadeHitDistMax),
grenadeHitsTG: Number(statObj.stats.grenadeHitsTG),
grenadeHitSVMax: Number(statObj.stats.grenadeHitSVMax),
grenadeHitVVMax: Number(statObj.stats.grenadeHitVVMax),
grenadeKillAirAirTG: Number(statObj.stats.grenadeKillAirAirTG),
grenadeKillAirGroundTG: Number(statObj.stats.grenadeKillAirGroundTG),
grenadeKillAirTG: Number(statObj.stats.grenadeKillAirTG),
grenadeKillDistMax: Number(statObj.stats.grenadeKillDistMax),
grenadeKillGroundAirTG: Number(statObj.stats.grenadeKillGroundAirTG),
grenadeKillGroundGroundTG: Number(statObj.stats.grenadeKillGroundGroundTG),
grenadeKillGroundTG: Number(statObj.stats.grenadeKillGroundTG),
grenadeKillsTG: Number(statObj.stats.grenadeKillsTG),
grenadeKillSVMax: Number(statObj.stats.grenadeKillSVMax),
grenadeKillVVMax: Number(statObj.stats.grenadeKillVVMax),
grenadeMADistMax: Number(statObj.stats.grenadeMADistMax),
grenadeMAHitDistMax: Number(statObj.stats.grenadeMAHitDistMax),
grenadeMATG: Number(statObj.stats.grenadeMATG),
grenadeScoreTG: Number(statObj.stats.grenadeScoreTG),
grenadeShotsFiredTG: Number(statObj.stats.grenadeShotsFiredTG),
groundDeathsTG: Number(statObj.stats.groundDeathsTG),
groundKillsTG: Number(statObj.stats.groundKillsTG),
groundTimeAvg: Number(statObj.stats.groundTimeAvg),
groundTimeTG: Number(statObj.stats.groundTimeTG),
hapcFlyerRDTG: Number(statObj.stats.hapcFlyerRDTG),
hapcFlyerRKTG: Number(statObj.stats.hapcFlyerRKTG),
hArmorTimeTG: Number(statObj.stats.hArmorTimeTG),
heldTimeSecAvgI: Number(statObj.stats.heldTimeSecAvgI),
heldTimeSecLowMin: Number(statObj.stats.heldTimeSecLowMin),
heldTimeSecMin: Number(statObj.stats.heldTimeSecMin),
hGrenadeACCAvg: Number(statObj.stats.hGrenadeACCAvg),
hGrenadeComTG: Number(statObj.stats.hGrenadeComTG),
hGrenadeDeathAirAirTG: Number(statObj.stats.hGrenadeDeathAirAirTG),
hGrenadeDeathAirGroundTG: Number(statObj.stats.hGrenadeDeathAirGroundTG),
hGrenadeDeathAirTG: Number(statObj.stats.hGrenadeDeathAirTG),
hGrenadeDeathGroundAirTG: Number(statObj.stats.hGrenadeDeathGroundAirTG),
hGrenadeDeathGroundGroundTG: Number(statObj.stats.hGrenadeDeathGroundGroundTG),
hGrenadeDeathGroundTG: Number(statObj.stats.hGrenadeDeathGroundTG),
hGrenadeDeathsTG: Number(statObj.stats.hGrenadeDeathsTG),
hGrenadeDmgTG: Number(statObj.stats.hGrenadeDmgTG),
hGrenadeHitDistMax: Number(statObj.stats.hGrenadeHitDistMax),
hGrenadeHitsTG: Number(statObj.stats.hGrenadeHitsTG),
hGrenadeHitSVMax: Number(statObj.stats.hGrenadeHitSVMax),
hGrenadeHitVVMax: Number(statObj.stats.hGrenadeHitVVMax),
hGrenadeKillAirAirTG: Number(statObj.stats.hGrenadeKillAirAirTG),
hGrenadeKillAirGroundTG: Number(statObj.stats.hGrenadeKillAirGroundTG),
hGrenadeKillAirTG: Number(statObj.stats.hGrenadeKillAirTG),
hGrenadeKillDistMax: Number(statObj.stats.hGrenadeKillDistMax),
hGrenadeKillGroundAirTG: Number(statObj.stats.hGrenadeKillGroundAirTG),
hGrenadeKillGroundGroundTG: Number(statObj.stats.hGrenadeKillGroundGroundTG),
hGrenadeKillGroundTG: Number(statObj.stats.hGrenadeKillGroundTG),
hGrenadeKillsTG: Number(statObj.stats.hGrenadeKillsTG),
hGrenadeKillSVMax: Number(statObj.stats.hGrenadeKillSVMax),
hGrenadeKillVVMax: Number(statObj.stats.hGrenadeKillVVMax),
hGrenadeMADistMax: Number(statObj.stats.hGrenadeMADistMax),
hGrenadeMAHitDistMax: Number(statObj.stats.hGrenadeMAHitDistMax),
hGrenadeMATG: Number(statObj.stats.hGrenadeMATG),
hGrenadeScoreTG: Number(statObj.stats.hGrenadeScoreTG),
hGrenadeShotsFiredTG: Number(statObj.stats.hGrenadeShotsFiredTG),
hitHeadBackTG: Number(statObj.stats.hitHeadBackTG),
hitHeadFrontTG: Number(statObj.stats.hitHeadFrontTG),
hitHeadLeftTG: Number(statObj.stats.hitHeadLeftTG),
hitHeadRightTG: Number(statObj.stats.hitHeadRightTG),
hitHeadTG: Number(statObj.stats.hitHeadTG),
hitLegBackLTG: Number(statObj.stats.hitLegBackLTG),
hitLegBackRTG: Number(statObj.stats.hitLegBackRTG),
hitLegFrontLTG: Number(statObj.stats.hitLegFrontLTG),
hitLegFrontRTG: Number(statObj.stats.hitLegFrontRTG),
hitLegsTG: Number(statObj.stats.hitLegsTG),
hitTakenHeadBackTG: Number(statObj.stats.hitTakenHeadBackTG),
hitTakenHeadFrontTG: Number(statObj.stats.hitTakenHeadFrontTG),
hitTakenHeadLeftTG: Number(statObj.stats.hitTakenHeadLeftTG),
hitTakenHeadRightTG: Number(statObj.stats.hitTakenHeadRightTG),
hitTakenHeadTG: Number(statObj.stats.hitTakenHeadTG),
hitTakenLegBackLTG: Number(statObj.stats.hitTakenLegBackLTG),
hitTakenLegBackRTG: Number(statObj.stats.hitTakenLegBackRTG),
hitTakenLegFrontLTG: Number(statObj.stats.hitTakenLegFrontLTG),
hitTakenLegFrontRTG: Number(statObj.stats.hitTakenLegFrontRTG),
hitTakenLegsTG: Number(statObj.stats.hitTakenLegsTG),
hitTakenTorsoBackLTG: Number(statObj.stats.hitTakenTorsoBackLTG),
hitTakenTorsoBackRTG: Number(statObj.stats.hitTakenTorsoBackRTG),
hitTakenTorsoFrontLTG: Number(statObj.stats.hitTakenTorsoFrontLTG),
hitTakenTorsoFrontRTG: Number(statObj.stats.hitTakenTorsoFrontRTG),
hitTakenTorsoTG: Number(statObj.stats.hitTakenTorsoTG),
hitTorsoBackLTG: Number(statObj.stats.hitTorsoBackLTG),
hitTorsoBackRTG: Number(statObj.stats.hitTorsoBackRTG),
hitTorsoFrontLTG: Number(statObj.stats.hitTorsoFrontLTG),
hitTorsoFrontRTG: Number(statObj.stats.hitTorsoFrontRTG),
hitTorsoTG: Number(statObj.stats.hitTorsoTG),
idleTimeAvg: Number(statObj.stats.idleTimeAvg),
idleTimeTG: Number(statObj.stats.idleTimeTG),
impactDeathsTG: Number(statObj.stats.impactDeathsTG),
impactKillsTG: Number(statObj.stats.impactKillsTG),
indoorDepTurretDeathsTG: Number(statObj.stats.indoorDepTurretDeathsTG),
indoorDepTurretKillsTG: Number(statObj.stats.indoorDepTurretKillsTG),
interceptedFlagTG: Number(statObj.stats.interceptedFlagTG),
interceptFlagSpeedMax: Number(statObj.stats.interceptFlagSpeedMax),
interceptSpeedMax: Number(statObj.stats.interceptSpeedMax),
inventoryDeathsTG: Number(statObj.stats.inventoryDeathsTG),
InventoryDepTG: Number(statObj.stats.InventoryDepTG),
inventoryKillsTG: Number(statObj.stats.inventoryKillsTG),
iStationDestroysTG: Number(statObj.stats.iStationDestroysTG),
kdrAvg: Number(statObj.stats.kdrAvg),
killAirAirTG: Number(statObj.stats.killAirAirTG),
killAirGroundTG: Number(statObj.stats.killAirGroundTG),
killAirTG: Number(statObj.stats.killAirTG),
killerDiscJumpTG: Number(statObj.stats.killerDiscJumpTG),
killGroundAirTG: Number(statObj.stats.killGroundAirTG),
killGroundGroundTG: Number(statObj.stats.killGroundGroundTG),
killGroundTG: Number(statObj.stats.killGroundTG),
killsTG: Number(statObj.stats.killsTG),
killStreakMax: Number(statObj.stats.killStreakMax),
killStreakTG: Number(statObj.stats.killStreakTG),
lArmorTimeTG: Number(statObj.stats.lArmorTimeTG),
laserACCAvg: Number(statObj.stats.laserACCAvg),
laserComTG: Number(statObj.stats.laserComTG),
laserDeathAirAirTG: Number(statObj.stats.laserDeathAirAirTG),
laserDeathAirGroundTG: Number(statObj.stats.laserDeathAirGroundTG),
laserDeathAirTG: Number(statObj.stats.laserDeathAirTG),
laserDeathGroundAirTG: Number(statObj.stats.laserDeathGroundAirTG),
laserDeathGroundGroundTG: Number(statObj.stats.laserDeathGroundGroundTG),
laserDeathGroundTG: Number(statObj.stats.laserDeathGroundTG),
laserDeathsTG: Number(statObj.stats.laserDeathsTG),
laserDmgTG: Number(statObj.stats.laserDmgTG),
laserHeadShotTG: Number(statObj.stats.laserHeadShotTG),
laserHitDistMax: Number(statObj.stats.laserHitDistMax),
laserHitsTG: Number(statObj.stats.laserHitsTG),
laserHitSVMax: Number(statObj.stats.laserHitSVMax),
laserHitVVMax: Number(statObj.stats.laserHitVVMax),
laserKillAirAirTG: Number(statObj.stats.laserKillAirAirTG),
laserKillAirGroundTG: Number(statObj.stats.laserKillAirGroundTG),
laserKillAirTG: Number(statObj.stats.laserKillAirTG),
laserKillDistMax: Number(statObj.stats.laserKillDistMax),
laserKillGroundAirTG: Number(statObj.stats.laserKillGroundAirTG),
laserKillGroundGroundTG: Number(statObj.stats.laserKillGroundGroundTG),
laserKillGroundTG: Number(statObj.stats.laserKillGroundTG),
laserKillsTG: Number(statObj.stats.laserKillsTG),
laserKillSVMax: Number(statObj.stats.laserKillSVMax),
laserKillVVMax: Number(statObj.stats.laserKillVVMax),
laserMADistMax: Number(statObj.stats.laserMADistMax),
laserMAHitDistMax: Number(statObj.stats.laserMAHitDistMax),
laserMATG: Number(statObj.stats.laserMATG),
laserScoreTG: Number(statObj.stats.laserScoreTG),
laserShotsFiredTG: Number(statObj.stats.laserShotsFiredTG),
lastKillTG: Number(statObj.stats.lastKillTG),
lavaDeathsTG: Number(statObj.stats.lavaDeathsTG),
lavaKillsTG: Number(statObj.stats.lavaKillsTG),
lightningDeathsTG: Number(statObj.stats.lightningDeathsTG),
lightningKillsTG: Number(statObj.stats.lightningKillsTG),
lightningMAEVHitsTG: Number(statObj.stats.lightningMAEVHitsTG),
lightningMAEVKillsTG: Number(statObj.stats.lightningMAEVKillsTG),
lightningMAHitsTG: Number(statObj.stats.lightningMAHitsTG),
lightningMAkillsTG: Number(statObj.stats.lightningMAkillsTG),
lossCountTG: Number(statObj.stats.lossCountTG),
maFlagCatchSpeedMax: Number(statObj.stats.maFlagCatchSpeedMax),
maFlagCatchTG: Number(statObj.stats.maFlagCatchTG),
maHitDistMax: Number(statObj.stats.maHitDistMax),
maHitHeightMax: Number(statObj.stats.maHitHeightMax),
maHitSVMax: Number(statObj.stats.maHitSVMax),
maInterceptedFlagTG: Number(statObj.stats.maInterceptedFlagTG),
mannedTurretKillsTG: Number(statObj.stats.mannedTurretKillsTG),
mapGameID: Number(statObj.stats.mapGameID),
mArmorTimeTG: Number(statObj.stats.mArmorTimeTG),
masTG: Number(statObj.stats.masTG),
maxSpeedMax: Number(statObj.stats.maxSpeedMax),
MidairflagGrabPointsTG: Number(statObj.stats.MidairflagGrabPointsTG),
MidairflagGrabsTG: Number(statObj.stats.MidairflagGrabsTG),
mineACCAvg: Number(statObj.stats.mineACCAvg),
mineComTG: Number(statObj.stats.mineComTG),
mineDeathAirAirTG: Number(statObj.stats.mineDeathAirAirTG),
mineDeathAirGroundTG: Number(statObj.stats.mineDeathAirGroundTG),
mineDeathAirTG: Number(statObj.stats.mineDeathAirTG),
mineDeathGroundAirTG: Number(statObj.stats.mineDeathGroundAirTG),
mineDeathGroundGroundTG: Number(statObj.stats.mineDeathGroundGroundTG),
mineDeathGroundTG: Number(statObj.stats.mineDeathGroundTG),
mineDeathsTG: Number(statObj.stats.mineDeathsTG),
mineDiscAccAvg: Number(statObj.stats.mineDiscAccAvg),
mineDiscAccMPAvg: Number(statObj.stats.mineDiscAccMPAvg),
mineDiscHitTG: Number(statObj.stats.mineDiscHitTG),
mineDiscPctAvg: Number(statObj.stats.mineDiscPctAvg),
mineDiscShotsTG: Number(statObj.stats.mineDiscShotsTG),
mineDmgTG: Number(statObj.stats.mineDmgTG),
mineHitDistMax: Number(statObj.stats.mineHitDistMax),
mineHitsTG: Number(statObj.stats.mineHitsTG),
mineHitVVMax: Number(statObj.stats.mineHitVVMax),
mineKillAGroundAirTG: Number(statObj.stats.mineKillAGroundAirTG),
mineKillAGroundGroundTG: Number(statObj.stats.mineKillAGroundGroundTG),
mineKillAirAirTG: Number(statObj.stats.mineKillAirAirTG),
mineKillAirGroundTG: Number(statObj.stats.mineKillAirGroundTG),
mineKillAirTG: Number(statObj.stats.mineKillAirTG),
mineKillDistMax: Number(statObj.stats.mineKillDistMax),
mineKillGroundAirTG: Number(statObj.stats.mineKillGroundAirTG),
mineKillGroundGroundTG: Number(statObj.stats.mineKillGroundGroundTG),
mineKillGroundTG: Number(statObj.stats.mineKillGroundTG),
mineKillsTG: Number(statObj.stats.mineKillsTG),
mineKillVVMax: Number(statObj.stats.mineKillVVMax),
mineMADistMax: Number(statObj.stats.mineMADistMax),
mineMAHitDistMax: Number(statObj.stats.mineMAHitDistMax),
mineMATG: Number(statObj.stats.mineMATG),
minePlusDiscKillTG: Number(statObj.stats.minePlusDiscKillTG),
minePlusDiscTG: Number(statObj.stats.minePlusDiscTG),
mineScoreTG: Number(statObj.stats.mineScoreTG),
mineShotsFiredTG: Number(statObj.stats.mineShotsFiredTG),
missileACCAvg: Number(statObj.stats.missileACCAvg),
missileComTG: Number(statObj.stats.missileComTG),
missileDeathAirAirTG: Number(statObj.stats.missileDeathAirAirTG),
missileDeathAirGroundTG: Number(statObj.stats.missileDeathAirGroundTG),
missileDeathAirTG: Number(statObj.stats.missileDeathAirTG),
missileDeathGroundAirTG: Number(statObj.stats.missileDeathGroundAirTG),
missileDeathGroundGroundTG: Number(statObj.stats.missileDeathGroundGroundTG),
missileDeathGroundTG: Number(statObj.stats.missileDeathGroundTG),
missileDeathsTG: Number(statObj.stats.missileDeathsTG),
missileDmgTG: Number(statObj.stats.missileDmgTG),
missileHitDistMax: Number(statObj.stats.missileHitDistMax),
missileHitsTG: Number(statObj.stats.missileHitsTG),
missileHitSVMax: Number(statObj.stats.missileHitSVMax),
missileHitVVMax: Number(statObj.stats.missileHitVVMax),
missileKillAirAirTG: Number(statObj.stats.missileKillAirAirTG),
missileKillAirGroundTG: Number(statObj.stats.missileKillAirGroundTG),
missileKillAirTG: Number(statObj.stats.missileKillAirTG),
missileKillDistMax: Number(statObj.stats.missileKillDistMax),
missileKillGroundAirTG: Number(statObj.stats.missileKillGroundAirTG),
missileKillGroundGroundTG: Number(statObj.stats.missileKillGroundGroundTG),
missileKillGroundTG: Number(statObj.stats.missileKillGroundTG),
missileKillsTG: Number(statObj.stats.missileKillsTG),
missileKillSVMax: Number(statObj.stats.missileKillSVMax),
missileKillVVMax: Number(statObj.stats.missileKillVVMax),
missileMADistMax: Number(statObj.stats.missileMADistMax),
missileMAHitDistMax: Number(statObj.stats.missileMAHitDistMax),
missileMATG: Number(statObj.stats.missileMATG),
missileScoreTG: Number(statObj.stats.missileScoreTG),
missileShotsFiredTG: Number(statObj.stats.missileShotsFiredTG),
missileTKTG: Number(statObj.stats.missileTKTG),
missileTurretDeathsTG: Number(statObj.stats.missileTurretDeathsTG),
missileTurretKillsTG: Number(statObj.stats.missileTurretKillsTG),
mobileBaseRDTG: Number(statObj.stats.mobileBaseRDTG),
mobileBaseRKTG: Number(statObj.stats.mobileBaseRKTG),
monthStamp: Number(statObj.stats.monthStamp),
morepointsTG: Number(statObj.stats.morepointsTG),
mortarACCAvg: Number(statObj.stats.mortarACCAvg),
mortarAoeMATG: Number(statObj.stats.mortarAoeMATG),
mortarComTG: Number(statObj.stats.mortarComTG),
mortarDeathAirAirTG: Number(statObj.stats.mortarDeathAirAirTG),
mortarDeathAirGroundTG: Number(statObj.stats.mortarDeathAirGroundTG),
mortarDeathAirTG: Number(statObj.stats.mortarDeathAirTG),
mortarDeathGroundAirTG: Number(statObj.stats.mortarDeathGroundAirTG),
mortarDeathGroundGroundTG: Number(statObj.stats.mortarDeathGroundGroundTG),
mortarDeathGroundTG: Number(statObj.stats.mortarDeathGroundTG),
mortarDeathsTG: Number(statObj.stats.mortarDeathsTG),
mortarDmgACCAvg: Number(statObj.stats.mortarDmgACCAvg),
mortarDmgHitsTG: Number(statObj.stats.mortarDmgHitsTG),
mortarDmgTG: Number(statObj.stats.mortarDmgTG),
mortarHitDistMax: Number(statObj.stats.mortarHitDistMax),
mortarHitsTG: Number(statObj.stats.mortarHitsTG),
mortarHitSVMax: Number(statObj.stats.mortarHitSVMax),
mortarHitVVMax: Number(statObj.stats.mortarHitVVMax),
mortarKillAirAirTG: Number(statObj.stats.mortarKillAirAirTG),
mortarKillAirGroundTG: Number(statObj.stats.mortarKillAirGroundTG),
mortarKillAirTG: Number(statObj.stats.mortarKillAirTG),
mortarKillDistMax: Number(statObj.stats.mortarKillDistMax),
mortarKillGroundAirTG: Number(statObj.stats.mortarKillGroundAirTG),
mortarKillGroundGroundTG: Number(statObj.stats.mortarKillGroundGroundTG),
mortarKillGroundTG: Number(statObj.stats.mortarKillGroundTG),
mortarKillsTG: Number(statObj.stats.mortarKillsTG),
mortarKillSVMax: Number(statObj.stats.mortarKillSVMax),
mortarKillVVMax: Number(statObj.stats.mortarKillVVMax),
mortarMADistMax: Number(statObj.stats.mortarMADistMax),
mortarMAHitDistMax: Number(statObj.stats.mortarMAHitDistMax),
mortarMATG: Number(statObj.stats.mortarMATG),
mortarScoreTG: Number(statObj.stats.mortarScoreTG),
mortarShotsFiredTG: Number(statObj.stats.mortarShotsFiredTG),
mortarTurretDeathsTG: Number(statObj.stats.mortarTurretDeathsTG),
mortarTurretKillsTG: Number(statObj.stats.mortarTurretKillsTG),
MotionSensorDepTG: Number(statObj.stats.MotionSensorDepTG),
mpbGlitchTG: Number(statObj.stats.mpbGlitchTG),
mpbtstationDestroysTG: Number(statObj.stats.mpbtstationDestroysTG),
mpbtstationRepairsTG: Number(statObj.stats.mpbtstationRepairsTG),
multiKillTG: Number(statObj.stats.multiKillTG),
nexusCampingDeathsTG: Number(statObj.stats.nexusCampingDeathsTG),
nexusCampingKillsTG: Number(statObj.stats.nexusCampingKillsTG),
nonupleChainKillTG: Number(statObj.stats.nonupleChainKillTG),
nonupleKillTG: Number(statObj.stats.nonupleKillTG),
nuclearKillTG: Number(statObj.stats.nuclearKillTG),
nullTG: Number(statObj.stats.nullTG),
octupleChainKillTG: Number(statObj.stats.octupleChainKillTG),
octupleKillTG: Number(statObj.stats.octupleKillTG),
offenseScoreTG: Number(statObj.stats.offenseScoreTG),
onFireAvg: Number(statObj.stats.onFireAvg),
onFireTG: Number(statObj.stats.onFireTG),
onInputAvg: Number(statObj.stats.onInputAvg),
onInputTG: Number(statObj.stats.onInputTG),
onTargetAccAvg: Number(statObj.stats.onTargetAccAvg),
onTargetHitTG: Number(statObj.stats.onTargetHitTG),
onTargetHMRAvg: Number(statObj.stats.onTargetHMRAvg),
onTargetMisTG: Number(statObj.stats.onTargetMisTG),
outdoorDepTurretDeathsTG: Number(statObj.stats.outdoorDepTurretDeathsTG),
outdoorDepTurretKillsTG: Number(statObj.stats.outdoorDepTurretKillsTG),
outOfBoundDeathsTG: Number(statObj.stats.outOfBoundDeathsTG),
outOfBoundKillsTG: Number(statObj.stats.outOfBoundKillsTG),
plasmaACCAvg: Number(statObj.stats.plasmaACCAvg),
plasmaAoeMATG: Number(statObj.stats.plasmaAoeMATG),
plasmaComTG: Number(statObj.stats.plasmaComTG),
plasmaDeathAirAirTG: Number(statObj.stats.plasmaDeathAirAirTG),
plasmaDeathAirGroundTG: Number(statObj.stats.plasmaDeathAirGroundTG),
plasmaDeathAirTG: Number(statObj.stats.plasmaDeathAirTG),
plasmaDeathGroundAirTG: Number(statObj.stats.plasmaDeathGroundAirTG),
plasmaDeathGroundGroundTG: Number(statObj.stats.plasmaDeathGroundGroundTG),
plasmaDeathGroundTG: Number(statObj.stats.plasmaDeathGroundTG),
plasmaDeathsTG: Number(statObj.stats.plasmaDeathsTG),
plasmaDmgACCAvg: Number(statObj.stats.plasmaDmgACCAvg),
plasmaDmgHitsTG: Number(statObj.stats.plasmaDmgHitsTG),
plasmaDmgTG: Number(statObj.stats.plasmaDmgTG),
plasmaHitDistMax: Number(statObj.stats.plasmaHitDistMax),
plasmaHitsTG: Number(statObj.stats.plasmaHitsTG),
plasmaHitSVMax: Number(statObj.stats.plasmaHitSVMax),
plasmaHitVVMax: Number(statObj.stats.plasmaHitVVMax),
plasmaKillAirAirTG: Number(statObj.stats.plasmaKillAirAirTG),
plasmaKillAirGroundTG: Number(statObj.stats.plasmaKillAirGroundTG),
plasmaKillAirTG: Number(statObj.stats.plasmaKillAirTG),
plasmaKillDistMax: Number(statObj.stats.plasmaKillDistMax),
plasmaKillGroundAirTG: Number(statObj.stats.plasmaKillGroundAirTG),
plasmaKillGroundGroundTG: Number(statObj.stats.plasmaKillGroundGroundTG),
plasmaKillGroundTG: Number(statObj.stats.plasmaKillGroundTG),
plasmaKillsTG: Number(statObj.stats.plasmaKillsTG),
plasmaKillSVMax: Number(statObj.stats.plasmaKillSVMax),
plasmaKillVVMax: Number(statObj.stats.plasmaKillVVMax),
plasmaMADistMax: Number(statObj.stats.plasmaMADistMax),
plasmaMAHitDistMax: Number(statObj.stats.plasmaMAHitDistMax),
plasmaMATG: Number(statObj.stats.plasmaMATG),
plasmaScoreTG: Number(statObj.stats.plasmaScoreTG),
plasmaShotsFiredTG: Number(statObj.stats.plasmaShotsFiredTG),
plasmaTurretDeathsTG: Number(statObj.stats.plasmaTurretDeathsTG),
plasmaTurretKillsTG: Number(statObj.stats.plasmaTurretKillsTG),
playerName: Number(statObj.stats.playerName),
PulseSensorDepTG: Number(statObj.stats.PulseSensorDepTG),
quadrupleChainKillTG: Number(statObj.stats.quadrupleChainKillTG),
quadrupleKillTG: Number(statObj.stats.quadrupleKillTG),
quarterStamp: Number(statObj.stats.quarterStamp),
quintupleChainKillTG: Number(statObj.stats.quintupleChainKillTG),
quintupleKillTG: Number(statObj.stats.quintupleKillTG),
repairEnemyTG: Number(statObj.stats.repairEnemyTG),
repairsTG: Number(statObj.stats.repairsTG),
roadDeathsTG: Number(statObj.stats.roadDeathsTG),
roadKillsTG: Number(statObj.stats.roadKillsTG),
satchelACCAvg: Number(statObj.stats.satchelACCAvg),
satchelComTG: Number(statObj.stats.satchelComTG),
satchelDeathAirAirTG: Number(statObj.stats.satchelDeathAirAirTG),
satchelDeathAirGroundTG: Number(statObj.stats.satchelDeathAirGroundTG),
satchelDeathAirTG: Number(statObj.stats.satchelDeathAirTG),
satchelDeathGroundAirTG: Number(statObj.stats.satchelDeathGroundAirTG),
satchelDeathGroundGroundTG: Number(statObj.stats.satchelDeathGroundGroundTG),
satchelDeathsTG: Number(statObj.stats.satchelDeathsTG),
satchelDmgTG: Number(statObj.stats.satchelDmgTG),
satchelHitDistMax: Number(statObj.stats.satchelHitDistMax),
satchelHitsTG: Number(statObj.stats.satchelHitsTG),
satchelHitVVMax: Number(statObj.stats.satchelHitVVMax),
satchelKillAGroundAirTG: Number(statObj.stats.satchelKillAGroundAirTG),
satchelKillAGroundGroundTG: Number(statObj.stats.satchelKillAGroundGroundTG),
satchelKillAirAirTG: Number(statObj.stats.satchelKillAirAirTG),
satchelKillAirGroundTG: Number(statObj.stats.satchelKillAirGroundTG),
satchelKillAirTG: Number(statObj.stats.satchelKillAirTG),
satchelKillDistMax: Number(statObj.stats.satchelKillDistMax),
satchelKillGroundAirTG: Number(statObj.stats.satchelKillGroundAirTG),
satchelKillGroundGroundTG: Number(statObj.stats.satchelKillGroundGroundTG),
satchelKillGroundTG: Number(statObj.stats.satchelKillGroundTG),
satchelKillsTG: Number(statObj.stats.satchelKillsTG),
satchelKillVVMax: Number(statObj.stats.satchelKillVVMax),
satchelMATG: Number(statObj.stats.satchelMATG),
satchelScoreTG: Number(statObj.stats.satchelScoreTG),
satchelShotsFiredTG: Number(statObj.stats.satchelShotsFiredTG),
scoreAvg: Number(statObj.stats.scoreAvg),
scoreHeadshotTG: Number(statObj.stats.scoreHeadshotTG),
scoreMax: Number(statObj.stats.scoreMax),
scoreMidAirTG: Number(statObj.stats.scoreMidAirTG),
scoreRearshotTG: Number(statObj.stats.scoreRearshotTG),
scoreTG: Number(statObj.stats.scoreTG),
scoutFlyerRDTG: Number(statObj.stats.scoutFlyerRDTG),
scoutFlyerRKTG: Number(statObj.stats.scoutFlyerRKTG),
sensorDestroysTG: Number(statObj.stats.sensorDestroysTG),
SensorRepairsTG: Number(statObj.stats.SensorRepairsTG),
sentryDestroysTG: Number(statObj.stats.sentryDestroysTG),
sentryRepairsTG: Number(statObj.stats.sentryRepairsTG),
sentryTurretDeathsTG: Number(statObj.stats.sentryTurretDeathsTG),
sentryTurretKillsTG: Number(statObj.stats.sentryTurretKillsTG),
septupleChainKillTG: Number(statObj.stats.septupleChainKillTG),
septupleKillTG: Number(statObj.stats.septupleKillTG),
sextupleChainKillTG: Number(statObj.stats.sextupleChainKillTG),
sextupleKillTG: Number(statObj.stats.sextupleKillTG),
shockACCAvg: Number(statObj.stats.shockACCAvg),
shockComTG: Number(statObj.stats.shockComTG),
shockDeathAirAirTG: Number(statObj.stats.shockDeathAirAirTG),
shockDeathAirGroundTG: Number(statObj.stats.shockDeathAirGroundTG),
shockDeathAirTG: Number(statObj.stats.shockDeathAirTG),
shockDeathGroundAirTG: Number(statObj.stats.shockDeathGroundAirTG),
shockDeathGroundGroundTG: Number(statObj.stats.shockDeathGroundGroundTG),
shockDeathGroundTG: Number(statObj.stats.shockDeathGroundTG),
shockDeathsTG: Number(statObj.stats.shockDeathsTG),
shockDmgTG: Number(statObj.stats.shockDmgTG),
shockHitDistMax: Number(statObj.stats.shockHitDistMax),
shockHitsTG: Number(statObj.stats.shockHitsTG),
shockHitSVMax: Number(statObj.stats.shockHitSVMax),
shockHitVVMax: Number(statObj.stats.shockHitVVMax),
shockKillAirAirTG: Number(statObj.stats.shockKillAirAirTG),
shockKillAirGroundTG: Number(statObj.stats.shockKillAirGroundTG),
shockKillAirTG: Number(statObj.stats.shockKillAirTG),
shockKillDistMax: Number(statObj.stats.shockKillDistMax),
shockKillGroundAirTG: Number(statObj.stats.shockKillGroundAirTG),
shockKillGroundGroundTG: Number(statObj.stats.shockKillGroundGroundTG),
shockKillGroundTG: Number(statObj.stats.shockKillGroundTG),
shockKillsTG: Number(statObj.stats.shockKillsTG),
shockKillSVMax: Number(statObj.stats.shockKillSVMax),
shockKillVVMax: Number(statObj.stats.shockKillVVMax),
shockMADistMax: Number(statObj.stats.shockMADistMax),
shockMAHitDistMax: Number(statObj.stats.shockMAHitDistMax),
shockMATG: Number(statObj.stats.shockMATG),
shockRearShotTG: Number(statObj.stats.shockRearShotTG),
shockScoreTG: Number(statObj.stats.shockScoreTG),
shockShotsFiredTG: Number(statObj.stats.shockShotsFiredTG),
shotsFiredTG: Number(statObj.stats.shotsFiredTG),
shrikeBlasterDeathsTG: Number(statObj.stats.shrikeBlasterDeathsTG),
shrikeBlasterKillsTG: Number(statObj.stats.shrikeBlasterKillsTG),
solarDestroysTG: Number(statObj.stats.solarDestroysTG),
solarRepairsTG: Number(statObj.stats.solarRepairsTG),
StationRepairsTG: Number(statObj.stats.StationRepairsTG),
statsOverWrite: Number(statObj.stats.statsOverWrite),
suicidesTG: Number(statObj.stats.suicidesTG),
tankChaingunDeathsTG: Number(statObj.stats.tankChaingunDeathsTG),
tankChaingunKillsTG: Number(statObj.stats.tankChaingunKillsTG),
tankMortarDeathsTG: Number(statObj.stats.tankMortarDeathsTG),
tankMortarKillsTG: Number(statObj.stats.tankMortarKillsTG),
teamKillsTG: Number(statObj.stats.teamKillsTG),
teamScoreGame: Number(statObj.stats.teamScoreGame),
timeTLAvg: Number(statObj.stats.timeTLAvg),
timeTLTG: Number(statObj.stats.timeTLTG),
tkDestroysTG: Number(statObj.stats.tkDestroysTG),
totalGames: Number(statObj.stats.totalGames),
totalMATG: Number(statObj.stats.totalMATG),
totalTimeTG: Number(statObj.stats.totalTimeTG),
timeOnTeamZeroTG: Number(statObj.stats.timeOnTeamZeroTG),
timeOnTeamOneTG: Number(statObj.stats.timeOnTeamOneTG),
timeOnTeamTwoTG: Number(statObj.stats.timeOnTeamTwoTG),
matchRunTimeTG: Number(statObj.stats.matchRunTimeTG),
totalWepDmgTG: Number(statObj.stats.totalWepDmgTG),
tripleChainKillTG: Number(statObj.stats.tripleChainKillTG),
tripleKillTG: Number(statObj.stats.tripleKillTG),
turretDestroysTG: Number(statObj.stats.turretDestroysTG),
TurretIndoorDepTG: Number(statObj.stats.TurretIndoorDepTG),
turretKillsTG: Number(statObj.stats.turretKillsTG),
TurretOutdoorDepTG: Number(statObj.stats.TurretOutdoorDepTG),
TurretRepairsTG: Number(statObj.stats.TurretRepairsTG),
vehicleBonusTG: Number(statObj.stats.vehicleBonusTG),
vehicleScoreTG: Number(statObj.stats.vehicleScoreTG),
vehicleSpawnDeathsTG: Number(statObj.stats.vehicleSpawnDeathsTG),
vehicleSpawnKillsTG: Number(statObj.stats.vehicleSpawnKillsTG),
versionNum: Number(statObj.stats.versionNum),
vstationDestroysTG: Number(statObj.stats.vstationDestroysTG),
VStationRepairsTG: Number(statObj.stats.VStationRepairsTG),
weaponHitDistMax: Number(statObj.stats.weaponHitDistMax),
weaponScoreTG: Number(statObj.stats.weaponScoreTG),
weekStamp: Number(statObj.stats.weekStamp),
wildRDTG: Number(statObj.stats.wildRDTG),
wildRKTG: Number(statObj.stats.wildRKTG),
winCountTG: Number(statObj.stats.winCountTG),
winLostPctAvg: Number(statObj.stats.winLostPctAvg),
yearStamp: Number(statObj.stats.yearStamp),
};
}
export default formatPlayerStats;

View file

@ -0,0 +1,47 @@
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Games } from '../../games/entities/Games';
import { Players } from '../../players/entities/Players';
@Index('games_pk', [ 'id' ], { unique: true })
@Index('game_detail_uuid_key', [ 'uuid' ], { unique: true })
@Entity('game_detail', { schema: 'public' })
export class GameDetail {
@PrimaryGeneratedColumn({ type: 'integer', name: 'id' })
id: number;
@Column('text', { name: 'player_name' })
playerName: string;
@Column('numeric', { name: 'stat_overwrite', select: false })
statOverwrite: string;
@Column('text', { name: 'map' })
map: string;
@Column('jsonb', { name: 'stats' })
stats: any;
@Column('timestamp without time zone', { name: 'datestamp' })
datestamp: Date;
@Column('text', { name: 'uuid', unique: true })
uuid: string;
@Column('text', { name: 'gametype' })
gametype: string;
@Column('timestamp with time zone', {
name: 'created_at',
default: () => 'now()'
})
createdAt: Date;
@ManyToOne(() => Games, (games) => games.gameDetails)
@JoinColumn([ { name: 'game_id', referencedColumnName: 'gameId' } ])
game: Games;
@ManyToOne(() => Players, (players) => players.gameDetails)
@JoinColumn([ { name: 'player_guid', referencedColumnName: 'playerGuid' } ])
playerGuid: Players;
}

View file

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GameController } from './game.controller';
describe('Game Controller', () => {
let controller: GameController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [GameController],
}).compile();
controller = module.get<GameController>(GameController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View file

@ -0,0 +1,16 @@
import { Controller, Get, Param } from '@nestjs/common';
import { ApiOperation } from '@nestjs/swagger';
import { GameService } from './game.service';
@Controller('game')
export class GameController {
constructor(private readonly gameService: GameService) {}
// /games/:gameId
@Get(':gameId')
@ApiOperation({ tags: [ 'Game' ], summary: 'Find game by Id' })
findOne(@Param('gameId') gameId: string) {
return this.gameService.findOne(gameId);
}
}

View file

@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { GameController } from './game.controller';
import { GameService } from './game.service';
import { GameDetail } from './entities/GameDetail';
import { Games } from '../games/entities/Games';
import { Players } from '../players/entities/Players';
@Module({
imports: [ TypeOrmModule.forFeature([ Games, GameDetail, Players ]), ConfigModule ],
controllers: [ GameController ],
providers: [ GameService ],
exports: [ GameService ]
})
export class GameModule {}

View file

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GameService } from './game.service';
describe('GameService', () => {
let service: GameService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [GameService],
}).compile();
service = module.get<GameService>(GameService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View file

@ -0,0 +1,188 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Connection, Repository } from 'typeorm';
import { Games } from '../games/entities/Games';
import { GameDetail } from './entities/GameDetail';
import formatPlayerStats from '../common/util/formatStats';
@Injectable()
export class GameService {
constructor(
private readonly connection: Connection,
private readonly configService: ConfigService,
@InjectRepository(Games)
private readonly gamesRepository: Repository<Games>,
@InjectRepository(GameDetail)
private readonly gameRepository: Repository<GameDetail>,
) {}
async findOne(gameId: string) {
const query = await this.gameRepository.find({
relations: ['game', 'playerGuid'],
where: [{ game: { gameId: gameId } }],
});
if (!query.length) {
throw new NotFoundException(`Game ID: ${gameId} not found`);
}
const game: any = {
...query[0].game,
};
// Need to set return based off gameType
// Modify game object if not a CTF type game and return early
if (query[0].gametype !== 'CTFGame' && query[0].gametype !== 'SCtFGame') {
game['players'] = [];
for (const player of query) {
const { playerName } = player;
const stats = formatPlayerStats(player);
const p = {
playerGuid: player.playerGuid.playerGuid,
playerName,
stats,
};
game.players.push(p);
}
return game;
}
// Team Based game stats (CTF/SCtF)
game['teams'] = {
obs: { score: 0, players: [] },
storm: { score: 0, players: [] },
inferno: { score: 0, players: [] },
};
const teamZero = [],
teamOne = [],
teamTwo = [];
for (const player of query) {
const { playerName } = player;
const stats = formatPlayerStats(player);
const p = {
playerGuid: player.playerGuid.playerGuid,
playerName,
stats,
};
if (isNaN(player.stats.teamScoreGame)) {
// legacy calculations for game totals (not using the new teamScoreGame attribute)
const flagGrabsTG = parseInt(player.stats.flagGrabsTG[0]);
const flagCapsTG = parseInt(player.stats.flagCapsTG[0]) * 100;
const totalFlagScore = flagGrabsTG + flagCapsTG;
if (player.stats.dtTeamGame[0] === '1') {
// Storm
game.teams.storm.score += totalFlagScore;
teamOne.push(p);
} else if (player.stats.dtTeamGame[0] === '2') {
// Inferno
game.teams.inferno.score += totalFlagScore;
teamTwo.push(p);
} else {
// OBS
game.teams.obs.score += totalFlagScore;
teamZero.push(p);
}
} else {
// Use new player.stats.teamScoreGame key
if (player.stats.dtTeamGame[0] === '1') {
// Storm
game.teams.storm.score = Number(player.stats.teamScoreGame);
teamOne.push(p);
} else if (player.stats.dtTeamGame[0] === '2') {
// Inferno
game.teams.inferno.score = Number(player.stats.teamScoreGame);
teamTwo.push(p);
} else {
// OBS
game.teams.obs.score = Number(player.stats.teamScoreGame);
teamZero.push(p);
}
}
}
game['teams']['obs']['players'] = teamZero.sort(
(a, b) => b.stats.scoreTG - a.stats.scoreTG,
);
game['teams']['storm']['players'] = teamOne.sort(
(a, b) => b.stats.scoreTG - a.stats.scoreTG,
);
game['teams']['inferno']['players'] = teamTwo.sort(
(a, b) => b.stats.scoreTG - a.stats.scoreTG,
);
return game;
}
async findOneAbvSummary(gameId: string) {
const query = await this.gameRepository.find({
relations: ['game', 'playerGuid'],
where: [{ game: { gameId: gameId } }],
});
if (!query.length) {
throw new NotFoundException(`Game ID: ${gameId} not found`);
}
const game: any = {
...query[0].game,
};
// Need to set return based off gameType
// Modify game object if not a CTF type game and return early
if (query[0].gametype !== 'CTFGame' && query[0].gametype !== 'SCtFGame') {
game['totalScore'] = 0;
for (const player of query) {
const stats = formatPlayerStats(player);
game.totalScore += stats.scoreTG;
}
return game;
}
// Team Based game stats (CTF/SCtF)
game['teams'] = {
obs: { score: 0, playerCount: 0 },
storm: { score: 0, playerCount: 0 },
inferno: { score: 0, playerCount: 0 },
};
// console.log(query);
for (const player of query) {
const flagGrabsTG = parseInt(player.stats.flagGrabsTG[0]);
const flagCapsTG = parseInt(player.stats.flagCapsTG[0]) * 100;
const totalFlagScore = flagGrabsTG + flagCapsTG;
if (player.stats.dtTeamGame[0] === '1') {
// Storm
game.teams.storm.score += totalFlagScore;
game.teams.storm.playerCount += 1;
} else if (player.stats.dtTeamGame[0] === '2') {
// Inferno
game.teams.inferno.score += totalFlagScore;
game.teams.inferno.playerCount += 1;
} else {
// OBS
game.teams.obs.score += totalFlagScore;
game.teams.obs.playerCount += 1;
}
}
game['totalScore'] =
game.teams.storm.score + game.teams.inferno.score + game.teams.obs.score;
return game;
}
}

View file

@ -0,0 +1,27 @@
import { Column, Entity, Index, OneToMany } from 'typeorm';
import { GameDetail } from '../../game/entities/GameDetail';
@Index('game_pk', [ 'gameId' ], { unique: true })
@Entity('games', { schema: 'public' })
export class Games {
@Column('numeric', { primary: true, name: 'game_id' })
gameId: string;
@Column('text', { name: 'map' })
map: string;
@Column('timestamp without time zone', { name: 'datestamp' })
datestamp: Date;
@Column('text', { name: 'gametype' })
gametype: string;
@Column('timestamp with time zone', {
name: 'created_at',
default: () => 'now()'
})
createdAt: Date;
@OneToMany(() => GameDetail, (gameDetail) => gameDetail.game)
gameDetails: GameDetail[];
}

View file

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GamesController } from './games.controller';
describe('Games Controller', () => {
let controller: GamesController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [GamesController],
}).compile();
controller = module.get<GamesController>(GamesController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View file

@ -0,0 +1,44 @@
import { Controller, Get, Param, Query } from '@nestjs/common';
import { ApiOperation } from '@nestjs/swagger';
import { GamesService } from './games.service';
import { PaginationQueryDto } from '../common/dto/pagination-query.dto';
@Controller('games')
export class GamesController {
constructor(private readonly gamesService: GamesService) {}
// /games
@Get()
@ApiOperation({ tags: [ 'Game' ], summary: 'Return the latest games' })
findAll(@Query() paginationQuery: PaginationQueryDto) {
const { limit = 10, offset = 0 } = paginationQuery;
return this.gamesService.findAll({ limit, offset });
}
// /games/summary
@Get('summary')
@ApiOperation({ tags: [ 'Game' ], summary: 'Return the latest games with summary' })
findAllWithSummary(@Query() paginationQuery: PaginationQueryDto) {
const { limit = 10, offset = 0 } = paginationQuery;
return this.gamesService.findAllWithSummary({ limit, offset });
}
// /games/gametype/:gametype
@Get('gametype/:gametype')
@ApiOperation({ tags: [ 'Game' ], summary: 'Return the latest games by game type' })
findByType(@Param('gametype') gametype: string, @Query() paginationQuery: PaginationQueryDto) {
const { limit = 10, offset = 0 } = paginationQuery;
return this.gamesService.findByType(gametype, { limit, offset });
}
// /games/gametype/CTFGame/summary
@Get('gametype/:gametype/summary')
@ApiOperation({ tags: [ 'Game' ], summary: 'Return the latest games by game type with game summaries' })
findByTypeWithSummary(@Param('gametype') gametype: string, @Query() paginationQuery: PaginationQueryDto) {
const { limit = 10, offset = 0 } = paginationQuery;
return this.gamesService.findByTypeWithSummary(gametype, { limit, offset });
}
}

View file

@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { GamesController } from './games.controller';
import { GamesService } from './games.service';
import { Games } from './entities/Games';
import { GameDetail } from '../game/entities/GameDetail';
import { GameModule } from '../game/game.module';
@Module({
imports: [ TypeOrmModule.forFeature([ Games, GameDetail ]), ConfigModule, GameModule ],
controllers: [ GamesController ],
providers: [ GamesService ]
})
export class GamesModule {}

View file

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GamesService } from './games.service';
describe('GamesService', () => {
let service: GamesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [GamesService],
}).compile();
service = module.get<GamesService>(GamesService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View file

@ -0,0 +1,114 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Connection, Repository } from 'typeorm';
import { Games } from './entities/Games';
import { GameService } from '../game/game.service';
import { PaginationQueryDto } from '../common/dto/pagination-query.dto';
@Injectable()
export class GamesService {
constructor(
private readonly connection: Connection,
private readonly configService: ConfigService,
private readonly gameService: GameService,
@InjectRepository(Games) private readonly gamesRepository: Repository<Games>
) {}
async findAll(paginationQuery: PaginationQueryDto) {
const { limit, offset } = paginationQuery;
const returnMaxLimit = Math.min(300, Math.max(0, limit));
const games = await this.gamesRepository.find({
skip: offset,
take: returnMaxLimit,
order: {
datestamp: 'DESC'
}
});
const abvSummary = [];
for (const game of games) {
const summary = await this.gameService.findOneAbvSummary(game.gameId);
abvSummary.push(summary);
}
// Only return games when the score is greater than 1
return abvSummary.filter((g) => g.totalScore > 1);
}
async findAllWithSummary(paginationQuery: PaginationQueryDto) {
const { limit, offset } = paginationQuery;
const returnMaxLimit = Math.min(100, Math.max(0, limit));
const games = await this.gamesRepository.find({
skip: offset,
take: returnMaxLimit,
order: {
datestamp: 'DESC'
}
});
const withSummary = [];
for (const game of games) {
const summary = await this.gameService.findOne(game.gameId);
withSummary.push(summary);
}
return withSummary;
}
async findByType(gametype: string, paginationQuery: PaginationQueryDto) {
const { limit, offset } = paginationQuery;
const returnMaxLimit = Math.min(100, Math.max(0, limit));
const games = await this.gamesRepository.find({
where: { gametype: gametype },
skip: offset,
take: returnMaxLimit,
order: {
datestamp: 'DESC'
}
});
if (!games.length) {
throw new NotFoundException(`Game Type: ${gametype} not found`);
}
const abvSummary = [];
for (const game of games) {
const summary = await this.gameService.findOneAbvSummary(game.gameId);
abvSummary.push(summary);
}
// Only return games when the score is greater than 1
return abvSummary.filter((g) => g.totalScore > 1);
}
async findByTypeWithSummary(gametype: string, paginationQuery: PaginationQueryDto) {
const { limit, offset } = paginationQuery;
const returnMaxLimit = Math.min(100, Math.max(0, limit));
const games = await this.gamesRepository.find({
where: { gametype: gametype },
skip: offset,
take: returnMaxLimit,
order: {
datestamp: 'DESC'
}
});
if (!games.length) {
throw new NotFoundException(`Game Type: ${gametype} not found`);
}
const withSummary = [];
for (const game of games) {
const summary = await this.gameService.findOne(game.gameId);
withSummary.push(summary);
}
return withSummary;
}
}

45
app/api/src/main.ts Normal file
View file

@ -0,0 +1,45 @@
import * as fs from 'fs';
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
// Use local gen'd cert in container
const httpsOptions = {
key: fs.readFileSync('/localcert/key.pem', 'utf8'),
cert: fs.readFileSync('/localcert/cert.pem', 'utf8')
};
const app = await NestFactory.create(AppModule, { httpsOptions });
app.enableCors({
credentials: true
});
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
transformOptions: {
enableImplicitConversion: true
}
})
);
const swaggerOptions = new DocumentBuilder()
.setTitle('Tribes 2 Stats API')
.setDescription('Powering stats.playt2.com')
.setVersion('1.0')
.addTag('Game')
.addTag('Player')
.build();
const document = SwaggerModule.createDocument(app, swaggerOptions);
SwaggerModule.setup('docs', app, document);
await app.listen(8443);
}
bootstrap();

View file

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PlayerController } from './player.controller';
describe('PlayerController', () => {
let controller: PlayerController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PlayerController],
}).compile();
controller = module.get<PlayerController>(PlayerController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View file

@ -0,0 +1,16 @@
import { Controller, Get, Param } from '@nestjs/common';
import { ApiOperation } from '@nestjs/swagger';
import { PlayerService } from './player.service';
@Controller('player')
export class PlayerController {
constructor(private readonly playerService: PlayerService) {}
// /player/:playerGuid
@Get(':playerGuid')
@ApiOperation({ tags: [ 'Player' ], summary: 'Return player stats by guid' })
findOne(@Param('playerGuid') playerGuid: string) {
return this.playerService.findOne(playerGuid);
}
}

View file

@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PlayerController } from './player.controller';
import { PlayerService } from './player.service';
import { Players } from '../players/entities/Players';
import { GameDetail } from '../game/entities/GameDetail';
@Module({
imports: [ TypeOrmModule.forFeature([ Players, GameDetail ]), ConfigModule ],
controllers: [ PlayerController ],
providers: [ PlayerService ]
})
export class PlayerModule {}

View file

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PlayerService } from './player.service';
describe('PlayerService', () => {
let service: PlayerService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [PlayerService],
}).compile();
service = module.get<PlayerService>(PlayerService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View file

@ -0,0 +1,92 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Connection, Repository } from 'typeorm';
import { Players } from '../players/entities/Players';
import { GameDetail } from '../game/entities/GameDetail';
import formatPlayerStats from '../common/util/formatStats';
@Injectable()
export class PlayerService {
constructor(
private readonly connection: Connection,
private readonly configService: ConfigService,
@InjectRepository(Players) private readonly playersRepository: Repository<Players>,
@InjectRepository(GameDetail) private readonly gameRepository: Repository<GameDetail>
) {}
async findOne(playerGuid: string) {
const player = await this.playersRepository.findOne({
relations: [ 'gameDetails' ],
where: [ { playerGuid: playerGuid } ]
});
if (!player) {
throw new NotFoundException(`Player GUID: ${playerGuid} not found`);
}
const gameDetails = [];
for (const game in player.gameDetails) {
const g = player.gameDetails[game];
const stats = formatPlayerStats(g);
gameDetails.push({ ...g, stats });
}
/* stat sum */
// Dynamically generate and sum the statTotals object
// This is dirty and should be cleaned up but will do for now :)
const playerStatTotals = {},
statKeys = Object.keys(gameDetails[0].stats);
for (let i = 0; i < statKeys.length; i++) {
if (
statKeys[i] === 'map' ||
statKeys[i] === 'dateStamp' ||
statKeys[i] === 'timeDayMonth' ||
statKeys[i] === 'gameID' ||
statKeys[i] === 'mapID'
) {
continue;
}
playerStatTotals[statKeys[i]] = 0;
}
gameDetails.map((statLine) => {
// look through each object in playerStatsData array
for (const [ key, value ] of Object.entries(statLine.stats)) {
// console.log(`${key}: ${value}`);
// If the stat item exists, add it -- if not create a new key in playerStatTotals
if (playerStatTotals.hasOwnProperty(key) === true) {
playerStatTotals[key] = playerStatTotals[key] + Number(value);
} else {
playerStatTotals[key] = Number(value);
}
}
});
/* end stat sum */
const formattedStats = {
...player,
totalGamesCtfgame: Number(player.totalGamesCtfgame),
totalGamesDmgame: Number(player.totalGamesDmgame),
totalGamesSctfgame: Number(player.totalGamesSctfgame),
totalGamesLakrabbitgame: Number(player.totalGamesLakrabbitgame),
totalGames:
Number(player.totalGamesCtfgame) +
Number(player.totalGamesDmgame) +
Number(player.totalGamesSctfgame) +
Number(player.totalGamesLakrabbitgame),
gameDetails: gameDetails
.filter((g) => g.stats.scoreTG > 0)
.sort((a, b) => Number(b.stats.gameID) - Number(a.stats.gameID)),
statTotals: playerStatTotals
};
return formattedStats;
}
}

View file

@ -0,0 +1,63 @@
import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { GameDetail } from '../../game/entities/GameDetail';
@Index('players_pk', [ 'playerGuid' ], { unique: true })
@Index('players_player_name_key', [ 'playerName' ], { unique: true })
@Index('players_uuid_key', [ 'uuid' ], { unique: true })
@Entity('players', { schema: 'public' })
export class Players {
// @PrimaryGeneratedColumn({ type: 'integer', name: 'id' })
// id: number;
@Column('numeric', { primary: true, name: 'player_guid' })
playerGuid: string;
@Column('text', { name: 'player_name', unique: true })
playerName: string;
@Column('numeric', { name: 'total_games_ctfgame', default: () => '0' })
totalGamesCtfgame: string;
@Column('numeric', { name: 'total_games_dmgame', default: () => '0' })
totalGamesDmgame: string;
@Column('numeric', { name: 'total_games_lakrabbitgame', default: () => '0' })
totalGamesLakrabbitgame: string;
@Column('numeric', { name: 'total_games_sctfgame', default: () => '0' })
totalGamesSctfgame: string;
@Column('numeric', { name: 'stat_overwrite_ctfgame', select: false, default: () => '0' })
statOverwriteCtfgame: string;
@Column('numeric', { name: 'stat_overwrite_dmgame', select: false, default: () => '0' })
statOverwriteDmgame: string;
@Column('numeric', {
name: 'stat_overwrite_lakrabbitgame',
select: false,
default: () => '0'
})
statOverwriteLakrabbitgame: string;
@Column('numeric', { name: 'stat_overwrite_sctfgame', select: false, default: () => '0' })
statOverwriteSctfgame: string;
@Column('text', { name: 'uuid', unique: true })
uuid: string;
@Column('timestamp with time zone', {
name: 'created_at',
default: () => 'now()'
})
createdAt: Date;
@Column('timestamp with time zone', {
name: 'updated_at',
default: () => 'now()'
})
updatedAt: Date;
@OneToMany(() => GameDetail, (gameDetail) => gameDetail.playerGuid)
gameDetails: GameDetail[];
}

View file

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PlayersController } from './players.controller';
describe('PlayersController', () => {
let controller: PlayersController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PlayersController],
}).compile();
controller = module.get<PlayersController>(PlayersController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View file

@ -0,0 +1,92 @@
import { Controller, Get, Query, Param, Inject, CACHE_MANAGER } from '@nestjs/common';
import { Cache } from 'cache-manager';
import { ApiOperation } from '@nestjs/swagger';
import { PlayersService } from './players.service';
import { PaginationQueryDto } from '../common/dto/pagination-query.dto';
import {
TopAccuracyQueryDto,
TopWinsQueryDto,
} from '../common/dto/top-players-query.dto';
import { cache } from 'joi';
@Controller('players')
export class PlayersController {
constructor(
private readonly playerService: PlayersService,
@Inject(CACHE_MANAGER) private cacheManager: Cache
) {}
// /players
@Get()
@ApiOperation({ tags: ['Player'], summary: 'Return a list of players' })
findAll(@Query() paginationQuery: PaginationQueryDto) {
const { limit = 10, offset = 0 } = paginationQuery;
return this.playerService.findAll({ limit, offset });
}
@Get('top/accuracy')
@ApiOperation({
tags: ['Player', 'Leaderboard'],
summary: 'Return a leaderboard of players for a specific accuracy stat',
})
async findTopAccuracy(@Query() topPlayersQuery: TopAccuracyQueryDto) {
const {
stat,
gameType,
minGames = 10,
minShots = 100,
limit = 10,
timePeriod,
} = topPlayersQuery;
const cacheKey =`topacc_${stat}_${gameType}_${minGames}_${minShots}_${limit}_${timePeriod}`;
const queryCache = await this.cacheManager.get(cacheKey);
if(!queryCache){
const results = await this.playerService.findTopAccuracy({
stat,
gameType,
minGames,
minShots,
limit,
timePeriod,
});
await this.cacheManager.set(cacheKey, results, { ttl: 3600 * 2 }); // 2 hours
return results
}
return queryCache
}
@Get('top/wins')
@ApiOperation({
tags: ['Player', 'Leaderboard'],
summary: 'Return a leaderboard of players for win percentage',
})
async findTopWins(@Query() topPlayersQuery: TopWinsQueryDto) {
const { minGames = 100, limit = 10, timePeriod } = topPlayersQuery;
const cacheKey =`topwins_${minGames}_${limit}_${timePeriod}`;
const queryCache = await this.cacheManager.get(cacheKey);
/*
If we don't have a cache ready, lets make one so the next hit is super fast
Cache ttl is in seconds
*/
if(!queryCache){
const results = await this.playerService.findTopWins({
minGames,
limit,
timePeriod,
});
await this.cacheManager.set(cacheKey, results, { ttl: 3600 * 2 }); // 2 hours
return results
}
return queryCache
}
}

View file

@ -0,0 +1,20 @@
import { Module, CacheModule } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PlayersService } from './players.service';
import { PlayersController } from './players.controller';
import { Players } from './entities/Players';
import { GameDetail } from '../game/entities/GameDetail';
@Module({
imports: [
CacheModule.register(),
TypeOrmModule.forFeature([Players, GameDetail]),
ConfigModule
],
providers: [PlayersService],
controllers: [PlayersController],
})
export class PlayersModule {}

View file

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PlayersService } from './players.service';
describe('PlayersService', () => {
let service: PlayersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [PlayersService],
}).compile();
service = module.get<PlayersService>(PlayersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View file

@ -0,0 +1,378 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Connection, Repository } from 'typeorm';
import { Players } from './entities/Players';
import { GameDetail } from '../game/entities/GameDetail';
import { PaginationQueryDto } from '../common/dto/pagination-query.dto';
import {
TopAccuracyQueryDto,
TopWinsQueryDto,
} from '../common/dto/top-players-query.dto';
@Injectable()
export class PlayersService {
constructor(
private readonly connection: Connection,
private readonly configService: ConfigService,
@InjectRepository(Players)
private readonly playersRepository: Repository<Players>,
@InjectRepository(GameDetail)
private readonly gameRepository: Repository<GameDetail>,
) {}
async findAll(paginationQuery: PaginationQueryDto) {
const { limit, offset } = paginationQuery;
const returnMaxLimit = Math.min(500, Math.max(0, limit));
const players = await this.playersRepository.find({
skip: offset,
take: returnMaxLimit,
order: {
updatedAt: 'DESC',
},
});
return players;
}
async findOne(playerGuid: string) {
const player = await this.playersRepository.findOne({
relations: ['gameDetails'],
where: [{ playerGuid: playerGuid }],
});
if (!player) {
throw new NotFoundException(`Player GUID: ${playerGuid} not found`);
}
return player;
}
async findTopAccuracy(topAccuracyQuery: TopAccuracyQueryDto) {
const {
stat: hitsStat,
gameType,
minGames,
minShots,
limit,
timePeriod = null,
} = topAccuracyQuery;
const shotsStat = {
discDmgHitsTG: 'discShotsFiredTG',
discHitsTG: 'discShotsFiredTG',
discMATG: 'discShotsFiredTG',
laserHitsTG: 'laserShotsFiredTG',
laserMATG: 'laserShotsFiredTG',
cgHitsTG: 'cgShotsFiredTG',
shockHitsTG: 'shockShotsFiredTG',
grenadeDmgHitsTG: 'grenadeShotsFiredTG',
grenadeHitsTG: 'grenadeShotsFiredTG',
grenadeMATG: 'grenadeShotsFiredTG',
}[hitsStat];
// Possibly make this a query param at some point?
const excludeDiscJumps = shotsStat === 'discShotsFiredTG';
const hitsValue = '(game.stats->:hitsStat->>0)::integer';
const shotsValue = '(game.stats->:shotsStat->>0)::integer';
const discJumpsValue =
"COALESCE((game.stats->'discJumpTG'->>0)::integer, 0)";
const killerDiscJumpsValue =
"COALESCE((game.stats->'killerDiscJumpTG'->>0)::integer, 0)";
// pgSQL doesn't let you reference aliased selections you've made in the
// same select statement, so unfortunately these computed JSON values are
// repeated in a couple places. Using template strings here to easily repeat
// the same expression instead of having multiple nested subqueries. Rest
// assured, no user-supplied values are ever interpolated, those are all
// parameterized instead.
const aggregatedHits = `SUM(${hitsValue})::integer`;
// Add sums of `discJumpTG` and `killerDiscJumpTG`.
const aggregatedDiscJumps = `(SUM(${discJumpsValue})::integer + SUM(${killerDiscJumpsValue})::integer)`;
let aggregatedShots = `SUM(${shotsValue})::integer`;
if (excludeDiscJumps) {
// Since subtracting disc jumps could theoretically drop the shots count
// to 0, clamp it to at least the number of hits or 1, otherwise it'd be
// possible to divide by zero.
aggregatedShots = `GREATEST(1, ${aggregatedHits}, ${aggregatedShots} - ${aggregatedDiscJumps})`;
}
// Cast to float to avoid integer division truncating the result.
const aggregatedAccuracy = `(${aggregatedHits}::float / ${aggregatedShots}::float)`;
const sinceDate = '(now() - (:timePeriod)::interval)';
// TODO: This whole query could probably be turned into a `ViewEntity` at
// some point, but I couldn't get that to work.
let playersQuery = this.playersRepository
.createQueryBuilder('player')
.setParameters({
hitsStat,
shotsStat,
minGames,
minShots,
timePeriod,
})
.select([
'player.player_guid',
'player.player_name',
'stats.game_count',
'stats.hits',
'stats.shots',
'stats.accuracy',
])
.addSelect(
timePeriod ? sinceDate : 'NULL',
'since_date'
)
.innerJoin(
(subQuery) => {
let statsQuery = subQuery
.select(['game.player_guid'])
.from(GameDetail, 'game')
.addSelect('COUNT(game.id)::integer', 'game_count')
.addSelect(aggregatedHits, 'hits')
.addSelect(aggregatedShots, 'shots')
.addSelect(aggregatedAccuracy, 'accuracy')
.where(`${shotsValue} > 0`)
.andWhere(timePeriod ? `game.datestamp >= ${sinceDate}` : 'TRUE')
.groupBy('game.player_guid');
if (excludeDiscJumps) {
statsQuery = statsQuery.addSelect(
aggregatedDiscJumps,
'disc_jumps',
);
}
if (gameType) {
statsQuery = statsQuery.andWhere('game.gametype = :gameType', {
gameType,
});
}
return statsQuery;
},
'stats',
'stats.player_guid = player.player_guid',
)
.where('stats.game_count >= :minGames')
.andWhere('stats.shots >= :minShots')
.orderBy('stats.accuracy', 'DESC')
.limit(limit);
if (excludeDiscJumps) {
playersQuery = playersQuery.addSelect('stats.disc_jumps');
}
// Uncomment to debug:
// console.log(query.getQueryAndParameters());
// typeorm doesn't let you select computed columns since they're not part
// of the entity definition. There are workarounds, but I'm not a fan of any
// and would rather use `getRawMany()` for now, which does include them.
// See: https://github.com/typeorm/typeorm/issues/296
const rows = await playersQuery.getRawMany();
// `getRawMany` was used, so manually snake_case -> camelCase.
const players = rows.map((row) => ({
playerGuid: row.player_guid,
playerName: row.player_name,
gameCount: row.game_count,
hits: row.hits,
shots: row.shots,
discJumps: row.disc_jumps,
accuracy: row.accuracy,
}));
return {
// Even though some of these parameters might have been supplied as input,
// it's still useful to know what values were actually used, in case
// defaults were used instead, values were clamped to min/max, etc.
hitsStat,
shotsStat,
excludeDiscJumps,
minGames,
minShots,
limit,
timePeriod,
sinceDate: rows.length ? rows[0].since_date : null,
players,
};
}
async findTopWins(topWinsQuery: TopWinsQueryDto) {
const { minGames, limit, timePeriod = null } = topWinsQuery;
const sinceDate = '(now() - (:timePeriod)::interval)';
const query = this.playersRepository
.createQueryBuilder('player')
.setParameters({ minGames, timePeriod })
.select(['stats.player_name', 'stats.player_guid'])
.addSelect('COUNT(stats.game_id)::integer', 'game_count')
.addSelect(
"COUNT(stats.player_match_result = 'win' OR NULL)::integer",
'win_count',
)
.addSelect(
"COUNT(stats.player_match_result = 'loss' OR NULL)::integer",
'loss_count',
)
.addSelect(
"COUNT(stats.player_match_result = 'draw' OR NULL)::integer",
'draw_count',
)
.addSelect(
"(COUNT(stats.player_match_result = 'win' OR NULL)::float + COUNT(stats.player_match_result = 'draw' OR NULL)::float / 2.0) / COUNT(stats.game_id)::float",
'win_percent',
)
.addSelect(
timePeriod ? sinceDate : 'NULL',
'since_date'
)
.innerJoin(
(qb) => {
return (
qb
.select([
'game.player_name',
'game.player_guid',
'game.map',
'game.datestamp',
'join_g.*',
])
// Determine whether they spent at least 67% of the total match time on
// one team, and then determine whether that means they won or lost.
// Note that this team may be different from `dtTeamGame`.
.addSelect(
`CASE
WHEN
((game.stats->'timeOnTeamOneTG'->>0)::float / (game.stats->'matchRunTimeTG'->>0)::float) >= 0.67
THEN CASE
WHEN
join_g.score_storm > join_g.score_inferno
THEN 'win'
WHEN
join_g.score_storm < join_g.score_inferno
THEN 'loss'
WHEN
join_g.score_storm = join_g.score_inferno
THEN 'draw'
END
WHEN
((game.stats->'timeOnTeamTwoTG'->>0)::float / (game.stats->'matchRunTimeTG'->>0)::float) >= 0.67
THEN CASE
WHEN
join_g.score_inferno > join_g.score_storm
THEN 'win'
WHEN
join_g.score_inferno < join_g.score_storm
THEN 'loss'
WHEN
join_g.score_inferno = join_g.score_storm
THEN 'draw'
END
ELSE 'none'
END`.replace(/\s+/g, ' '),
'player_match_result',
)
.from(GameDetail, 'game')
.innerJoin(
(qb) => {
return (
qb
.select(['g.game_id'])
.addSelect(
"COUNT(CASE WHEN (g.stats->'dtTeamGame'->>0)::integer = 1 AND (g.stats->'timeOnTeamOneTG'->>0)::float > 0 THEN 1 ELSE NULL END)::integer",
'team_size_storm',
)
.addSelect(
"COUNT(CASE WHEN (g.stats->'dtTeamGame'->>0)::integer = 2 AND (g.stats->'timeOnTeamTwoTG'->>0)::float > 0 THEN 1 ELSE NULL END)::integer",
'team_size_inferno',
)
// `teamScoreGame` can get screwed up: players on one team
// can be assigned the score from the other team (most
// likely due to team switching). So to determine each
// team's score, we take the most common `teamScoreGame`
// from all players on that team who don't have a `timeOnTeam`
// value of 0.
.addSelect(
"(mode() WITHIN GROUP (ORDER BY CASE WHEN ((g.stats->'dtTeamGame'->>0)::integer = 1 AND (g.stats->'timeOnTeamOneTG'->>0)::float > 0) THEN (g.stats->'teamScoreGame'->>0)::integer ELSE NULL END)) / 100",
'score_storm',
)
.addSelect(
"(mode() WITHIN GROUP (ORDER BY CASE WHEN ((g.stats->'dtTeamGame'->>0)::integer = 2 AND (g.stats->'timeOnTeamTwoTG'->>0)::float > 0) THEN (g.stats->'teamScoreGame'->>0)::integer ELSE NULL END)) / 100",
'score_inferno',
)
.from(GameDetail, 'g')
.groupBy('g.game_id')
);
},
'join_g',
'join_g.game_id = game.game_id',
)
.where("(game.gametype = 'CTFGame' OR game.gametype = 'SCtFGame')")
// Only count if the player's `gamePCT` was at least 67%. This is
// effectively how much of the match they were present for.
.andWhere("(game.stats->'gamePCT'->>0)::float >= 67")
// As an extra precaution against prematurely ended matches, only count
// games that lasted at least 10 minutes.
.andWhere("(game.stats->'matchRunTimeTG'->>0)::float >= 10")
// Each team must have at least 2 players.
.andWhere('join_g.team_size_storm >= 2')
.andWhere('join_g.team_size_inferno >= 2')
// Must fall within the specified time period.
.andWhere(timePeriod ? `game.datestamp >= ${sinceDate}` : 'TRUE')
);
},
'stats',
'stats.player_guid = player.player_guid',
)
.where("stats.player_match_result != 'none'")
.having('COUNT(stats.game_id)::integer >= :minGames')
.groupBy('stats.player_guid')
.addGroupBy('stats.player_name')
.orderBy(
"(COUNT(stats.player_match_result = 'win' OR NULL)::float + COUNT(stats.player_match_result = 'draw' OR NULL)::float / 2.0) / COUNT(stats.game_id)::float",
'DESC',
)
.limit(limit);
// Uncomment to debug:
// console.log(query.getQueryAndParameters());
// typeorm doesn't let you select computed columns since they're not part
// of the entity definition. There are workarounds, but I'm not a fan of any
// and would rather use `getRawMany()` for now, which does include them.
// See: https://github.com/typeorm/typeorm/issues/296
const rows = await query.getRawMany();
// `getRawMany` was used, so manually snake_case -> camelCase.
const players = rows.map((row) => ({
playerGuid: row.player_guid,
playerName: row.player_name,
gameCount: row.game_count,
winCount: row.win_count,
lossCount: row.loss_count,
drawCount: row.draw_count,
winPercent: row.win_percent,
}));
return {
// Even though some of these parameters might have been supplied as input,
// it's still useful to know what values were actually used, in case
// defaults were used instead, values were clamped to min/max, etc.
minGames,
gameType: ['CTFGame', 'SCtFGame'],
limit,
timePeriod,
sinceDate: rows.length ? rows[0].since_date : null,
players,
};
}
}

View file

@ -0,0 +1,21 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [ AppModule ]
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer()).get('/').expect(200).expect('Healthy!');
});
});

View file

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View file

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"exclude": [
"node_modules",
"test",
"dist",
"**/*spec.ts"
]
}

15
app/api/tsconfig.json Normal file
View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true
}
}

View file

@ -6,6 +6,7 @@ import (
"log" "log"
"os" "os"
"os/exec" "os/exec"
"strings"
) )
//ShellToUse is... //ShellToUse is...
@ -24,12 +25,22 @@ func Shellout(command string) (error, string, string) {
func initFTP() { func initFTP() {
ftpHOST := os.Getenv("FTP_HOST") ftpHOST := os.Getenv("FTP_HOST")
ftpPath := os.Getenv("FTP_PATH")
ftpUSER := os.Getenv("FTP_USER") ftpUSER := os.Getenv("FTP_USER")
ftpPW := os.Getenv("FTP_PW") ftpPW := strings.Replace(os.Getenv("FTP_PW"), `\\`, `\`, -1)
fmt.Println("Downloading stat files from", ftpHOST + ftpPath)
// wget is a bit buggy with FTP in v1.20.x
// cmd := "wget --recursive --no-passive-ftp -nH --cut-dirs=4 --ftp-user=" + ftpUSER + " --no-parent --ftp-password="+ ftpPW +" -P /app/serverStats/stats/ ftp://" + ftpHOST + ftpPath
cmd := "lftp -c 'open "+ftpHOST+"; user "+ftpUSER+" "+ftpPW+"; mirror -e "+ftpPath+" /app/serverStats/stats/; quit'"
err, out, errout := Shellout(cmd)
fmt.Println("Downloading stat files from", ftpHOST)
err, out, errout := Shellout("wget --recursive -nH --cut-dirs=4 --user=" + ftpUSER + " --no-parent --password=" + ftpPW + " -P /app/serverStats/stats/ ftp://" + ftpHOST + "/" + ftpHOST + "_port_28000/classic/serverStats/stats/")
if err != nil { if err != nil {
log.Printf("error: %v\n", err) log.Printf("error: %v\n", err)
} }
@ -39,3 +50,5 @@ func initFTP() {
fmt.Println(errout) fmt.Println(errout)
} }

View file

@ -10,14 +10,15 @@ package main
import ( import (
"fmt" "fmt"
"time"
) )
func main() { func main() {
fmt.Println("Starting FTP stat file download") // fmt.Println("Starting FTP stat file download")
initFTP() initFTP()
fmt.Println("Stat files downloaded!") // fmt.Println("Stat files downloaded!")
fmt.Println("Starting stat parser") fmt.Println("Starting stat parser")
initParser() initParser()
fmt.Println("All done!") fmt.Println("All done!", time.Now().Format(time.RFC850))
} }

View file

@ -5,5 +5,6 @@ echo "T2 Stat Parser Running...";
# Run this initially, then we'll execute this on a schedule # Run this initially, then we'll execute this on a schedule
./start.sh ./start.sh
# Keep container running echo "T2 Stat Parser Done!";
tail -f /dev/null # Keep container running with the cron schedule
#crond -l 2 -f

View file

@ -32,8 +32,8 @@ import (
var ( var (
connectionString = flag.String("conn", getenvWithDefault("DATABASE_URL", ""), "PostgreSQL connection string") connectionString = flag.String("conn", getenvWithDefault("DATABASE_URL", ""), "PostgreSQL connection string")
db *sqlx.DB db *sqlx.DB
maxStatOverwrite int = 100 maxStatOverwrite int = 98 // there are 99 entries, remember arr counts 0
debugLevel int = 1 // 0 off,min | 1 Basic output checks | 2 Output all the things debugLevel int = 1 // 0 off,min | 1 Basic output checks | 2 Output all the things
) )
func getenvWithDefault(name, defaultValue string) string { func getenvWithDefault(name, defaultValue string) string {
@ -59,11 +59,12 @@ type Game struct {
dbStatOverWrite int `db.players:"stat_overwrite"` dbStatOverWrite int `db.players:"stat_overwrite"`
statOverWrite int statOverWrite int
gameMap string `db.games:"map"` gameMap string `db.game_detail:"map"`
gameType string `db.games:"gametype"` gameID int `db.game_detail:"game_id"`
dateStamp string `db.games:"datestamp"` gameType string `db.game_detail:"gametype"`
stats string `db.games:"stats"` dateStamp string `db.game_detail:"datestamp"`
uuid string `db.games:"uuid"` stats string `db.game_detail:"stats"`
uuid string `db.game_detail:"uuid"`
} }
func initParser() { func initParser() {
@ -163,6 +164,9 @@ func parseGameTypeStats(gt string) {
break break
} }
// Base the maxGamesLength off how many elements are present in the map length since this should always be there
statArrayMaxLength := len(mStatLine["map"])
checkPlayer(g) checkPlayer(g)
g.dbStatOverWrite = getDBStatOverWrite(g.playerGUID, strings.ToLower(gt)) g.dbStatOverWrite = getDBStatOverWrite(g.playerGUID, strings.ToLower(gt))
@ -175,32 +179,39 @@ func parseGameTypeStats(gt string) {
fmt.Println("Stat Overwrite", g.statOverWrite) fmt.Println("Stat Overwrite", g.statOverWrite)
fmt.Println("maxStatOverwrite", maxStatOverwrite) fmt.Println("maxStatOverwrite", maxStatOverwrite)
fmt.Println("statArrayMaxLength", statArrayMaxLength)
fmt.Println("g.dbStatOverWrite", g.dbStatOverWrite) fmt.Println("g.dbStatOverWrite", g.dbStatOverWrite)
} }
statCron := 0 // statEntryDiff := 0
if g.statOverWrite < g.dbStatOverWrite { // if g.statOverWrite < g.dbStatOverWrite {
// 100 - // // 100 -
statCron = (maxStatOverwrite - g.statOverWrite) + g.dbStatOverWrite // statEntryDiff = (maxStatOverwrite - g.statOverWrite) + g.dbStatOverWrite
} else { // } else {
statCron = g.statOverWrite - g.dbStatOverWrite // statEntryDiff = g.statOverWrite - g.dbStatOverWrite
} // }
// Reset statCron if it flows over maxStatOverwrite // // Reset statEntryDiff if it flows over maxStatOverwrite
if statCron > maxStatOverwrite { // if statEntryDiff > maxStatOverwrite {
statCron = 0 // statEntryDiff = 0
} // }
if debugLevel >= 1 { // if debugLevel >= 1 {
fmt.Println("statCron", statCron) // fmt.Println("statEntryDiff", statEntryDiff)
} // }
for i := 0; i <= statCron; i++ { for i := 0; i < statArrayMaxLength; i++ {
// arrPosition := i - 1 fmt.Println("index", i)
// fmt.Println(arrPosition)
parseStatOverWriteLine(g, mStatLine, i, strings.ToLower(gt)) parseStatOverWriteLine(g, mStatLine, i, strings.ToLower(gt))
//g.dbStatOverWrite = getDBStatOverWrite(g.playerGUID, strings.ToLower(gt))
} }
// account for new players and new statlines
// if g.statOverWrite == 0 && g.dbStatOverWrite == 0 {
// parseStatOverWriteLine(g, mStatLine, 0, strings.ToLower(gt))
// }
fmt.Println("---") fmt.Println("---")
} }
@ -219,8 +230,13 @@ func parseStatOverWriteLine(g Game, mStatLine map[string][]string, arrPosition i
cleanStatLine := make(map[string][]string) cleanStatLine := make(map[string][]string)
for index, element := range mStatLine { for index, element := range mStatLine {
if len(element) > 1 { //fmt.Println("index", index, " - arrPosition", arrPosition, " - element Length", len(element))
if len(element) > 1 && arrPosition < len(element) {
//fmt.Println("index", index, " - arrPosition", arrPosition, " - element Length", len(element))
cleanStatLine[index] = append(cleanStatLine[index], element[arrPosition]) cleanStatLine[index] = append(cleanStatLine[index], element[arrPosition])
} else {
//fmt.Println(index, "prefilling this index since there isnt any data in this position...")
cleanStatLine[index] = append(cleanStatLine[index], "0")
} }
} }
@ -233,13 +249,31 @@ func parseStatOverWriteLine(g Game, mStatLine map[string][]string, arrPosition i
g.uuid = genXid() g.uuid = genXid()
g.gameMap = cleanStatLine["map"][0] g.gameMap = cleanStatLine["map"][0]
if value, ok := mStatLine["gameID"]; ok {
fmt.Println("gameID", value[arrPosition])
g.gameID, err = strconv.Atoi(value[arrPosition])
if err != nil {
fmt.Println("Couldn't convert gameID to an int", err)
}
} else {
fmt.Println("No gameID found!")
g.gameID = 0
}
if debugLevel >= 2 { if debugLevel >= 2 {
// log the game struct // log the game struct
fmt.Println(g) fmt.Println(g)
} }
// insert game stat // Check if we need to create a new game record
addPlayerGameStat(g, strings.ToLower(gt)) checkGameRecord(g)
if checkGameEntryForPlayer(g) == false && g.gameID != 0 {
fmt.Println("does not exist, add")
// insert game stat
addPlayerGameStat(g, strings.ToLower(gt))
}
} }
func rowExists(query string, args ...interface{}) bool { func rowExists(query string, args ...interface{}) bool {
@ -252,6 +286,28 @@ func rowExists(query string, args ...interface{}) bool {
return exists return exists
} }
func checkGameRecord(g Game) {
check := rowExists("select game_id from games where game_id = $1 and map = $2", g.gameID, g.gameMap)
if !check {
createGame(g)
} else {
fmt.Println("Game Record ", g.gameID, g.gameMap, " already exists")
}
}
func createGame(g Game) {
fmt.Println("Creating new Game ", g.gameMap, g.gameID, g.dateStamp, g.gameType)
if g.gameID != 0 {
sqlInsert := `insert into games(map, game_id, datestamp, gametype) values($1,$2,$3,$4)`
_, err := db.Exec(sqlInsert, g.gameMap, g.gameID, g.dateStamp, g.gameType)
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to create new game record (Possible Dupe ID): %v\n", err)
// Don't exit - just skip this insert
// os.Exit(1)
}
}
}
func checkPlayer(g Game) { func checkPlayer(g Game) {
check := rowExists("select player_guid from players where player_guid = $1", g.playerGUID) check := rowExists("select player_guid from players where player_guid = $1", g.playerGUID)
if !check { if !check {
@ -261,6 +317,16 @@ func checkPlayer(g Game) {
} }
} }
func checkGameEntryForPlayer(g Game) bool {
check := rowExists("select player_guid from game_detail where player_guid = $1 and game_id = $2 and map = $3", g.playerGUID, g.gameID, g.gameMap)
if !check {
return false
} else {
fmt.Println("Game Entry ", g.gameID, g.gameMap, " already exists for ", g.playerName)
return true
}
}
func createPlayer(uuid string, g Game) { func createPlayer(uuid string, g Game) {
fmt.Println("Creating new player", g.playerName) fmt.Println("Creating new player", g.playerName)
_, err := db.Exec("insert into players(uuid, player_guid, player_name) values($1,$2,$3)", uuid, g.playerGUID, g.playerName) _, err := db.Exec("insert into players(uuid, player_guid, player_name) values($1,$2,$3)", uuid, g.playerGUID, g.playerName)
@ -281,17 +347,27 @@ func getDBStatOverWrite(playerGUID int, gt string) int {
return dbStatOverWrite return dbStatOverWrite
} }
func resetDBStatOverWrite(playerGUID int, gt string) {
sqlUpdate := `UPDATE players SET stat_overwrite_` + gt + `= 1 WHERE player_guid = $1;`
_, err := db.Exec(sqlUpdate, playerGUID)
if err != nil {
panic(err)
}
fmt.Println("Reset player", playerGUID, "for GameType", gt)
}
func addPlayerGameStat(g Game, gt string) { func addPlayerGameStat(g Game, gt string) {
if debugLevel == 1 { if debugLevel == 1 {
fmt.Println("g.dbStatOverWrite", g.dbStatOverWrite, "g.statOverWrite", g.statOverWrite) fmt.Println("g.dbStatOverWrite", g.dbStatOverWrite, "g.statOverWrite", g.statOverWrite)
fmt.Println("Checking line: ", g.playerGUID, g.playerName, g.statOverWrite, g.gameMap, g.gameID, g.dateStamp, g.gameType)
} }
if g.dbStatOverWrite != g.statOverWrite && g.dateStamp != "0" { if g.dateStamp != "0" {
// Insert new stat line // Insert new stat line
fmt.Println("New stat line!", g.playerName, g.dateStamp) fmt.Println("New stat line!", g.playerName, g.dateStamp)
sqlInsert := `insert into games(player_guid, player_name, stat_overwrite, map, stats, datestamp, uuid, gametype) values($1,$2,$3,$4,$5,$6,$7,$8)` sqlInsert := `insert into game_detail(player_guid, player_name, stat_overwrite, map, game_id, stats, datestamp, uuid, gametype) values($1,$2,$3,$4,$5,$6,$7,$8,$9)`
_, err := db.Exec(sqlInsert, g.playerGUID, g.playerName, g.statOverWrite, g.gameMap, g.stats, g.dateStamp, g.uuid, g.gameType) _, err := db.Exec(sqlInsert, g.playerGUID, g.playerName, g.statOverWrite, g.gameMap, g.gameID, g.stats, g.dateStamp, g.uuid, g.gameType)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Unable to add player's game stat: %v\n", err) fmt.Fprintf(os.Stderr, "Unable to add player's game stat: %v\n", err)
os.Exit(1) os.Exit(1)
@ -304,6 +380,9 @@ func addPlayerGameStat(g Game, gt string) {
panic(err) panic(err)
} }
} else { } else {
fmt.Println("This stat line already exists") fmt.Println("The dateStamp looks malformed")
} }
// else {
// fmt.Println("This stat line already exists", g.playerGUID, g.playerName, g.statOverWrite, g.gameMap, g.gameID, g.dateStamp, g.gameType)
// }
} }

View file

@ -0,0 +1,7 @@
./lData
./mlData
./stats/CTFGame
./stats/DMGame
./stats/LakRabbitGame
./stats/SCTF/Game

View file

@ -7,5 +7,6 @@ then
/app/main /app/main
else else
echo "No build found, running from source" echo "No build found, running from source"
cd /app
go run *.go go run *.go
fi fi

2
build/api/.dockerignore Normal file
View file

@ -0,0 +1,2 @@
.git
node_modules

71
build/api/Dockerfile Normal file
View file

@ -0,0 +1,71 @@
# [ Stage 1 ] - install node modules
FROM node:12.18-alpine as builder
WORKDIR /build/app
RUN npm i -g @nestjs/cli@7.4.1
COPY ./app/api .
RUN npm install
ENV NODE_ENV=production
RUN nest build
# ============================
# ============================
# [ Stage 2 ]
FROM node:12.18-alpine
LABEL maintainer="Anthony Mineo <anthonymineo@gmail.com>"
RUN apk update && apk add --no-cache openssl bash curl
# Default envs as prod
ENV HOST=0.0.0.0 \
PORT=8443 \
NODE_ENV=production \
APP_NAME=NestJS
ENV APP_URL=https://${HOST}:${PORT}
# Setup pm2 as our node process manager
# https://pm2.keymetrics.io/docs/usage/docker-pm2-nodejs/
RUN npm i -g pm2 @nestjs/cli@7.4.1
RUN mkdir /opt/node_app && chown node:node /opt/node_app && mkdir /localcert && chown node:node /localcert
RUN mkdir -p /opt/node_app/app/node_modules/ && chown node:node /opt/node_app/app/node_modules/
WORKDIR /opt/node_app
#USER node
# Generate a localhost cert
RUN openssl req -newkey rsa:2048 -new -nodes -x509 -days 360 -keyout /localcert/key.pem -out /localcert/cert.pem -subj "/C=US/ST=New Jersey/L=Warren/O=localhost/OU=IT/CN=127.0.0.1"
ENV PATH /opt/node_app/node_modules/.bin:$PATH
# Our App
WORKDIR /opt/node_app/app
# Set node modules outside our app to keep it clean
COPY --from=builder /build/app/node_modules/ /opt/node_app/node_modules/
COPY --from=builder /build/app/dist/ /opt/node_app/app/dist/
COPY ./app/api/package.json ./app/api/package-lock.json* ./
# Start script and config
COPY ./build/api/ecosystem._PROD_.config.js /opt/node_app/ecosystem._PROD_.config.js
COPY ./build/api/entrypoint.sh /entrypoint.sh
EXPOSE 8080 8443
HEALTHCHECK --interval=20s --timeout=30s --start-period=5s --retries=5 \
CMD curl -f -k https://localhost:8443/ || exit 1
ENTRYPOINT [ "/entrypoint.sh" ]

View file

@ -0,0 +1,14 @@
module.exports = {
apps: [
{
name: process.env.APP_NAME,
cwd: '/opt/node_app/app/',
script: 'npm run start:dev --interpreter bash',
// Options reference: https://pm2.keymetrics.io/docs/usage/application-declaration/
instances: 1,
autorestart: true,
watch: false,
ignore_watch: [ 'node_modules', 'dist' ]
}
]
};

View file

@ -0,0 +1,10 @@
module.exports = {
apps: [
{
cwd: '/opt/node_app/app/',
script: 'node dist/main --interpreter bash',
autorestart: true,
instances: 1
}
]
};

41
build/api/entrypoint.sh Executable file
View file

@ -0,0 +1,41 @@
#!/bin/bash
echo "Node ENV: $NODE_ENV"
file_env() {
local var="$1"
local fileVar="${var}_FILE"
local def="${2:-}"
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
exit 1
fi
local val="$def"
if [ "${!var:-}" ]; then
val="${!var}"
elif [ "${!fileVar:-}" ]; then
val="$(< "${!fileVar}")"
fi
export "$var"="$val"
unset "$fileVar"
}
# file_env 'APP_KEY'
# # Exit if APP_KEY is missing, this key is important!
# if [ -z "$APP_KEY" ]
# then
# echo >&2 "[ERROR] No ENV for APP_KEY or APP_KEY_FILE found!"
# echo >&2 "Run the ./generate-adonis-app-key.sh script or provide one at runtime"
# exit 1
# fi
if [[ "$NODE_ENV" == "production" ]]; then
pm2-runtime start /opt/node_app/ecosystem._PROD_.config.js
else
npm install && \
pm2-runtime start /opt/node_app/ecosystem._DEV_.config.js
fi

View file

@ -24,13 +24,13 @@ RUN go build -o main .
FROM golang:1.14-alpine FROM golang:1.14-alpine
LABEL maintainer="Anthony Mineo <anthonymineo@gmail.com>" LABEL maintainer="Anthony Mineo <anthonymineo@gmail.com>"
RUN apk update && apk add --no-cache wget tzdata RUN apk update && apk add --no-cache wget tzdata lftp
#Set TimeZone #Set TimeZone
ENV TZ=America/New_York ENV TZ=America/New_York
#Set cron schedule (every day at 8am est) # #Set cron schedule (every day at 11:30am est)
RUN crontab -l -u root | echo "0 8 * * * sh -c \"/app/start.sh\"" | crontab -u root - # RUN echo '30 11 * * * /app/start.sh' > /etc/crontabs/root
WORKDIR /app WORKDIR /app
@ -38,4 +38,5 @@ COPY --from=builder /app/main /app/main
COPY ./app/t2-stat-parser/start.sh /app/start.sh COPY ./app/t2-stat-parser/start.sh /app/start.sh
COPY ./app/t2-stat-parser/parser-daemon.sh /app/parser-daemon.sh COPY ./app/t2-stat-parser/parser-daemon.sh /app/parser-daemon.sh
ENTRYPOINT ./parser-daemon.sh #ENTRYPOINT ./parser-daemon.sh
ENTRYPOINT ./start.sh

View file

@ -31,26 +31,54 @@ CREATE TABLE "public"."players" (
); );
DROP TABLE IF EXISTS "public"."games"; DROP TABLE IF EXISTS "public"."games";
CREATE SEQUENCE IF NOT EXISTS games_id_seq;
-- Table Definition -- Table Definition
CREATE TABLE "public"."games" ( CREATE TABLE "public"."games" (
"game_id" numeric NOT NULL UNIQUE,
"map" text NOT NULL,
"datestamp" timestamp NOT NULL,
"gametype" text NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT game_pk PRIMARY KEY (game_id)
);
DROP TABLE IF EXISTS "public"."game_detail";
CREATE SEQUENCE IF NOT EXISTS games_id_seq;
-- Table Definition
CREATE TABLE "public"."game_detail" (
"id" int4 NOT NULL DEFAULT nextval('games_id_seq'::regclass), "id" int4 NOT NULL DEFAULT nextval('games_id_seq'::regclass),
"player_guid" numeric NOT NULL, "player_guid" numeric NOT NULL,
"player_name" text NOT NULL, "player_name" text NOT NULL,
"stat_overwrite" numeric NOT NULL, "stat_overwrite" numeric NOT NULL,
"map" text NOT NULL, "map" text NOT NULL,
"game_id" numeric NOT NULL,
"stats" jsonb NOT NULL, "stats" jsonb NOT NULL,
"datestamp" timestamp NOT NULL, "datestamp" timestamp NOT NULL,
"uuid" text NOT NULL UNIQUE, "uuid" text NOT NULL UNIQUE,
"gametype" text NOT NULL, "gametype" text NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT games_pk PRIMARY KEY (id), CONSTRAINT games_pk PRIMARY KEY (id),
FOREIGN KEY (game_id) REFERENCES games (game_id),
FOREIGN KEY (player_guid) REFERENCES players (player_guid) FOREIGN KEY (player_guid) REFERENCES players (player_guid)
); );
CREATE OR REPLACE FUNCTION trigger_set_timestamp() CREATE OR REPLACE FUNCTION trigger_set_timestamp()
RETURNS TRIGGER AS $$ RETURNS TRIGGER AS $$
BEGIN BEGIN

View file

@ -3,7 +3,7 @@
echo "Setting up database..." echo "Setting up database..."
BACKUP_FILE_PATH=/docker-entrypoint-initdb.d/backup/t2_stats.sql BACKUP_FILE_PATH=/docker-entrypoint-initdb.d/backup/t2_stats.sql
psql -d t2_stats -U dev -p 5432 -a -q -f $BACKUP_FILE_PATH psql -d t2_stats -U $POSTGRES_USER -p 5432 -a -q -f $BACKUP_FILE_PATH
# -h PostgreSQL server IP address # -h PostgreSQL server IP address
# -d database name # -d database name
# -U user name # -U user name

View file

@ -0,0 +1,693 @@
# -----------------------------
# PostgreSQL configuration file
# -----------------------------
#
# This file consists of lines of the form:
#
# name = value
#
# (The "=" is optional.) Whitespace may be used. Comments are introduced with
# "#" anywhere on a line. The complete list of parameter names and allowed
# values can be found in the PostgreSQL documentation.
#
# The commented-out settings shown in this file represent the default values.
# Re-commenting a setting is NOT sufficient to revert it to the default value;
# you need to reload the server.
#
# This file is read on server startup and when the server receives a SIGHUP
# signal. If you edit the file on a running system, you have to SIGHUP the
# server for the changes to take effect, run "pg_ctl reload", or execute
# "SELECT pg_reload_conf()". Some parameters, which are marked below,
# require a server shutdown and restart to take effect.
#
# Any parameter can also be given as a command-line option to the server, e.g.,
# "postgres -c log_connections=on". Some parameters can be changed at run time
# with the "SET" SQL command.
#
# Memory units: kB = kilobytes Time units: ms = milliseconds
# MB = megabytes s = seconds
# GB = gigabytes min = minutes
# TB = terabytes h = hours
# d = days
#------------------------------------------------------------------------------
# FILE LOCATIONS
#------------------------------------------------------------------------------
# The default values of these variables are driven from the -D command-line
# option or PGDATA environment variable, represented here as ConfigDir.
#data_directory = 'ConfigDir' # use data in another directory
# (change requires restart)
#hba_file = 'ConfigDir/pg_hba.conf' # host-based authentication file
# (change requires restart)
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
# (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
#external_pid_file = '' # write an extra PID file
# (change requires restart)
#------------------------------------------------------------------------------
# CONNECTIONS AND AUTHENTICATION
#------------------------------------------------------------------------------
# - Connection Settings -
listen_addresses = '*'
# comma-separated list of addresses;
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432 # (change requires restart)
max_connections = 200 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
#unix_socket_directories = '/var/run/postgresql' # comma-separated list of directories
# (change requires restart)
#unix_socket_group = '' # (change requires restart)
#unix_socket_permissions = 0777 # begin with 0 to use octal notation
# (change requires restart)
#bonjour = off # advertise server via Bonjour
# (change requires restart)
#bonjour_name = '' # defaults to the computer name
# (change requires restart)
# - TCP Keepalives -
# see "man 7 tcp" for details
tcp_keepalives_idle = 180 # TCP_KEEPIDLE, in seconds;
# 0 selects the system default
tcp_keepalives_interval = 10 # TCP_KEEPINTVL, in seconds;
# 0 selects the system default
tcp_keepalives_count = 6 # TCP_KEEPCNT;
# 0 selects the system default
# - Authentication -
#authentication_timeout = 1min # 1s-600s
#password_encryption = md5 # md5 or scram-sha-256
#db_user_namespace = off
# GSSAPI using Kerberos
#krb_server_keyfile = ''
#krb_caseins_users = off
# - SSL -
#ssl = off
#ssl_ca_file = ''
#ssl_cert_file = 'server.crt'
#ssl_crl_file = ''
#ssl_key_file = 'server.key'
#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers
#ssl_prefer_server_ciphers = on
#ssl_ecdh_curve = 'prime256v1'
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
#------------------------------------------------------------------------------
# RESOURCE USAGE (except WAL)
#------------------------------------------------------------------------------
# - Memory -
shared_buffers = 1634MB # min 128kB
# (change requires restart)
#huge_pages = try # on, off, or try
# (change requires restart)
#temp_buffers = 8MB # min 800kB
#max_prepared_transactions = 0 # zero disables the feature
# (change requires restart)
# Caution: it is not advisable to set max_prepared_transactions nonzero unless
# you actively intend to use prepared transactions.
work_mem = 8MB # min 64kB
maintenance_work_mem = 524MB # min 1MB
#autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem
#max_stack_depth = 2MB # min 100kB
dynamic_shared_memory_type = posix # the default is the first option
# supported by the operating system:
# posix
# sysv
# windows
# mmap
# use none to disable dynamic shared memory
# (change requires restart)
# - Disk -
#temp_file_limit = -1 # limits per-process temp file space
# in kB, or -1 for no limit
# - Kernel Resources -
#max_files_per_process = 1000 # min 25
# (change requires restart)
# - Cost-Based Vacuum Delay -
#vacuum_cost_delay = 0 # 0-100 milliseconds
#vacuum_cost_page_hit = 1 # 0-10000 credits
#vacuum_cost_page_miss = 10 # 0-10000 credits
#vacuum_cost_page_dirty = 20 # 0-10000 credits
#vacuum_cost_limit = 200 # 1-10000 credits
# - Background Writer -
#bgwriter_delay = 200ms # 10-10000ms between rounds
#bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables
#bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round
#bgwriter_flush_after = 512kB # measured in pages, 0 disables
# - Asynchronous Behavior -
effective_io_concurrency = 16 # 1-1000; 0 disables prefetching
#max_worker_processes = 8 # (change requires restart)
#max_parallel_maintenance_workers = 2 # taken from max_parallel_workers
#max_parallel_workers_per_gather = 2 # taken from max_parallel_workers
#parallel_leader_participation = on
#max_parallel_workers = 8 # maximum number of max_worker_processes that
# can be used in parallel operations
#old_snapshot_threshold = -1 # 1min-60d; -1 disables; 0 is immediate
# (change requires restart)
#backend_flush_after = 0 # measured in pages, 0 disables
#------------------------------------------------------------------------------
# WRITE-AHEAD LOG
#------------------------------------------------------------------------------
# - Settings -
wal_level = logical # minimal, replica, or logical
# (change requires restart)
#fsync = on # flush data to disk for crash safety
# (turning this off can cause
# unrecoverable data corruption)
synchronous_commit = off # synchronization level;
# off, local, remote_write, remote_apply, or on
#wal_sync_method = fsync # the default is the first option
# supported by the operating system:
# open_datasync
# fdatasync (default on Linux)
# fsync
# fsync_writethrough
# open_sync
#full_page_writes = on # recover from partial page writes
#wal_compression = off # enable compression of full-page writes
#wal_log_hints = off # also do full page writes of non-critical updates
# (change requires restart)
wal_buffers = 17MB # min 32kB, -1 sets based on shared_buffers
# (change requires restart)
#wal_writer_delay = 200ms # 1-10000 milliseconds
#wal_writer_flush_after = 1MB # measured in pages, 0 disables
#commit_delay = 0 # range 0-100000, in microseconds
#commit_siblings = 5 # range 1-1000
# - Checkpoints -
checkpoint_timeout = 3600s # range 30s-1d
max_wal_size = 6818MB
min_wal_size = 80MB
checkpoint_completion_target = 0.75 # checkpoint target duration, 0.0 - 1.0
#checkpoint_flush_after = 256kB # measured in pages, 0 disables
#checkpoint_warning = 30s # 0 disables
# - Archiving -
#archive_mode = off # enables archiving; off, on, or always
# (change requires restart)
#archive_command = '' # command to use to archive a logfile segment
# placeholders: %p = path of file to archive
# %f = file name only
# e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f'
archive_timeout = 300 # force a logfile segment switch after this
# number of seconds; 0 disables
#------------------------------------------------------------------------------
# REPLICATION
#------------------------------------------------------------------------------
# - Sending Servers -
# Set these on the master and on any standby that will send replication data.
max_wal_senders = 8 # max number of walsender processes
# (change requires restart)
wal_keep_segments = 426 # in logfile segments; 0 disables
#wal_sender_timeout = 60s # in milliseconds; 0 disables
max_replication_slots = 8 # max number of replication slots
# (change requires restart)
#track_commit_timestamp = off # collect timestamp of transaction commit
# (change requires restart)
# - Master Server -
# These settings are ignored on a standby server.
#synchronous_standby_names = '' # standby servers that provide sync rep
# method to choose sync standbys, number of sync standbys,
# and comma-separated list of application_name
# from standby(s); '*' = all
#vacuum_defer_cleanup_age = 0 # number of xacts by which cleanup is delayed
# - Standby Servers -
# These settings are ignored on a master server.
#hot_standby = on # "off" disallows queries during recovery
# (change requires restart)
#max_standby_archive_delay = 30s # max delay before canceling queries
# when reading WAL from archive;
# -1 allows indefinite delay
#max_standby_streaming_delay = 30s # max delay before canceling queries
# when reading streaming WAL;
# -1 allows indefinite delay
#wal_receiver_status_interval = 10s # send replies at least this often
# 0 disables
#hot_standby_feedback = off # send info from standby to prevent
# query conflicts
#wal_receiver_timeout = 60s # time that receiver waits for
# communication from master
# in milliseconds; 0 disables
#wal_retrieve_retry_interval = 5s # time to wait before retrying to
# retrieve WAL after a failed attempt
# - Subscribers -
# These settings are ignored on a publisher.
#max_logical_replication_workers = 4 # taken from max_worker_processes
# (change requires restart)
#max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers
#------------------------------------------------------------------------------
# QUERY TUNING
#------------------------------------------------------------------------------
# - Planner Method Configuration -
#enable_bitmapscan = on
#enable_hashagg = on
#enable_hashjoin = on
#enable_indexscan = on
#enable_indexonlyscan = on
#enable_material = on
#enable_mergejoin = on
#enable_nestloop = on
#enable_parallel_append = on
#enable_seqscan = on
#enable_sort = on
#enable_tidscan = on
#enable_partitionwise_join = off
#enable_partitionwise_aggregate = off
#enable_parallel_hash = on
#enable_partition_pruning = on
# - Planner Cost Constants -
#seq_page_cost = 1.0 # measured on an arbitrary scale
random_page_cost = 1.0 # same scale as above
#cpu_tuple_cost = 0.01 # same scale as above
#cpu_index_tuple_cost = 0.005 # same scale as above
#cpu_operator_cost = 0.0025 # same scale as above
#parallel_tuple_cost = 0.1 # same scale as above
#parallel_setup_cost = 1000.0 # same scale as above
#jit_above_cost = 100000 # perform JIT compilation if available
# and query more expensive than this;
# -1 disables
#jit_inline_above_cost = 500000 # inline small functions if query is
# more expensive than this; -1 disables
#jit_optimize_above_cost = 500000 # use expensive JIT optimizations if
# query is more expensive than this;
# -1 disables
#min_parallel_table_scan_size = 8MB
#min_parallel_index_scan_size = 512kB
effective_cache_size = 4899MB
# - Genetic Query Optimizer -
#geqo = on
#geqo_threshold = 12
#geqo_effort = 5 # range 1-10
#geqo_pool_size = 0 # selects default based on effort
#geqo_generations = 0 # selects default based on effort
#geqo_selection_bias = 2.0 # range 1.5-2.0
#geqo_seed = 0.0 # range 0.0-1.0
# - Other Planner Options -
#default_statistics_target = 100 # range 1-10000
#constraint_exclusion = partition # on, off, or partition
#cursor_tuple_fraction = 0.1 # range 0.0-1.0
#from_collapse_limit = 8
#join_collapse_limit = 8 # 1 disables collapsing of explicit
# JOIN clauses
#force_parallel_mode = off
#jit = off # allow JIT compilation
#------------------------------------------------------------------------------
# REPORTING AND LOGGING
#------------------------------------------------------------------------------
# - Where to Log -
#log_destination = 'stderr' # Valid values are combinations of
# stderr, csvlog, syslog, and eventlog,
# depending on platform. csvlog
# requires logging_collector to be on.
# This is used when logging to stderr:
#logging_collector = off # Enable capturing of stderr and csvlog
# into log files. Required to be on for
# csvlogs.
# (change requires restart)
# These are only used if logging_collector is on:
#log_directory = 'log' # directory where log files are written,
# can be absolute or relative to PGDATA
#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern,
# can include strftime() escapes
#log_file_mode = 0600 # creation mode for log files,
# begin with 0 to use octal notation
#log_truncate_on_rotation = off # If on, an existing log file with the
# same name as the new log file will be
# truncated rather than appended to.
# But such truncation only occurs on
# time-driven rotation, not on restarts
# or size-driven rotation. Default is
# off, meaning append to existing files
# in all cases.
#log_rotation_age = 1d # Automatic rotation of logfiles will
# happen after that time. 0 disables.
#log_rotation_size = 10MB # Automatic rotation of logfiles will
# happen after that much log output.
# 0 disables.
# These are relevant when logging to syslog:
#syslog_facility = 'LOCAL0'
#syslog_ident = 'postgres'
#syslog_sequence_numbers = on
#syslog_split_messages = on
# This is only relevant when logging to eventlog (win32):
# (change requires restart)
#event_source = 'PostgreSQL'
# - When to Log -
#log_min_messages = warning # values in order of decreasing detail:
# debug5
# debug4
# debug3
# debug2
# debug1
# info
# notice
# warning
# error
# log
# fatal
# panic
#log_min_error_statement = error # values in order of decreasing detail:
# debug5
# debug4
# debug3
# debug2
# debug1
# info
# notice
# warning
# error
# log
# fatal
# panic (effectively off)
log_min_duration_statement = 1000 # -1 is disabled, 0 logs all statements
# and their durations, > 0 logs only
# statements running at least this number
# of milliseconds
# - What to Log -
#debug_print_parse = off
#debug_print_rewritten = off
#debug_print_plan = off
#debug_pretty_print = on
log_checkpoints = on
log_connections = on
log_disconnections = on
#log_duration = off
#log_error_verbosity = default # terse, default, or verbose messages
#log_hostname = off
log_line_prefix = 'user=%u,db=%d,app=%a,client=%h ' # special values:
# %a = application name
# %u = user name
# %d = database name
# %r = remote host and port
# %h = remote host
# %p = process ID
# %t = timestamp without milliseconds
# %m = timestamp with milliseconds
# %n = timestamp with milliseconds (as a Unix epoch)
# %i = command tag
# %e = SQL state
# %c = session ID
# %l = session line number
# %s = session start timestamp
# %v = virtual transaction ID
# %x = transaction ID (0 if none)
# %q = stop here in non-session
# processes
# %% = '%'
# e.g. '<%u%%%d> '
log_lock_waits = on # log lock waits >= deadlock_timeout
#log_statement = 'none' # none, ddl, mod, all
#log_replication_commands = off
log_temp_files = 100 # log temporary files equal or larger
# than the specified size in kilobytes;
# -1 disables, 0 logs all temp files
log_timezone = 'Etc/UTC'
#------------------------------------------------------------------------------
# PROCESS TITLE
#------------------------------------------------------------------------------
#cluster_name = '' # added to process titles if nonempty
# (change requires restart)
#update_process_title = on
#------------------------------------------------------------------------------
# STATISTICS
#------------------------------------------------------------------------------
# - Query and Index Statistics Collector -
#track_activities = on
#track_counts = on
#track_io_timing = off
#track_functions = none # none, pl, all
#track_activity_query_size = 1024 # (change requires restart)
#stats_temp_directory = 'pg_stat_tmp'
# - Monitoring -
#log_parser_stats = off
#log_planner_stats = off
#log_executor_stats = off
#log_statement_stats = off
#------------------------------------------------------------------------------
# AUTOVACUUM
#------------------------------------------------------------------------------
#autovacuum = on # Enable autovacuum subprocess? 'on'
# requires track_counts to also be on.
log_autovacuum_min_duration = 1000 # -1 disables, 0 logs all actions and
# their durations, > 0 logs only
# actions running at least this number
# of milliseconds.
#autovacuum_max_workers = 3 # max number of autovacuum subprocesses
# (change requires restart)
#autovacuum_naptime = 1min # time between autovacuum runs
#autovacuum_vacuum_threshold = 50 # min number of row updates before
# vacuum
#autovacuum_analyze_threshold = 50 # min number of row updates before
# analyze
#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum
#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze
autovacuum_freeze_max_age = 1000000000 # maximum XID age before forced vacuum
# (change requires restart)
#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age
# before forced vacuum
# (change requires restart)
#autovacuum_vacuum_cost_delay = 20ms # default vacuum cost delay for
# autovacuum, in milliseconds;
# -1 means use vacuum_cost_delay
#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for
# autovacuum, -1 means use
# vacuum_cost_limit
#------------------------------------------------------------------------------
# CLIENT CONNECTION DEFAULTS
#------------------------------------------------------------------------------
# - Statement Behavior -
#client_min_messages = notice # values in order of decreasing detail:
# debug5
# debug4
# debug3
# debug2
# debug1
# log
# notice
# warning
# error
#search_path = '"$user", public' # schema names
#row_security = on
#default_tablespace = '' # a tablespace name, '' uses the default
#temp_tablespaces = '' # a list of tablespace names, '' uses
# only default tablespace
#check_function_bodies = on
#default_transaction_isolation = 'read committed'
#default_transaction_read_only = off
#default_transaction_deferrable = off
#session_replication_role = 'origin'
#statement_timeout = 0 # in milliseconds, 0 is disabled
#lock_timeout = 0 # in milliseconds, 0 is disabled
#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled
#vacuum_freeze_min_age = 50000000
#vacuum_freeze_table_age = 150000000
#vacuum_multixact_freeze_min_age = 5000000
#vacuum_multixact_freeze_table_age = 150000000
#vacuum_cleanup_index_scale_factor = 0.1 # fraction of total number of tuples
# before index cleanup, 0 always performs
# index cleanup
#bytea_output = 'hex' # hex, escape
#xmlbinary = 'base64'
#xmloption = 'content'
#gin_fuzzy_search_limit = 0
#gin_pending_list_limit = 4MB
# - Locale and Formatting -
datestyle = 'iso, mdy'
#intervalstyle = 'postgres'
timezone = 'Etc/UTC'
#timezone_abbreviations = 'Default' # Select the set of available time zone
# abbreviations. Currently, there are
# Default
# Australia (historical usage)
# India
# You can create your own file in
# share/timezonesets/.
#extra_float_digits = 0 # min -15, max 3
#client_encoding = sql_ascii # actually, defaults to database
# encoding
# These settings are initialized by initdb, but they can be changed.
lc_messages = 'en_US.utf8' # locale for system error message
# strings
lc_monetary = 'en_US.utf8' # locale for monetary formatting
lc_numeric = 'en_US.utf8' # locale for number formatting
lc_time = 'en_US.utf8' # locale for time formatting
# default configuration for text search
default_text_search_config = 'pg_catalog.simple'
# - Shared Library Preloading -
#shared_preload_libraries = '' # (change requires restart)
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
#local_preload_libraries = ''
#session_preload_libraries = ''
#jit_provider = 'llvmjit' # JIT library to use
# - Other Defaults -
#dynamic_library_path = '$libdir'
#------------------------------------------------------------------------------
# LOCK MANAGEMENT
#------------------------------------------------------------------------------
#deadlock_timeout = 1s
#max_locks_per_transaction = 64 # min 10
# (change requires restart)
#max_pred_locks_per_transaction = 64 # min 10
# (change requires restart)
#max_pred_locks_per_relation = -2 # negative values mean
# (max_pred_locks_per_transaction
# / -max_pred_locks_per_relation) - 1
#max_pred_locks_per_page = 2 # min 0
#------------------------------------------------------------------------------
# VERSION AND PLATFORM COMPATIBILITY
#------------------------------------------------------------------------------
# - Previous PostgreSQL Versions -
#array_nulls = on
#backslash_quote = safe_encoding # on, off, or safe_encoding
#default_with_oids = off
#escape_string_warning = on
#lo_compat_privileges = off
#operator_precedence_warning = off
#quote_all_identifiers = off
#standard_conforming_strings = on
#synchronize_seqscans = on
# - Other Platforms and Clients -
#transform_null_equals = off
#------------------------------------------------------------------------------
# ERROR HANDLING
#------------------------------------------------------------------------------
#exit_on_error = off # terminate session on any error?
#restart_after_crash = on # reinitialize after backend crash?
#data_sync_retry = off # retry or panic on failure to fsync
# data?
# (change requires restart)
#------------------------------------------------------------------------------
# CONFIG FILE INCLUDES
#------------------------------------------------------------------------------
# These options allow settings to be loaded from files other than the
# default postgresql.conf.
#include_dir = '' # include files ending in '.conf' from
# a directory, e.g., 'conf.d'
#include_if_exists = '' # include file only if it exists
#include = '' # include file
#------------------------------------------------------------------------------
# CUSTOMIZED OPTIONS
#------------------------------------------------------------------------------
# Add settings for extensions here

View file

@ -2,19 +2,57 @@ version: "3.7"
# Service Definitions # Service Definitions
services: services:
db: db:
environment: environment:
POSTGRES_DB: "t2_stats" POSTGRES_DB: "t2_stats"
POSTGRES_USER: "dev" POSTGRES_USER: "dev"
POSTGRES_PASSWORD: "dev" POSTGRES_PASSWORD: "dev"
ports:
- "5432:5432"
volumes: volumes:
- ./build/postgres/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d - ./build/postgres/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
- ./build/postgres/export_local_db.sh:/export_local_db.sh - ./build/postgres/export_local_db.sh:/export_local_db.sh
# - ./build/postgres/postgresql.conf:/var/lib/postgresql/data/postgresql.conf
parser: parser:
environment: environment:
DATABASE_URL: "postgres://dev:dev@db:5432/t2_stats" DATABASE_URL: "postgres://dev:dev@db:5432/t2_stats"
ports:
- "8000:8080"
volumes: volumes:
- ./app/t2-stat-parser:/app - ./app/t2-stat-parser:/app
api:
environment:
NODE_ENV: "development" # auto-reloads app on save
env_file:
- .env
ports:
- "8080:8080"
- "8443:8443"
volumes:
- ./build/api/ecosystem._PROD_.config.js:/opt/node_app/ecosystem._PROD_.config.js
- ./build/api/ecosystem._DEV_.config.js:/opt/node_app/ecosystem._DEV_.config.js
- ./app/api:/opt/node_app/app:delegated
# temp vols
- notused:/opt/node_app/app/node_modules
- builtincontainer:/opt/node_app/app/dist
# pghero:
# image: ankane/pghero
# ports:
# - "9999:8080"
# environment:
# DATABASE_URL: "postgres://dev:dev@db:5432/t2_stats"
# networks:
# - internal
# - external
volumes:
notused:
builtincontainer:

View file

@ -2,14 +2,11 @@ version: "3.7"
# Service Definitions # Service Definitions
services: services:
db: db:
image: "amineo/t2-stats-db:v0.1.1" image: "amineo/t2-stats-db:v0.2.0"
build: build:
context: . context: .
dockerfile: ./build/postgres/Dockerfile dockerfile: ./build/postgres/Dockerfile
ports:
- "5432:5432"
environment: environment:
POSTGRES_DB: "t2_stats" POSTGRES_DB: "t2_stats"
POSTGRES_USER: "${POSTGRES_USER}" POSTGRES_USER: "${POSTGRES_USER}"
@ -18,31 +15,52 @@ services:
- psqldata:/var/lib/postgresql/data - psqldata:/var/lib/postgresql/data
networks: networks:
- internal - internal
deploy:
labels:
- traefik.enable=false
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]
parser: parser:
image: "amineo/t2-stats-parser:v0.1.1" image: "amineo/t2-stats-parser:v0.5.0"
build: build:
context: . context: .
dockerfile: ./build/go-t2-stat-parser/Dockerfile dockerfile: ./build/go-t2-stat-parser/Dockerfile
ports:
- "8080:8080"
environment: environment:
DATABASE_URL: "postgres://${POSTGRES_USER}:{POSTGRES_PASSWORD}@db:5432/t2_stats" DATABASE_URL: "postgres://${POSTGRES_USER}:{POSTGRES_PASSWORD}@db:5432/t2_stats"
FTP_HOST: "${FTP_HOST}" FTP_HOST: "${FTP_HOST}"
FTP_USER: "${FTP_USER}" FTP_USER: "${FTP_USER}"
FTP_PW: "${FTP_PW}" FTP_PW: "${FTP_PW}"
FTP_PATH: "${FTP_PATH}"
depends_on:
- db
networks:
- internal
deploy:
labels:
- traefik.enable=false
mode: replicated
replicas: 1
api:
image: "amineo/t2-stats-api:v0.0.26"
build:
context: .
dockerfile: ./build/api/Dockerfile
environment:
NODE_ENV: "production" # set as default in image
APP_NAME: "T2StatsAPI" # set as default in image
depends_on: depends_on:
- db - db
networks: networks:
- internal - internal
- external - external
volumes: volumes:
psqldata: psqldata:
networks: networks:
external: external:
internal: internal:

7
tsconfig.json Normal file
View file

@ -0,0 +1,7 @@
{
"compilerOptions": {
"rootDirs": [
"./app/api"
]
}
}