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

@ -6,4 +6,15 @@ POSTGRES_PASSWORD="dev"
FTP_HOST="127.0.0.1"
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"

13
.gitignore vendored
View file

@ -1,4 +1,15 @@
.env
node_modules
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"
"os"
"os/exec"
"strings"
)
//ShellToUse is...
@ -24,12 +25,22 @@ func Shellout(command string) (error, string, string) {
func initFTP() {
ftpHOST := os.Getenv("FTP_HOST")
ftpPath := os.Getenv("FTP_PATH")
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 {
log.Printf("error: %v\n", err)
}
@ -39,3 +50,5 @@ func initFTP() {
fmt.Println(errout)
}

View file

@ -10,14 +10,15 @@ package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("Starting FTP stat file download")
// fmt.Println("Starting FTP stat file download")
initFTP()
fmt.Println("Stat files downloaded!")
// fmt.Println("Stat files downloaded!")
fmt.Println("Starting stat parser")
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
./start.sh
# Keep container running
tail -f /dev/null
echo "T2 Stat Parser Done!";
# Keep container running with the cron schedule
#crond -l 2 -f

View file

@ -32,8 +32,8 @@ import (
var (
connectionString = flag.String("conn", getenvWithDefault("DATABASE_URL", ""), "PostgreSQL connection string")
db *sqlx.DB
maxStatOverwrite int = 100
debugLevel int = 1 // 0 off,min | 1 Basic output checks | 2 Output all the things
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
)
func getenvWithDefault(name, defaultValue string) string {
@ -59,11 +59,12 @@ type Game struct {
dbStatOverWrite int `db.players:"stat_overwrite"`
statOverWrite int
gameMap string `db.games:"map"`
gameType string `db.games:"gametype"`
dateStamp string `db.games:"datestamp"`
stats string `db.games:"stats"`
uuid string `db.games:"uuid"`
gameMap string `db.game_detail:"map"`
gameID int `db.game_detail:"game_id"`
gameType string `db.game_detail:"gametype"`
dateStamp string `db.game_detail:"datestamp"`
stats string `db.game_detail:"stats"`
uuid string `db.game_detail:"uuid"`
}
func initParser() {
@ -163,6 +164,9 @@ func parseGameTypeStats(gt string) {
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)
g.dbStatOverWrite = getDBStatOverWrite(g.playerGUID, strings.ToLower(gt))
@ -175,32 +179,39 @@ func parseGameTypeStats(gt string) {
fmt.Println("Stat Overwrite", g.statOverWrite)
fmt.Println("maxStatOverwrite", maxStatOverwrite)
fmt.Println("statArrayMaxLength", statArrayMaxLength)
fmt.Println("g.dbStatOverWrite", g.dbStatOverWrite)
}
statCron := 0
if g.statOverWrite < g.dbStatOverWrite {
// 100 -
statCron = (maxStatOverwrite - g.statOverWrite) + g.dbStatOverWrite
} else {
statCron = g.statOverWrite - g.dbStatOverWrite
}
// statEntryDiff := 0
// if g.statOverWrite < g.dbStatOverWrite {
// // 100 -
// statEntryDiff = (maxStatOverwrite - g.statOverWrite) + g.dbStatOverWrite
// } else {
// statEntryDiff = g.statOverWrite - g.dbStatOverWrite
// }
// Reset statCron if it flows over maxStatOverwrite
if statCron > maxStatOverwrite {
statCron = 0
}
// // Reset statEntryDiff if it flows over maxStatOverwrite
// if statEntryDiff > maxStatOverwrite {
// statEntryDiff = 0
// }
if debugLevel >= 1 {
fmt.Println("statCron", statCron)
}
// if debugLevel >= 1 {
// fmt.Println("statEntryDiff", statEntryDiff)
// }
for i := 0; i <= statCron; i++ {
// arrPosition := i - 1
// fmt.Println(arrPosition)
for i := 0; i < statArrayMaxLength; i++ {
fmt.Println("index", i)
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("---")
}
@ -219,8 +230,13 @@ func parseStatOverWriteLine(g Game, mStatLine map[string][]string, arrPosition i
cleanStatLine := make(map[string][]string)
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])
} 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.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 {
// log the game struct
fmt.Println(g)
}
// insert game stat
addPlayerGameStat(g, strings.ToLower(gt))
// Check if we need to create a new game record
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 {
@ -252,6 +286,28 @@ func rowExists(query string, args ...interface{}) bool {
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) {
check := rowExists("select player_guid from players where player_guid = $1", g.playerGUID)
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) {
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)
@ -281,17 +347,27 @@ func getDBStatOverWrite(playerGUID int, gt string) int {
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) {
if debugLevel == 1 {
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
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)`
_, err := db.Exec(sqlInsert, g.playerGUID, g.playerName, g.statOverWrite, g.gameMap, g.stats, g.dateStamp, g.uuid, g.gameType)
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.gameID, g.stats, g.dateStamp, g.uuid, g.gameType)
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to add player's game stat: %v\n", err)
os.Exit(1)
@ -304,6 +380,9 @@ func addPlayerGameStat(g Game, gt string) {
panic(err)
}
} 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
else
echo "No build found, running from source"
cd /app
go run *.go
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
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
ENV TZ=America/New_York
#Set cron schedule (every day at 8am est)
RUN crontab -l -u root | echo "0 8 * * * sh -c \"/app/start.sh\"" | crontab -u root -
# #Set cron schedule (every day at 11:30am est)
# RUN echo '30 11 * * * /app/start.sh' > /etc/crontabs/root
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/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";
CREATE SEQUENCE IF NOT EXISTS games_id_seq;
-- Table Definition
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),
"player_guid" numeric NOT NULL,
"player_name" text NOT NULL,
"stat_overwrite" numeric NOT NULL,
"map" text NOT NULL,
"game_id" numeric NOT NULL,
"stats" jsonb NOT NULL,
"datestamp" timestamp NOT NULL,
"uuid" text NOT NULL UNIQUE,
"gametype" text NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT games_pk PRIMARY KEY (id),
FOREIGN KEY (game_id) REFERENCES games (game_id),
FOREIGN KEY (player_guid) REFERENCES players (player_guid)
);
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
RETURNS TRIGGER AS $$
BEGIN

View file

@ -3,7 +3,7 @@
echo "Setting up database..."
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
# -d database 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
services:
db:
environment:
POSTGRES_DB: "t2_stats"
POSTGRES_USER: "dev"
POSTGRES_PASSWORD: "dev"
volumes:
ports:
- "5432:5432"
volumes:
- ./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:
environment:
environment:
DATABASE_URL: "postgres://dev:dev@db:5432/t2_stats"
ports:
- "8000:8080"
volumes:
- ./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
services:
db:
image: "amineo/t2-stats-db:v0.1.1"
image: "amineo/t2-stats-db:v0.2.0"
build:
context: .
dockerfile: ./build/postgres/Dockerfile
ports:
- "5432:5432"
environment:
POSTGRES_DB: "t2_stats"
POSTGRES_USER: "${POSTGRES_USER}"
@ -17,32 +14,53 @@ services:
volumes:
- psqldata:/var/lib/postgresql/data
networks:
- internal
- internal
deploy:
labels:
- traefik.enable=false
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]
parser:
image: "amineo/t2-stats-parser:v0.1.1"
image: "amineo/t2-stats-parser:v0.5.0"
build:
context: .
dockerfile: ./build/go-t2-stat-parser/Dockerfile
ports:
- "8080:8080"
environment:
environment:
DATABASE_URL: "postgres://${POSTGRES_USER}:{POSTGRES_PASSWORD}@db:5432/t2_stats"
FTP_HOST: "${FTP_HOST}"
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:
- db
networks:
- internal
- external
volumes:
psqldata:
networks:
external:
internal:

7
tsconfig.json Normal file
View file

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