├── .coveragerc ├── .dockerignore ├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── .travis └── send.sh ├── Dockerfile ├── LICENSE ├── NOTICE ├── README.md ├── RLBotServer.py ├── backend ├── __init__.py ├── blueprints │ ├── __init__.py │ ├── admin.py │ ├── api.py │ ├── auth.py │ ├── shared_renders.py │ ├── spa_api │ │ ├── __init__.py │ │ ├── errors │ │ │ ├── __init__.py │ │ │ └── errors.py │ │ ├── service_layers │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── documentation.py │ │ │ ├── global_stats.py │ │ │ ├── homepage │ │ │ │ ├── patreon.py │ │ │ │ ├── recent.py │ │ │ │ └── twitch.py │ │ │ ├── leaderboards.py │ │ │ ├── logged_in_user.py │ │ │ ├── ml │ │ │ │ └── ml.py │ │ │ ├── player │ │ │ │ ├── __init__.py │ │ │ │ ├── play_style.py │ │ │ │ ├── play_style_progression.py │ │ │ │ ├── player.py │ │ │ │ ├── player_profile_stats.py │ │ │ │ └── player_ranks.py │ │ │ ├── queue_status.py │ │ │ ├── replay │ │ │ │ ├── __init__.py │ │ │ │ ├── basic_stats.py │ │ │ │ ├── enums.py │ │ │ │ ├── groups.py │ │ │ │ ├── heatmaps.py │ │ │ │ ├── json_tag.py │ │ │ │ ├── kickoffs.py │ │ │ │ ├── match_history.py │ │ │ │ ├── predicted_ranks.py │ │ │ │ ├── replay.py │ │ │ │ ├── replay_player.py │ │ │ │ ├── replay_positions.py │ │ │ │ ├── savedgroups │ │ │ │ │ └── groups.py │ │ │ │ ├── visibility.py │ │ │ │ └── visualizations.py │ │ │ ├── stat.py │ │ │ └── utils.py │ │ ├── spa_api.py │ │ └── utils │ │ │ ├── __init__.py │ │ │ ├── decorators.py │ │ │ ├── query_param_definitions.py │ │ │ └── query_params_handler.py │ └── steam.py ├── data │ ├── __init__.py │ ├── categorized_items.json │ └── constants │ │ ├── __init__.py │ │ ├── car.py │ │ └── playlist.py ├── database │ ├── __init__.py │ ├── objects.py │ ├── startup.py │ ├── utils │ │ ├── __init__.py │ │ ├── debug_db.py │ │ ├── dynamic_field_manager.py │ │ └── utils.py │ └── wrapper │ │ ├── __init__.py │ │ ├── chart │ │ ├── __init__.py │ │ ├── chart_data.py │ │ ├── player_chart_metadata.py │ │ ├── stat_point.py │ │ └── team_chart_metadata.py │ │ ├── field_wrapper.py │ │ ├── player_wrapper.py │ │ ├── query_filter_builder.py │ │ ├── rank_wrapper.py │ │ ├── stats │ │ ├── __init__.py │ │ ├── chart_stats_wrapper.py │ │ ├── creation │ │ │ ├── __init__.py │ │ │ ├── player_stat_creation.py │ │ │ ├── replay_group_stat_creation.py │ │ │ ├── shared_stat_creation.py │ │ │ └── team_stat_creation.py │ │ ├── global_stats_wrapper.py │ │ ├── item_stats_wrapper.py │ │ ├── player_stat_wrapper.py │ │ ├── shared_stats_wrapper.py │ │ └── stat_math.py │ │ └── tag_wrapper.py ├── initial_setup.py ├── server_constants.py ├── tasks │ ├── __init__.py │ ├── add_replay.py │ ├── celery.sh │ ├── celery_tasks.py │ ├── celery_worker.py │ ├── celeryconfig.py │ ├── middleware.py │ ├── periodic_stats.py │ ├── task_creators.py │ ├── training_packs │ │ ├── __init__.py │ │ ├── packs │ │ │ ├── 1ShotBeckwithDefault.Tem │ │ │ └── 1ShotBeckwithDefaultGoalie.Tem │ │ ├── parsing │ │ │ ├── __init__.py │ │ │ ├── binary_reader.py │ │ │ ├── crc.py │ │ │ ├── decrypt.py │ │ │ └── parse.py │ │ ├── task.py │ │ └── training_packs.py │ ├── update.py │ └── utils.py └── utils │ ├── __init__.py │ ├── braacket_connection.py │ ├── checks.py │ ├── cloud_handler.py │ ├── file_manager.py │ ├── logger.py │ ├── metrics.py │ ├── parsing_manager.py │ ├── psyonix_api_handler.py │ ├── rlgarage_handler.py │ ├── safe_flask_globals.py │ └── time_related.py ├── celery2.sh ├── celerycron.sh ├── codecov.yml ├── data └── __init__.py ├── docker-compose.yml ├── gunicorn_conf.py ├── helpers ├── __init__.py ├── clean_database.py ├── convert_existing_replays.py ├── gcp_reparse.py ├── insert_pickled_replays.py ├── migrate_database.py ├── modify_existing_pickles.py ├── reparse_all_replays.py └── storage_transfer.py ├── imports_test.py ├── iptables.conf ├── loader.py ├── mac_run.sh ├── mac_stop.sh ├── redis ├── EventLog.dll ├── redis-server.exe ├── redis.windows-service.conf └── redis.windows.conf ├── requirements-ml.txt ├── requirements-test.txt ├── requirements.txt ├── run-ssl.sh ├── run.sh ├── tests ├── __init__.py ├── conftest.py ├── integration_tests │ ├── __init__.py │ ├── conftest.py │ └── no_react │ │ ├── __init__.py │ │ ├── heatmaps_test.py │ │ ├── test_upload.py │ │ └── upload_proto_test.py ├── server_tests │ ├── __init__.py │ ├── api_tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── documentation │ │ │ ├── __init__.py │ │ │ └── api_documentation_test.py │ │ ├── download │ │ │ ├── __init__.py │ │ │ └── download_position_test.py │ │ ├── private_replay_edit_test.py │ │ ├── search │ │ │ └── replay_history_test.py │ │ ├── stats │ │ │ ├── __init__.py │ │ │ ├── get_replay_stats_test.py │ │ │ └── kickoffs.py │ │ └── upload │ │ │ ├── __init__.py │ │ │ ├── private_replay_upload_test.py │ │ │ ├── tag_upload_test.py │ │ │ └── upload_test.py │ ├── backend_utils_tests │ │ ├── RLBot_Player.html │ │ ├── RLBot_Player_SkyBot.html │ │ ├── __init__.py │ │ ├── braacket_test.py │ │ ├── conftest.py │ │ └── initial_setup_test.py │ ├── conftest.py │ ├── database_tests │ │ ├── __init__.py │ │ └── wrapper │ │ │ ├── __init__.py │ │ │ ├── query_filter_builder_test.py │ │ │ ├── stats │ │ │ ├── __init__.py │ │ │ └── shared_stats_test.py │ │ │ ├── tag_wrapper_test.py │ │ │ └── utils_test.py │ └── task_tests │ │ ├── __init__.py │ │ ├── celery_test.py │ │ └── training_packs_test.py ├── test_replays │ ├── 3_DRIBBLES_2_FLICKS.replay │ ├── 3_KICKOFFS_4_SHOTS.replay │ ├── ALL_STAR.replay │ ├── ALL_STAR_SCOUT.replay │ ├── FAKE_BOTS_SkyBot.replay │ ├── NO_KICKOFF.replay │ ├── RUMBLE_FULL.replay │ ├── SKYBOT_DRIBBLE_INFO.replay │ ├── TRAINING_PACK.replay │ ├── WASTED_BOOST_WHILE_SUPER_SONIC.replay │ ├── ZEROED_STATS.replay │ ├── crossplatform_party.replay │ └── small_replays │ │ ├── 0_JUMPS.replay │ │ ├── 100_BOOST_PAD_0_USED.replay │ │ ├── 100_BOOST_PAD_100_USED.replay │ │ ├── 12_AND_100_BOOST_PADS_0_USED.replay │ │ ├── 12_BOOST_PAD_0_USED.replay │ │ ├── 12_BOOST_PAD_45_USED.replay │ │ ├── 1_AERIAL.replay │ │ ├── 1_CLEAR.replay │ │ ├── 1_DEMO.replay │ │ ├── 1_DOUBLE_JUMP.replay │ │ ├── 1_EPIC_SAVE.replay │ │ ├── 1_JUMP.replay │ │ ├── 1_NORMAL_SAVE.replay │ │ ├── 3_KICKOFFS_4_SHOTS.replay │ │ ├── CALCULATE_USED_BOOST_DEMO_WITH_FLIPS.replay │ │ ├── CALCULATE_USED_BOOST_WITH_DEMO.replay │ │ ├── GROUNDED_PASS_GOAL.replay │ │ ├── HIGH_AIR_PASS_GOAL.replay │ │ ├── LAST_KICKOFF_NO_TOUCH.replay │ │ ├── MID_AIR_PASS_GOAL.replay │ │ ├── MORE_THAN_100_BOOST.replay │ │ ├── NO_BOOST_PAD_0_USED.replay │ │ ├── NO_BOOST_PAD_33_USED.replay │ │ ├── NO_KICKOFF.replay │ │ ├── USE_BOOST_AFTER_GOAL.replay │ │ └── WASTED_BOOST_WHILE_SUPER_SONIC.replay └── utils │ ├── __init__.py │ ├── database_utils.py │ ├── killable_thread.py │ ├── location_utils.py │ ├── replay_utils.py │ └── test_utils.py ├── tmuxinator.yml ├── update_run.sh ├── webapp ├── .prettierrc.json ├── README.md ├── images.d.ts ├── package-lock.json ├── package.json ├── public │ ├── ai.jpg │ ├── calculated-icon.png │ ├── draco │ │ ├── draco_decoder.wasm │ │ └── draco_wasm_wrapper.js │ ├── fieldblack.png │ ├── fieldrblack.png │ ├── index.html │ ├── manifest.json │ ├── models │ │ ├── FieldTest1.mtl │ │ ├── FieldTest2.mtl │ │ └── field.obj │ ├── psynet.jpg │ ├── ranks │ │ ├── 0.png │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 11.png │ │ ├── 12.png │ │ ├── 13.png │ │ ├── 14.png │ │ ├── 15.png │ │ ├── 16.png │ │ ├── 17.png │ │ ├── 18.png │ │ ├── 19.png │ │ ├── 2.png │ │ ├── 20.png │ │ ├── 21.png │ │ ├── 22.png │ │ ├── 3.png │ │ ├── 31.png │ │ ├── 32.png │ │ ├── 33.png │ │ ├── 34.png │ │ ├── 35.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ ├── replay_page_background.jpg │ ├── replay_page_background_black.jpg │ ├── splash.png │ └── splash_black.png ├── src │ ├── App.tsx │ ├── AppListener.tsx │ ├── CodeSplitComponent.tsx │ ├── Components │ │ ├── Admin │ │ │ ├── AdminLogDisplayRow.tsx │ │ │ └── AdminLogResultDisplay.tsx │ │ ├── GlobalStatsChart.tsx │ │ ├── GlobalStatsRankGraph.tsx │ │ ├── Home │ │ │ ├── HomePageAppBar.tsx │ │ │ ├── HomePageFooter.tsx │ │ │ └── Widgets │ │ │ │ ├── Leaderboards.tsx │ │ │ │ ├── Patreon.tsx │ │ │ │ ├── Recent.tsx │ │ │ │ └── Twitch.tsx │ │ ├── ItemStats │ │ │ ├── ItemDisplay.tsx │ │ │ ├── ItemStatsGraph.tsx │ │ │ └── ItemStatsUsers.tsx │ │ ├── Leaderboards │ │ │ ├── LeaderListItem.tsx │ │ │ ├── LeaderboardList.tsx │ │ │ └── PlaylistLeaderboardGrid.tsx │ │ ├── Pages │ │ │ ├── AboutPage.tsx │ │ │ ├── AdminPage.tsx │ │ │ ├── BasePage.tsx │ │ │ ├── DocumentationPage.tsx │ │ │ ├── ExplanationsPage.tsx │ │ │ ├── GlobalStatsPage.tsx │ │ │ ├── HomePage.tsx │ │ │ ├── ItemStatsPage.tsx │ │ │ ├── LeaderboardsPage.tsx │ │ │ ├── PlayerComparePage.tsx │ │ │ ├── PlayerPage.tsx │ │ │ ├── PluginsPage.tsx │ │ │ ├── PrivacyPolicyPage.tsx │ │ │ ├── ReplayPage.tsx │ │ │ ├── ReplaysGroupPage.tsx │ │ │ ├── ReplaysSearchPage.tsx │ │ │ ├── SavedReplaysGroupPage.tsx │ │ │ ├── SavedReplaysMyGroupsPage.tsx │ │ │ ├── StatusPage.tsx │ │ │ ├── TagsPage.tsx │ │ │ ├── TrainingPackPage.tsx │ │ │ └── UploadPage.tsx │ │ ├── Player │ │ │ ├── Compare │ │ │ │ ├── AddPlayerInput.tsx │ │ │ │ ├── PlayStyle │ │ │ │ │ ├── PlayerComparePlayStyleCharts.tsx │ │ │ │ │ └── PlayerCompareTable.tsx │ │ │ │ ├── PlayerChip.tsx │ │ │ │ ├── PlayerCompareContent.tsx │ │ │ │ └── Progression │ │ │ │ │ ├── FieldSelect.tsx │ │ │ │ │ ├── PlayerProgressionCharts.tsx │ │ │ │ │ └── ProgressionChart.tsx │ │ │ ├── Overview │ │ │ │ ├── MatchHistory │ │ │ │ │ ├── FullMatchHistoryLinkButton.tsx │ │ │ │ │ ├── OverviewMatchHistory.tsx │ │ │ │ │ ├── OverviewMatchHistoryRow.tsx │ │ │ │ │ ├── PlayerMatchHistoryCard.tsx │ │ │ │ │ └── ReplayExpansionPanelSummary.tsx │ │ │ │ ├── PlayStyle │ │ │ │ │ ├── PlayStyleActions.tsx │ │ │ │ │ ├── PlayStyleExplanationTable.tsx │ │ │ │ │ ├── PlayerPlayStyle.tsx │ │ │ │ │ ├── PlayerPlayStyleCard.tsx │ │ │ │ │ └── PlayerPlayStyleChart.tsx │ │ │ │ └── SideBar │ │ │ │ │ ├── GroupIndicator.tsx │ │ │ │ │ ├── PlayerAdminToggles.tsx │ │ │ │ │ ├── PlayerNameDropdown.tsx │ │ │ │ │ ├── PlayerPlaylistRank.tsx │ │ │ │ │ ├── PlayerProfile.tsx │ │ │ │ │ ├── PlayerProfilePicture.tsx │ │ │ │ │ ├── PlayerRanksCard.tsx │ │ │ │ │ ├── PlayerSideBar.tsx │ │ │ │ │ └── PlayerStats │ │ │ │ │ ├── FavouriteCar.tsx │ │ │ │ │ ├── LoadoutDialogWrapper.tsx │ │ │ │ │ ├── PlayerStatsCard.tsx │ │ │ │ │ └── PlaysWith.tsx │ │ │ └── PlayerOverview.tsx │ │ ├── Replay │ │ │ ├── BasicStats │ │ │ │ ├── PlayerStats │ │ │ │ │ ├── PlayerStatsCharts.tsx │ │ │ │ │ ├── PlayerStatsContent.tsx │ │ │ │ │ └── PlayerStatsTabs.tsx │ │ │ │ └── TeamStats │ │ │ │ │ ├── TeamStatsCharts.tsx │ │ │ │ │ ├── TeamStatsContent.tsx │ │ │ │ │ └── TeamStatsTabs.tsx │ │ │ ├── Heatmap │ │ │ │ ├── Heatmap.tsx │ │ │ │ ├── HeatmapContent.tsx │ │ │ │ ├── HeatmapTabs.tsx │ │ │ │ ├── HeatmapTabsWrapper.tsx │ │ │ │ └── HitsContent.tsx │ │ │ ├── Kickoffs │ │ │ │ ├── KickoffCountsTable.tsx │ │ │ │ ├── KickoffField.tsx │ │ │ │ ├── KickoffMapWrapper.tsx │ │ │ │ ├── KickoffTabs.tsx │ │ │ │ ├── KickoffTabsWrapper.tsx │ │ │ │ └── PlayerStartEnd.tsx │ │ │ ├── Predictions │ │ │ │ ├── PredictedRanksRow.tsx │ │ │ │ ├── PredictedRanksTable.tsx │ │ │ │ └── Predictions.tsx │ │ │ ├── README.md │ │ │ ├── ReplayBoxScore.tsx │ │ │ ├── ReplayChart.tsx │ │ │ ├── ReplayTabs.tsx │ │ │ ├── ReplayTeamCard │ │ │ │ ├── CameraSettingsDisplay.tsx │ │ │ │ ├── Loadout │ │ │ │ │ ├── LoadoutItemDisplay.tsx │ │ │ │ │ ├── PaintedTriangle.tsx │ │ │ │ │ └── dataMaps.ts │ │ │ │ ├── LoadoutDisplay.tsx │ │ │ │ ├── ReplayTeamCard.tsx │ │ │ │ └── TeamCardPlayer.tsx │ │ │ ├── ReplayView.tsx │ │ │ ├── ReplayViewer │ │ │ │ └── Viewer.tsx │ │ │ └── Visualizations │ │ │ │ ├── BoostCountsTable.tsx │ │ │ │ ├── BoostField.tsx │ │ │ │ ├── BoostMapWrapper.tsx │ │ │ │ ├── TeamPie.tsx │ │ │ │ └── VisualizationsContent.tsx │ │ ├── ReplaysGroup │ │ │ ├── AddReplayInput.tsx │ │ │ ├── Charts │ │ │ │ ├── ReplaysGroupCharts.tsx │ │ │ │ └── ReplaysGroupChartsWrapper.tsx │ │ │ ├── ReplayChip.tsx │ │ │ ├── ReplaysGroupContent.tsx │ │ │ └── Table │ │ │ │ ├── BasicStatsTable.tsx │ │ │ │ ├── ReplaysGroupTable.tsx │ │ │ │ └── TableScrollWrapper.tsx │ │ ├── ReplaysSavedGroup │ │ │ ├── GroupAddDialog.tsx │ │ │ ├── GroupPlayerStatsTable.tsx │ │ │ ├── GroupPlayerStatsTableWrapper.tsx │ │ │ ├── GroupRenameDialog.tsx │ │ │ ├── GroupSubGroupAddDialog.tsx │ │ │ ├── GroupTeamStatsTable.tsx │ │ │ ├── GroupTeamStatsTableWrapper.tsx │ │ │ ├── Shared │ │ │ │ └── GroupStatsButtons.tsx │ │ │ └── SubgroupEntry.tsx │ │ ├── ReplaysSearch │ │ │ ├── Filter │ │ │ │ ├── PlayerEntry.tsx │ │ │ │ ├── ReplaysSearchFilter.tsx │ │ │ │ └── ReplaysSearchWithQueryString.tsx │ │ │ ├── ReplayDisplayRow.tsx │ │ │ ├── ReplaysSearchResultDisplay.tsx │ │ │ ├── ReplaysSearchTablePagination.tsx │ │ │ ├── ResultsActions.tsx │ │ │ └── VisibilityToggle..tsx │ │ ├── Shared │ │ │ ├── Charts │ │ │ │ ├── ColoredBarChart.tsx │ │ │ │ ├── ColoredPieChart.tsx │ │ │ │ ├── ColoredRadarChart.tsx │ │ │ │ └── StatChart.tsx │ │ │ ├── ClearableDatePicker.tsx │ │ │ ├── ColouredGameScore.tsx │ │ │ ├── Documentation │ │ │ │ └── QueryParams.tsx │ │ │ ├── Footer.tsx │ │ │ ├── IconTooltip.tsx │ │ │ ├── LinkButton.tsx │ │ │ ├── LoadableWrapper.tsx │ │ │ ├── Logo │ │ │ │ ├── Logo.tsx │ │ │ │ ├── calculated-logo-birthday-light.png │ │ │ │ ├── calculated-logo-birthday.png │ │ │ │ ├── calculated-logo-light.png │ │ │ │ └── calculated-logo.png │ │ │ ├── NavBar │ │ │ │ ├── AccountMenu.tsx │ │ │ │ └── NavBar.tsx │ │ │ ├── Notification │ │ │ │ ├── NotificationSnackbar.tsx │ │ │ │ ├── NotificationTestButton.tsx │ │ │ │ ├── NotificationUtils.ts │ │ │ │ └── Notifications.tsx │ │ │ ├── PageContent.tsx │ │ │ ├── Search.tsx │ │ │ ├── Selects │ │ │ │ ├── PlaylistSelect.tsx │ │ │ │ └── RankSelect.tsx │ │ │ ├── SideBar.tsx │ │ │ ├── Tag │ │ │ │ ├── CreateTagDialog.tsx │ │ │ │ ├── ReplayTagDisplay.tsx │ │ │ │ ├── TagDialog.tsx │ │ │ │ ├── TagDialogWrapper.tsx │ │ │ │ ├── TagPageListItem.tsx │ │ │ │ └── UserTagDisplay.tsx │ │ │ └── Upload │ │ │ │ ├── AddTagPrivateKeyDialog..tsx │ │ │ │ ├── BakkesModAd.tsx │ │ │ │ ├── PreviousUploads.tsx │ │ │ │ ├── StatusUtils.ts │ │ │ │ ├── UploadContainedButton.tsx │ │ │ │ ├── UploadDialog.tsx │ │ │ │ ├── UploadDialogWrapper.tsx │ │ │ │ ├── UploadDropzone.tsx │ │ │ │ ├── UploadFloatingButton.tsx │ │ │ │ ├── UploadForm.tsx │ │ │ │ ├── UploadTabs.tsx │ │ │ │ └── UploadTags.tsx │ │ └── Training │ │ │ ├── CreatePackDialog.tsx │ │ │ ├── TrainingPackDisplayRow.tsx │ │ │ └── TrainingPackResultDisplay.tsx │ ├── Contexts │ │ └── ThemeContext.ts │ ├── Globals.ts │ ├── Models │ │ ├── Admin │ │ │ └── Admin.d.ts │ │ ├── ChartData.ts │ │ ├── ItemStats.d.ts │ │ ├── Player │ │ │ ├── MatchHistory.ts │ │ │ ├── PlayStyle.ts │ │ │ ├── Player.d.ts │ │ │ ├── PlayerStats.d.ts │ │ │ ├── TrainingPack.d.ts │ │ │ └── index.ts │ │ ├── Replay │ │ │ ├── Groups.d.ts │ │ │ ├── KickoffData.d.ts │ │ │ ├── PredictedRank.d.ts │ │ │ ├── Replay.ts │ │ │ └── ReplayPlayer.d.ts │ │ ├── ReplaysSearchQueryParams.ts │ │ ├── index.ts │ │ └── types │ │ │ ├── Error.d.ts │ │ │ ├── GlobalStatsData.d.ts │ │ │ ├── Homepage.d.ts │ │ │ ├── Leaderboards.d.ts │ │ │ ├── LoggedInUser.d.ts │ │ │ ├── QueueLengths.d.ts │ │ │ ├── Tag.d.ts │ │ │ ├── UploadStatus.d.ts │ │ │ └── VisibilityResponse.d.ts │ ├── Redux │ │ ├── index.ts │ │ ├── loggedInUser │ │ │ ├── actions.ts │ │ │ └── reducer.ts │ │ ├── notifications │ │ │ ├── actions.ts │ │ │ └── reducer.ts │ │ └── tags │ │ │ ├── actions.ts │ │ │ └── reducer.ts │ ├── Requests │ │ ├── Config.ts │ │ ├── Documentation.ts │ │ ├── Global.ts │ │ ├── Home.ts │ │ ├── Mock.ts │ │ ├── Player │ │ │ ├── getMatchHistory.ts │ │ │ ├── getPlayStyle.ts │ │ │ ├── getPlayer.ts │ │ │ ├── getProgression.ts │ │ │ ├── getRanks.ts │ │ │ ├── getStats.ts │ │ │ └── resolvePlayerNameOrId.ts │ │ ├── Replay.ts │ │ ├── Tag.ts │ │ └── Utils.ts │ ├── Theme.tsx │ ├── Utils │ │ ├── Chart.ts │ │ ├── Color.ts │ │ ├── CopyToClipboard │ │ │ ├── clipboard.d.ts │ │ │ └── clipboard.js │ │ ├── Playlists.ts │ │ ├── String.ts │ │ └── types │ │ │ └── bad-words.d.ts │ ├── WrappedApp.test.tsx │ ├── WrappedApp.tsx │ ├── apiHandler │ │ └── apiHandler.ts │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── registerServiceWorker.ts │ └── test │ │ ├── App.test.tsx │ │ ├── CodeSplitComponent.test.tsx │ │ ├── UserJourney1 │ │ ├── Journey1.test.tsx │ │ └── mocks.ts │ │ └── mocks.ts ├── tsconfig.json ├── tsconfig.prod.json ├── tsconfig.test.json ├── tslint.json └── tslint.test.json └── win_run.bat /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = backend 4 | 5 | [report] 6 | exclude_lines = 7 | if self.debug: 8 | pragma: no cover 9 | raise NotImplementedError 10 | if __name__ == .__main__.: 11 | omit = 12 | tests/* 13 | loader.py 14 | imports_test.py 15 | helpers/* 16 | redis/* 17 | backend/tasks/training_packs/parsing/* -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .gitignore -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: calculated 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | custom: # Replace with a single custom sponsorship URL 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | __pycache__/ 3 | .pytest_cache 4 | *.py[cod] 5 | *$py.class 6 | *.so 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | MANIFEST 24 | *.manifest 25 | *.spec 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | htmlcov/ 29 | .tox/ 30 | .coverage 31 | .coverage.* 32 | .cache 33 | nosetests.xml 34 | coverage.xml 35 | *.cover 36 | .gitmodules 37 | .hypothesis/ 38 | *.mo 39 | *.pot 40 | *.log 41 | .static_storage/ 42 | .media/ 43 | local_settings.py 44 | instance/ 45 | .webassets-cache 46 | .scrapy 47 | docs/_build/ 48 | target/ 49 | .ipynb_checkpoints 50 | .python-version 51 | celerybeat-schedule 52 | *.sage.py 53 | .env 54 | .venv 55 | env/ 56 | venv/ 57 | ENV/ 58 | env.bak/ 59 | venv.bak/ 60 | .spyderproject 61 | .spyproject 62 | .ropeproject 63 | /site 64 | .mypy_cache/ 65 | replays/ 66 | .idea/ 67 | .vscode 68 | *.iws 69 | out/ 70 | .idea_modules/ 71 | atlassian-ide-plugin.xml 72 | .idea/replstate.xml 73 | com_crashlytics_export_strings.xml 74 | crashlytics.properties 75 | crashlytics-build.properties 76 | fabric.properties 77 | /config.py 78 | /config.cfg 79 | config.cfg 80 | .DS_Store 81 | *.iml 82 | data/postgres/ 83 | 84 | parsed/ 85 | rlreplays/ 86 | privkey.pem 87 | cert.pem 88 | localhost* 89 | 90 | *.rdb 91 | 92 | # dependencies 93 | /node_modules 94 | 95 | # testing 96 | /coverage 97 | 98 | # production 99 | /build 100 | 101 | # misc 102 | .DS_Store 103 | .env.local 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | 108 | npm-debug.log* 109 | yarn-debug.log* 110 | yarn-error.log* 111 | node_modules 112 | creds.json 113 | test_data 114 | .idea 115 | alembic.ini 116 | alembic 117 | *.py___jb_tmp___ 118 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6 2 | 3 | WORKDIR /app 4 | 5 | 6 | ADD requirements.txt . 7 | 8 | RUN apt-get update && apt-get install -y \ 9 | gcc \ 10 | libfreetype6-dev \ 11 | libpng-dev \ 12 | libpq-dev \ 13 | && rm -rf /var/lib/apt/lists/* 14 | RUN pip install --no-cache-dir -r requirements.txt 15 | 16 | RUN wget https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh \ 17 | && chmod +x ./wait-for-it.sh \ 18 | && mv wait-for-it.sh /usr/bin/wait-for-it 19 | 20 | # Install Dockerize 21 | ENV DOCKERIZE_VERSION v0.6.1 22 | ENV DOCKERIZE_FILE dockerize-linux-amd64-${DOCKERIZE_VERSION}.tar.gz 23 | RUN wget https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/${DOCKERIZE_FILE} \ 24 | && tar -C /usr/local/bin -xzvf ${DOCKERIZE_FILE} \ 25 | && rm ${DOCKERIZE_FILE} 26 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2017-2018 Saltie Group 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /RLBotServer.py: -------------------------------------------------------------------------------- 1 | app = None 2 | 3 | 4 | def start_app(): 5 | from backend.initial_setup import CalculatedServer 6 | global app 7 | server = CalculatedServer() 8 | app = server.app 9 | return server 10 | 11 | 12 | def start_server(): 13 | server = start_app() 14 | server.app.run(host='0.0.0.0', port=8000) 15 | 16 | 17 | if __name__ == '__main__': 18 | start_server() 19 | -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/__init__.py -------------------------------------------------------------------------------- /backend/blueprints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/blueprints/__init__.py -------------------------------------------------------------------------------- /backend/blueprints/shared_renders.py: -------------------------------------------------------------------------------- 1 | from flask import render_template 2 | 3 | 4 | def render_with_session(template, session, **kwargs): 5 | """ 6 | Render a template with session objects. Required if there are objs from objects.py being used in the template. Closes session after rendering. 7 | 8 | :param template: template to render 9 | :param session: session to use (and close afterwards) 10 | :param kwargs: extra arguments to be passed to render_template 11 | :return: response object 12 | """ 13 | response = render_template(template, **kwargs) 14 | session.close() 15 | return response 16 | -------------------------------------------------------------------------------- /backend/blueprints/spa_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/blueprints/spa_api/__init__.py -------------------------------------------------------------------------------- /backend/blueprints/spa_api/errors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/blueprints/spa_api/errors/__init__.py -------------------------------------------------------------------------------- /backend/blueprints/spa_api/service_layers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/blueprints/spa_api/service_layers/__init__.py -------------------------------------------------------------------------------- /backend/blueprints/spa_api/service_layers/global_stats.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from typing import List 4 | 5 | from flask import current_app 6 | 7 | from backend.utils.safe_flask_globals import get_redis 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class GlobalStatsMetadata: 13 | def __init__(self, name: str, field: str): 14 | self.name = name 15 | self.field = field 16 | 17 | 18 | class GlobalStatsGraphDataset: 19 | def __init__(self, name: str, keys: List[float], values: List[float]): 20 | self.name = name 21 | self.keys = keys 22 | self.values = values 23 | 24 | 25 | class GlobalStatsGraph: 26 | def __init__(self, name: str, datasets: List[GlobalStatsGraphDataset]): 27 | self.name = name 28 | self.data = [dataset.__dict__ for dataset in datasets] 29 | 30 | @staticmethod 31 | def create_by_playlist() -> List['GlobalStatsGraph']: 32 | r = get_redis() 33 | return json.loads(r.get('global_stats_by_playlist')) 34 | 35 | @staticmethod 36 | def create_by_rank() -> List['GlobalStatsGraph']: 37 | r = get_redis() 38 | return json.loads(r.get('global_stats_by_rank')) 39 | -------------------------------------------------------------------------------- /backend/blueprints/spa_api/service_layers/homepage/patreon.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | from bs4 import BeautifulSoup 5 | 6 | from backend.database.startup import lazy_get_redis 7 | 8 | try: 9 | from config import PATREON_PROGRESS_LOOKUP_CLASS 10 | except ImportError: 11 | PATREON_PROGRESS_LOOKUP_CLASS = "sc-gZMcBi fXaIeo" 12 | 13 | class PatreonProgress: 14 | def __init__(self, progress: str, total: str): 15 | self.progress = progress 16 | self.total = total 17 | 18 | @classmethod 19 | def create(cls): 20 | progress = cls.get_patreon_progress() 21 | return cls(progress[0], progress[1]) 22 | 23 | @staticmethod 24 | def get_patreon_progress(): 25 | if lazy_get_redis() is not None: 26 | r = lazy_get_redis() 27 | if r.get('patreon_progress'): 28 | return tuple(json.loads(r.get('patreon_progress'))) 29 | r = requests.get("https://patreon.com/calculated") 30 | bs = BeautifulSoup(r.text, "html.parser") 31 | progress = bs.find_all(class_=PATREON_PROGRESS_LOOKUP_CLASS)[0].text 32 | nums = [int(n[1:]) for n in progress.split(' of ')] 33 | if lazy_get_redis() is not None: 34 | r = lazy_get_redis() 35 | r.set('patreon_progress', json.dumps(nums), ex=60 * 60) 36 | return tuple(nums) 37 | 38 | 39 | if __name__ == '__main__': 40 | print(PatreonProgress.get_patreon_progress()) 41 | -------------------------------------------------------------------------------- /backend/blueprints/spa_api/service_layers/homepage/recent.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from sqlalchemy import desc 4 | 5 | from backend.blueprints.spa_api.service_layers.replay.replay import CompactReplay 6 | from backend.blueprints.spa_api.service_layers.utils import with_session 7 | from backend.database.objects import Game 8 | from backend.database.startup import lazy_get_redis 9 | 10 | 11 | class RecentReplays: 12 | def __init__(self, recent): 13 | self.recent = recent 14 | 15 | @classmethod 16 | def create(cls): 17 | recent = cls.get_recent_replays() 18 | return cls(recent) 19 | 20 | @staticmethod 21 | @with_session 22 | def get_recent_replays(session=None): 23 | r = lazy_get_redis() 24 | if r is not None and r.get('recent_replays') is not None: 25 | return json.loads(r.get('recent_replays')) 26 | replays = session.query(Game).order_by(desc(Game.upload_date))[:5] 27 | replays = [CompactReplay.create_from_game(r).__dict__ for r in replays] 28 | if r is not None: 29 | r.set('recent_replays', json.dumps(replays), ex=60 * 60) 30 | return replays 31 | 32 | 33 | if __name__ == '__main__': 34 | print(RecentReplays.get_recent_replays()) 35 | -------------------------------------------------------------------------------- /backend/blueprints/spa_api/service_layers/logged_in_user.py: -------------------------------------------------------------------------------- 1 | from flask import g 2 | 3 | from backend.blueprints.steam import get_steam_profile_or_random_response 4 | from backend.utils.checks import is_local_dev 5 | from backend.blueprints.spa_api.errors.errors import NotLoggedIn 6 | 7 | 8 | class LoggedInUser: 9 | def __init__(self, name: str, id_: str, avatar_link: str, admin: bool, alpha: bool, beta: bool): 10 | self.name = name 11 | self.id = id_ 12 | self.avatarLink = avatar_link 13 | self.admin = admin 14 | self.alpha = alpha 15 | self.beta = beta 16 | 17 | @staticmethod 18 | def create() -> 'LoggedInUser': 19 | if is_local_dev(): 20 | mock_steam_profile = get_steam_profile_or_random_response("TESTLOCALUSER")['response']['players'][0] 21 | name = mock_steam_profile['personaname'] 22 | id_ = mock_steam_profile['steamid'] 23 | avatar_link = mock_steam_profile['avatarfull'] 24 | return LoggedInUser(name, id_, avatar_link, True, True, True) 25 | if g.user is None: 26 | raise NotLoggedIn() 27 | return LoggedInUser(g.user.platformname, g.user.platformid, g.user.avatar, g.admin, g.admin or g.alpha, 28 | g.admin or g.alpha or g.beta) 29 | -------------------------------------------------------------------------------- /backend/blueprints/spa_api/service_layers/player/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/blueprints/spa_api/service_layers/player/__init__.py -------------------------------------------------------------------------------- /backend/blueprints/spa_api/service_layers/queue_status.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from backend.tasks.utils import get_queue_length 4 | 5 | 6 | class QueueMetadata: 7 | def __init__(self, name: str, priority: int): 8 | self.name = name 9 | self.priority = priority 10 | 11 | 12 | queues = [ 13 | QueueMetadata('Internal', 0), 14 | QueueMetadata('Priority', 3), 15 | QueueMetadata('Public', 6), 16 | QueueMetadata('Reparsing', 9), 17 | ] 18 | 19 | 20 | class QueueStatus: 21 | def __init__(self, name: str, priority: int, count: int): 22 | self.name = name 23 | self.priority = priority 24 | self.count = count 25 | 26 | @staticmethod 27 | def create_for_queues() -> List['QueueStatus']: 28 | counts: List[int] = get_queue_length() 29 | return [ 30 | QueueStatus(queue_metadata.name, queue_metadata.priority, count) 31 | for queue_metadata, count in zip(queues, counts) 32 | ] 33 | -------------------------------------------------------------------------------- /backend/blueprints/spa_api/service_layers/replay/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/blueprints/spa_api/service_layers/replay/__init__.py -------------------------------------------------------------------------------- /backend/blueprints/spa_api/service_layers/replay/basic_stats.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from backend.database.wrapper.chart.player_chart_metadata import player_group_stats_metadata 4 | from backend.database.wrapper.chart.stat_point import OutputChartData 5 | from backend.database.wrapper.chart.team_chart_metadata import team_stats_metadata 6 | from backend.database.wrapper.stats.chart_stats_wrapper import ChartStatsWrapper 7 | from backend.database.wrapper.stats.shared_stats_wrapper import SharedStatsWrapper 8 | 9 | wrapper = ChartStatsWrapper() 10 | 11 | 12 | class PlayerStatsChart: 13 | @staticmethod 14 | def create_from_id(id_: str) -> List[OutputChartData]: 15 | wrapped_player_games = wrapper.get_chart_stats_for_player(id_) 16 | protobuf_stats = wrapper.get_protobuf_stats(id_) 17 | all_basic_stats = SharedStatsWrapper.merge_stats(wrapped_player_games, protobuf_stats) 18 | return wrapper.wrap_chart_stats(all_basic_stats, player_group_stats_metadata) 19 | 20 | 21 | class TeamStatsChart: 22 | @staticmethod 23 | def create_from_id(id_: str) -> List[OutputChartData]: 24 | return wrapper.wrap_chart_stats(wrapper.get_chart_stats_for_team(id_), team_stats_metadata) 25 | -------------------------------------------------------------------------------- /backend/blueprints/spa_api/service_layers/replay/enums.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class HeatMapType(str, enum.Enum): 5 | POSITIONING = "Positioning" 6 | HITS = "Hits" 7 | SHOTS = "Shots" 8 | BOOST = "Boost" 9 | BOOST_COLLECT = "Boost Collect" 10 | BOOST_SPEED = "Boost Speed" 11 | SLOW_SPEED = "Slow Speed" 12 | -------------------------------------------------------------------------------- /backend/blueprints/spa_api/service_layers/stat.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from backend.blueprints.spa_api.service_layers.player.player_profile_stats import player_stat_wrapper 3 | 4 | 5 | class DataPoint: 6 | def __init__(self, name: str, average: float, std_dev: float = None): 7 | self.name = name 8 | self.average = average 9 | if std_dev is not None: 10 | self.stdDev = std_dev 11 | 12 | 13 | class ProgressionDataPoint: 14 | def __init__(self, date: str, data_points: List[DataPoint], count=None): 15 | self.date = date 16 | self.dataPoints = [data_point.__dict__ for data_point in data_points] 17 | self.replayCount = count 18 | 19 | 20 | class PlayerDataPoint: 21 | def __init__(self, name: str, data_points: List[DataPoint]): 22 | self.name = name 23 | self.dataPoints = [data_point.__dict__ for data_point in data_points] 24 | 25 | 26 | def get_explanations(): 27 | return {stat.field_rename if stat.field_rename is not None else stat.field_name: stat.__dict__ for name, stat in 28 | player_stat_wrapper.player_stats.stat_explanation_map.items()} 29 | -------------------------------------------------------------------------------- /backend/blueprints/spa_api/service_layers/utils.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import List, Dict 3 | 4 | from carball.generated.api.game_pb2 import Game 5 | from carball.generated.api.player_pb2 import Player 6 | 7 | from backend.database.objects import PlayerGame 8 | from backend.database.startup import get_current_session 9 | 10 | 11 | def with_session(decorated_function): 12 | @wraps(decorated_function) 13 | def func(*args, **kwargs): 14 | if 'session' in kwargs and kwargs['session'] is not None: 15 | return decorated_function(*args, **kwargs) 16 | session = get_current_session() 17 | try: 18 | kwargs['session'] = session 19 | result = decorated_function(*args, **kwargs) 20 | finally: 21 | session.close() 22 | return result 23 | return func 24 | 25 | 26 | def sort_player_games_by_team_then_id(player_games: List[PlayerGame]) -> List[PlayerGame]: 27 | def get_id(player_game: PlayerGame): 28 | return player_game.id 29 | 30 | def get_is_orange(player_game: PlayerGame): 31 | return player_game.is_orange 32 | 33 | return sorted(sorted(player_games, key=get_id), key=get_is_orange) 34 | 35 | 36 | def create_player_map(proto_game: Game) -> Dict[str, Player]: 37 | # create player metadata 38 | player_map = dict() 39 | for player_proto in proto_game.players: 40 | player_map[str(player_proto.id.id)] = player_proto 41 | return player_map 42 | 43 | 44 | def get_protobuf_player_name_by_id(player_id: str, protobuf_game: Game) -> Player: 45 | for player in protobuf_game.players: 46 | if player.id.id == player_id: 47 | return player 48 | -------------------------------------------------------------------------------- /backend/blueprints/spa_api/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/blueprints/spa_api/utils/__init__.py -------------------------------------------------------------------------------- /backend/blueprints/spa_api/utils/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from backend.blueprints.spa_api.utils.query_params_handler import create_validation_for_query_params, get_query_params 4 | from backend.blueprints.spa_api.errors.errors import NotLoggedIn 5 | from backend.utils.safe_flask_globals import UserManager, get_request 6 | 7 | 8 | def require_user(decorated_function): 9 | @wraps(decorated_function) 10 | def wrapper_require_user(*args, **kwargs): 11 | if UserManager.get_current_user() is None and 'internal_user' not in kwargs: 12 | raise NotLoggedIn() 13 | return decorated_function(*args, **kwargs) 14 | return wrapper_require_user 15 | 16 | 17 | class with_query_params(object): 18 | def __init__(self, accepted_query_params=None, provided_params=None): 19 | if accepted_query_params is None: 20 | raise Exception("Need query params") 21 | self.provided_params = provided_params 22 | self.accepted_query_params = accepted_query_params 23 | self.validation_func = create_validation_for_query_params(accepted_query_params, self.provided_params) 24 | 25 | def __call__(self, decorated_function): 26 | @wraps(decorated_function) 27 | def decorator(*args, **kwargs): 28 | query_params = get_query_params(self.accepted_query_params, get_request()) 29 | if query_params is None: 30 | return decorated_function(*args, **kwargs) 31 | validation = self.validation_func(query_params) 32 | if validation is not None: 33 | raise validation 34 | kwargs['query_params'] = query_params 35 | return decorated_function(*args, **kwargs) 36 | return decorator 37 | -------------------------------------------------------------------------------- /backend/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/data/__init__.py -------------------------------------------------------------------------------- /backend/data/constants/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/data/constants/__init__.py -------------------------------------------------------------------------------- /backend/data/constants/car.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from backend.utils.rlgarage_handler import RLGarageAPI 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | def get_car(index: int) -> str: 9 | try: 10 | return RLGarageAPI().get_item(index)['name'] 11 | except KeyError: 12 | logger.warning(f"Could not find car: {index}.") 13 | return "Unknown" 14 | except: 15 | logger.warning(f"Error getting car for index {index}") 16 | return "Unknown" 17 | -------------------------------------------------------------------------------- /backend/data/constants/playlist.py: -------------------------------------------------------------------------------- 1 | playlists = { 2 | 1: 'Duels (U)', 3 | 2: 'Doubles (U)', 4 | 3: 'Standard (U)', 5 | 4: 'Chaos (U)', 6 | 6: 'Custom', 7 | 8: 'Offline', 8 | 10: 'Duels', 9 | 11: 'Doubles', 10 | 12: 'Solo Standard', 11 | 13: 'Standard', 12 | 15: 'Snow Day (U)', 13 | 16: 'Rocket Labs', 14 | 17: 'Hoops (U)', 15 | 18: 'Rumble (U)', 16 | 23: 'Dropshot (U)', 17 | 25: 'Anniversary', 18 | 27: 'Hoops', 19 | 28: 'Rumble', 20 | 29: 'Dropshot', 21 | 30: 'Snow Day' 22 | } 23 | 24 | 25 | def get_playlist(playlist_id: int, teamsize: int) -> str: 26 | if playlist_id in playlists: 27 | return playlists[playlist_id] 28 | return f"Unknown playlist: {teamsize}'s" 29 | -------------------------------------------------------------------------------- /backend/database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/database/__init__.py -------------------------------------------------------------------------------- /backend/database/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/database/utils/__init__.py -------------------------------------------------------------------------------- /backend/database/utils/debug_db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.dialects.postgresql import dialect as DefaultDialect, ARRAY 2 | from sqlalchemy.sql.sqltypes import String, DateTime, NullType 3 | 4 | # python2/3 compatible. 5 | PY3 = str is not bytes 6 | text = str if PY3 else unicode 7 | int_type = int if PY3 else (int, long) 8 | str_type = str if PY3 else (str, unicode) 9 | 10 | 11 | class StringLiteral(String): 12 | """Teach SA how to literalize various things.""" 13 | def literal_processor(self, dialect): 14 | super_processor = super(StringLiteral, self).literal_processor(dialect) 15 | 16 | def process(value): 17 | if isinstance(value, int_type): 18 | return text(value) 19 | if not isinstance(value, str_type): 20 | value = text(value) 21 | 22 | if isinstance(value, list): 23 | return text("{" + ", ".join(value) + "}") 24 | result = super_processor(value) 25 | 26 | if isinstance(result, bytes): 27 | result = result.decode(dialect.encoding) 28 | return result 29 | return process 30 | 31 | 32 | class LiteralDialect(DefaultDialect): 33 | colspecs = { 34 | # prevent various encoding explosions 35 | String: StringLiteral, 36 | # teach SA about how to literalize a datetime 37 | DateTime: StringLiteral, 38 | # don't format py2 long integers to NULL 39 | NullType: StringLiteral, 40 | ARRAY: StringLiteral 41 | } 42 | 43 | 44 | def literalquery(statement): 45 | """NOTE: This is entirely insecure. DO NOT execute the resulting strings.""" 46 | import sqlalchemy.orm 47 | if isinstance(statement, sqlalchemy.orm.Query): 48 | statement = statement.statement 49 | return statement.compile( 50 | dialect=LiteralDialect(), 51 | compile_kwargs={'literal_binds': True}, 52 | ).string 53 | -------------------------------------------------------------------------------- /backend/database/wrapper/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/database/wrapper/__init__.py -------------------------------------------------------------------------------- /backend/database/wrapper/chart/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/database/wrapper/chart/__init__.py -------------------------------------------------------------------------------- /backend/database/wrapper/chart/chart_data.py: -------------------------------------------------------------------------------- 1 | import io 2 | from enum import Enum, auto 3 | from typing import List, Optional 4 | 5 | import pandas as pd 6 | 7 | 8 | class ChartDataPoint: 9 | def __init__(self, name: str, value: float, average: Optional[float] = None): 10 | self.name = name 11 | self.value = value 12 | if average is not None: 13 | self.average = average 14 | 15 | 16 | class ChartData: 17 | def __init__(self, title: str, chart_data_points: List[ChartDataPoint]): 18 | self.title = title 19 | self.chartDataPoints = [chart_data_point.__dict__ for chart_data_point in chart_data_points] 20 | 21 | 22 | class ChartType(Enum): 23 | radar = auto() 24 | bar = auto() 25 | pie = auto() 26 | 27 | 28 | class ChartSubcatagory(Enum): 29 | pass 30 | 31 | 32 | class ChartStatsMetadata: 33 | def __init__(self, stat_name: str, type_: ChartType, subcategory: ChartSubcatagory, is_protobuf=False): 34 | self.is_protobuf = is_protobuf 35 | self.stat_name = stat_name 36 | self.type = type_.name 37 | self.subcategory = subcategory.name.replace('_', ' ') 38 | 39 | 40 | def convert_to_csv(chart_data, filename='test.csv'): 41 | from flask import send_file 42 | 43 | mem = io.StringIO() 44 | df = pd.DataFrame(columns=["Player"] + [c.title for c in chart_data]) 45 | df["Player"] = pd.Series([c["name"] for c in chart_data[0].chartDataPoints]) 46 | for data in chart_data: 47 | df[data.title] = pd.Series([c["value"] for c in data.chartDataPoints]) 48 | df.to_csv(mem) 49 | csv = io.BytesIO() 50 | csv.write(mem.getvalue().encode()) 51 | csv.seek(0) 52 | mem.close() 53 | return send_file( 54 | csv, 55 | as_attachment=True, 56 | attachment_filename=filename, 57 | mimetype='text/csv' 58 | ) 59 | -------------------------------------------------------------------------------- /backend/database/wrapper/chart/stat_point.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from backend.database.wrapper.chart.chart_data import ChartDataPoint, ChartData 4 | 5 | 6 | class StatDataPoint(ChartDataPoint): 7 | def __init__(self, name: str, value: float, is_orange: bool): 8 | """ 9 | :param name: The name of the player 10 | :param value: The value of the stat 11 | :param is_orange: If the stat is for the orange team. 12 | """ 13 | super().__init__(name, value) 14 | self.isOrange = is_orange 15 | 16 | 17 | class DatabaseObjectDataPoint: 18 | def __init__(self, id: int, name: str, is_orange: bool, stats: dict): 19 | """ 20 | :param id: The Id of the player 21 | :param name: The name of the player 22 | :param is_orange: If the player is on the orange team 23 | :param stats: A dictionary of all stats associated with this player. 24 | """ 25 | self.id = id 26 | self.name = name 27 | self.is_orange = is_orange 28 | self.stats = stats 29 | 30 | 31 | class OutputChartData(ChartData): 32 | def __init__(self, title: str, chart_data_points: List[StatDataPoint], type_: str, subcategory: str): 33 | """ 34 | :param title: The title of the chart. 35 | :param chart_data_points: The 36 | :param type_: 37 | :param subcategory: 38 | """ 39 | super().__init__(title, chart_data_points) 40 | self.type = type_ 41 | self.subcategory = subcategory 42 | -------------------------------------------------------------------------------- /backend/database/wrapper/chart/team_chart_metadata.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from backend.database.wrapper.chart.chart_data import ChartSubcatagory, ChartStatsMetadata, ChartType 4 | 5 | 6 | class TeamStatSubcategory(ChartSubcatagory): 7 | CenterOfMass = auto() 8 | Positioning = auto() 9 | 10 | 11 | SubCat = TeamStatSubcategory 12 | Metadata = ChartStatsMetadata 13 | 14 | 15 | team_stats_metadata = [ 16 | # Positioning 17 | Metadata('time_high_in_air', ChartType.bar, SubCat.Positioning), 18 | Metadata('time_low_in_air', ChartType.bar, SubCat.Positioning), 19 | Metadata('time_on_ground', ChartType.bar, SubCat.Positioning), 20 | Metadata('time_in_defending_third', ChartType.bar, SubCat.Positioning), 21 | Metadata('time_in_neutral_third', ChartType.bar, SubCat.Positioning), 22 | Metadata('time_in_attacking_third', ChartType.bar, SubCat.Positioning), 23 | Metadata('time_in_defending_half', ChartType.bar, SubCat.Positioning), 24 | Metadata('time_in_attacking_half', ChartType.bar, SubCat.Positioning), 25 | Metadata('time_near_wall', ChartType.bar, SubCat.Positioning), 26 | Metadata('time_in_corner', ChartType.bar, SubCat.Positioning), 27 | 28 | # Center of mass 29 | Metadata('average_distance_from_center', ChartType.bar, SubCat.CenterOfMass), 30 | Metadata('average_max_distance_from_center', ChartType.bar, SubCat.CenterOfMass), 31 | Metadata('time_clumped', ChartType.bar, SubCat.CenterOfMass), 32 | Metadata('time_boondocks', ChartType.bar, SubCat.CenterOfMass), 33 | ] 34 | -------------------------------------------------------------------------------- /backend/database/wrapper/rank_wrapper.py: -------------------------------------------------------------------------------- 1 | from backend.database.objects import Playlist 2 | 3 | # Used to map playlists to a certain ranked playlist 4 | # In case of unranked playlists, this is where you would map them to their ranked counterparts 5 | # This is used to compare people in unranked playlists to each other 6 | # To use a playlist as its own rank, use `None`. 7 | rank_mapping = { 8 | Playlist.UNRANKED_DUELS: Playlist.RANKED_DUELS, 9 | Playlist.UNRANKED_DOUBLES: Playlist.RANKED_DOUBLES, 10 | Playlist.UNRANKED_STANDARD: Playlist.RANKED_STANDARD, 11 | Playlist.UNRANKED_CHAOS: Playlist.RANKED_STANDARD, 12 | 13 | # Ranked 14 | Playlist.RANKED_DUELS: None, 15 | Playlist.RANKED_DOUBLES: None, 16 | Playlist.RANKED_STANDARD: None, 17 | 18 | # Ranked Other modes 19 | Playlist.RANKED_HOOPS: None, 20 | Playlist.RANKED_RUMBLE: None, 21 | Playlist.RANKED_DROPSHOT: None, 22 | Playlist.RANKED_SNOW_DAY: None 23 | } 24 | real_rank_mapping = {} 25 | for k, v in rank_mapping.items(): 26 | if v is not None: 27 | real_rank_mapping[k.value] = v.value 28 | else: 29 | real_rank_mapping[k.value] = k.value 30 | 31 | 32 | def get_rank_tier(rank, playlist=13): 33 | if rank is not None: 34 | try: 35 | corresponding_rank = real_rank_mapping[int(playlist)] 36 | return rank[str(corresponding_rank)]['tier'] 37 | except KeyError: 38 | return rank[str(13)]['tier'] 39 | else: 40 | return 0 41 | 42 | 43 | def get_rank_obj_by_mapping(rank, playlist=13): 44 | try: 45 | return rank[str(real_rank_mapping[int(playlist)])] 46 | except KeyError: 47 | return rank['13'] 48 | -------------------------------------------------------------------------------- /backend/database/wrapper/stats/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/database/wrapper/stats/__init__.py -------------------------------------------------------------------------------- /backend/database/wrapper/stats/creation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/database/wrapper/stats/creation/__init__.py -------------------------------------------------------------------------------- /backend/database/wrapper/stats/creation/replay_group_stat_creation.py: -------------------------------------------------------------------------------- 1 | from backend.database.wrapper.stats.creation.shared_stat_creation import SharedStatCreation 2 | 3 | class ReplayGroupStatCreation(SharedStatCreation): 4 | def __init__(self): 5 | super().__init__() 6 | self.grouped_stat_total = [ 7 | 'aerial_efficiency', 8 | 'assists', 9 | 'average_hit_distance', 10 | 'average_speed', 11 | 'boost_ratio', 12 | 'collection_boost_efficiency', 13 | 'goals', 14 | 'saves', 15 | 'score', 16 | 'shots', 17 | 'shot_%', 18 | 'total_boost_efficiency', 19 | 'turnover_efficiency', 20 | 'used_boost_efficiency', 21 | 'useful/hits' 22 | ] 23 | self.grouped_stat_per_game = [ 24 | 'assists', 25 | 'goals', 26 | 'saves', 27 | 'score', 28 | 'shots' 29 | ] 30 | self.grouped_stat_per_minute = [ 31 | 'boost_usage', 32 | 'num_large_boosts', 33 | 'num_small_boosts', 34 | 'wasted_collection', 35 | 'wasted_usage' 36 | ] 37 | for stat_per_game in self.grouped_stat_per_game: 38 | assert stat_per_game not in self.grouped_stat_per_minute 39 | -------------------------------------------------------------------------------- /backend/database/wrapper/stats/creation/team_stat_creation.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from carball.generated.api.stats import team_stats_pb2 4 | 5 | from backend.database.objects import TeamStat 6 | from backend.database.utils.dynamic_field_manager import ProtoFieldResult, create_and_filter_proto_field 7 | from backend.database.wrapper.field_wrapper import get_explanations 8 | from backend.database.wrapper.stats.creation.shared_stat_creation import SharedStatCreation 9 | 10 | 11 | class TeamStatCreation(SharedStatCreation): 12 | 13 | def __init__(self): 14 | super().__init__() 15 | self.dynamic_field_list = self.create_dynamic_fields() 16 | self.stat_explanation_list, self.stat_explanation_map = get_explanations(self.dynamic_field_list) 17 | self.stat_list = self.create_stats_field_list(self.dynamic_field_list, self.stat_explanation_map, TeamStat) 18 | self.stats_query, self.std_query, self.individual_query = self.get_stats_query(self.stat_list) 19 | 20 | @staticmethod 21 | def create_dynamic_fields() -> List[ProtoFieldResult]: 22 | field_list = create_and_filter_proto_field(proto_message=team_stats_pb2.TeamStats, 23 | blacklist_field_names=[], 24 | blacklist_message_types=[], 25 | db_object=TeamStat) 26 | return field_list 27 | -------------------------------------------------------------------------------- /backend/server_constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | """ 3 | File for placement server constants. 4 | Can not depend on any other file in this project only python built ins 5 | 6 | """ 7 | SERVER_PERMISSION_GROUPS = ['admin', 'alpha', 'beta'] 8 | 9 | BASE_FOLDER = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 10 | CODE_FOLDER = os.path.dirname(os.path.realpath(__file__)) 11 | UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'replays') 12 | UPLOAD_RATE_LIMIT_MINUTES = 4.5 # TODO: Make use of this. 13 | -------------------------------------------------------------------------------- /backend/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/tasks/__init__.py -------------------------------------------------------------------------------- /backend/tasks/celery.sh: -------------------------------------------------------------------------------- 1 | celery -A celery_tasks.celery worker 2 | -------------------------------------------------------------------------------- /backend/tasks/celery_worker.py: -------------------------------------------------------------------------------- 1 | import RLBotServer 2 | # use by the application worker 3 | from backend.tasks.celery_tasks import celery 4 | 5 | server = RLBotServer.start_app() 6 | app = server.app 7 | app.app_context().push() 8 | -------------------------------------------------------------------------------- /backend/tasks/celeryconfig.py: -------------------------------------------------------------------------------- 1 | enable_utc = True 2 | worker_max_tasks_per_child = 100 3 | 4 | broker_url = 'redis://localhost:6379/0' 5 | result_backend = 'redis://localhost:6379/0' 6 | broker_transport_options = {'fanout_prefix': True} 7 | task_always_eager = False 8 | 9 | # errors from a task that uses apply or is eager will pass up exceptions 10 | task_eager_propagates = True -------------------------------------------------------------------------------- /backend/tasks/middleware.py: -------------------------------------------------------------------------------- 1 | from celery import Task 2 | from werkzeug.wsgi import LimitedStream 3 | 4 | from backend.database.startup import lazy_startup 5 | 6 | 7 | class StreamConsumingMiddleware(object): 8 | 9 | def __init__(self, app): 10 | self.app = app 11 | 12 | def __call__(self, environ, start_response): 13 | stream = LimitedStream(environ['wsgi.input'], 14 | 512 * 1024 * 1024) 15 | environ['wsgi.input'] = stream 16 | app_iter = self.app(environ, start_response) 17 | try: 18 | stream.exhaust() 19 | for event in app_iter: 20 | yield event 21 | finally: 22 | if hasattr(app_iter, 'close'): 23 | app_iter.close() 24 | 25 | 26 | class DBTask(Task): 27 | _session = None 28 | 29 | # def after_return(self, *args, **kwargs): 30 | # if self._session is not None: 31 | # self._session.remove() 32 | 33 | @property 34 | def session(self): 35 | if self._session is None: 36 | self._session = lazy_startup() 37 | return self._session 38 | -------------------------------------------------------------------------------- /backend/tasks/task_creators.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /backend/tasks/training_packs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/tasks/training_packs/__init__.py -------------------------------------------------------------------------------- /backend/tasks/training_packs/packs/1ShotBeckwithDefault.Tem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/tasks/training_packs/packs/1ShotBeckwithDefault.Tem -------------------------------------------------------------------------------- /backend/tasks/training_packs/packs/1ShotBeckwithDefaultGoalie.Tem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/tasks/training_packs/packs/1ShotBeckwithDefaultGoalie.Tem -------------------------------------------------------------------------------- /backend/tasks/training_packs/parsing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/tasks/training_packs/parsing/__init__.py -------------------------------------------------------------------------------- /backend/tasks/training_packs/parsing/crc.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | try: 3 | from config import crcTables 4 | except (ImportError, ModuleNotFoundError): 5 | crcTables = [] 6 | 7 | 8 | def calc_crc(data, startPos, len, crc): 9 | crc = ctypes.c_uint32(~(crc.value)) 10 | 11 | for i in range(len): 12 | d = ctypes.c_ubyte(data[startPos + i]) 13 | index = d.value ^ crc.value >> 24 14 | crc = crc.value << 8 ^ crcTables[index] 15 | crc = ctypes.c_uint32(crc) 16 | 17 | crc = ~(crc.value) 18 | return ctypes.c_uint32(crc) 19 | 20 | 21 | def create_crc(data: bytes): 22 | crc = calc_crc(data, 0, len(data), ctypes.c_uint32(0xEFCBF201)) 23 | return crc.value -------------------------------------------------------------------------------- /backend/tasks/update.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | from backend.blueprints.spa_api.errors.errors import AuthorizationException 5 | from backend.server_constants import BASE_FOLDER 6 | from backend.utils.checks import is_admin, is_local_dev 7 | 8 | try: 9 | import config 10 | update_code = config.update_code 11 | if update_code is None: 12 | update_code = 1234 13 | except: 14 | update_code = 1234 15 | 16 | 17 | def update_self(code): 18 | if code != update_code or not is_admin(): 19 | raise AuthorizationException() 20 | 21 | script = os.path.join(BASE_FOLDER, 'update_run.sh') 22 | if update_code == 1234 or is_local_dev(): 23 | subprocess.call([script, 'test']) 24 | else: 25 | subprocess.call([script]) 26 | -------------------------------------------------------------------------------- /backend/tasks/utils.py: -------------------------------------------------------------------------------- 1 | from backend.database.startup import get_strict_redis 2 | 3 | PRIORITY_SEP = '\x06\x16' 4 | DEFAULT_PRIORITY_STEPS = [0, 3, 6, 9] 5 | 6 | 7 | def make_queue_name_for_pri(queue, pri): 8 | """Make a queue name for redis 9 | 10 | Celery uses PRIORITY_SEP to separate different priorities of tasks into 11 | different queues in Redis. Each queue-priority combination becomes a key in 12 | redis with names like: 13 | 14 | - batch1\x06\x163 <-- P3 queue named batch1 15 | 16 | There's more information about this in Github, but it doesn't look like it 17 | will change any time soon: 18 | 19 | - https://github.com/celery/kombu/issues/422 20 | 21 | In that ticket the code below, from the Flower project, is referenced: 22 | 23 | - https://github.com/mher/flower/blob/master/flower/utils/broker.py#L135 24 | 25 | :param queue: The name of the queue to make a name for. 26 | :param pri: The priority to make a name with. 27 | :return: A name for the queue-priority pair. 28 | """ 29 | if pri not in DEFAULT_PRIORITY_STEPS: 30 | raise ValueError('Priority not in priority steps') 31 | return '{0}{1}{2}'.format(*((queue, PRIORITY_SEP, pri) if pri else 32 | (queue, '', ''))) 33 | 34 | 35 | def get_queue_length(queue_name='celery'): 36 | """Get the number of tasks in a celery queue. 37 | 38 | :param queue_name: The name of the queue you want to inspect. 39 | :return: the number of items in the queue. 40 | """ 41 | priority_names = [make_queue_name_for_pri(queue_name, pri) for pri in 42 | DEFAULT_PRIORITY_STEPS] 43 | r = get_strict_redis() 44 | return [r.llen(x) for x in priority_names] 45 | -------------------------------------------------------------------------------- /backend/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/backend/utils/__init__.py -------------------------------------------------------------------------------- /backend/utils/safe_flask_globals.py: -------------------------------------------------------------------------------- 1 | """ 2 | Safely gets global values from flask in locations where flask values may not exist. 3 | """ 4 | import logging 5 | 6 | import redis 7 | 8 | from backend.database.objects import Player 9 | from backend.utils.logger import ErrorLogger 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class FakeRequest(object): 15 | path = "task_path" 16 | method = "backend" 17 | 18 | 19 | def get_request(): 20 | try: 21 | from flask import request 22 | return request 23 | except: 24 | return FakeRequest() 25 | 26 | 27 | def get_current_user_id(player_id=None) -> str: 28 | if player_id is not None: 29 | return player_id 30 | try: 31 | user = UserManager.get_current_user() 32 | if user is None: 33 | return "" 34 | return user.platformid 35 | except Exception as e: 36 | ErrorLogger.log_error(e) 37 | return "" 38 | 39 | 40 | class UserManager: 41 | @staticmethod 42 | def get_current_user() -> Player or None: 43 | """ 44 | Tries to get the current user. 45 | Returns None if there are problems. 46 | """ 47 | try: 48 | from flask import g 49 | return g.user 50 | except: 51 | return None 52 | 53 | 54 | def get_redis() -> redis.Redis: 55 | """ 56 | Tries to get redis. 57 | Does a fallback if redis is not able to be grabbed from flask. 58 | """ 59 | try: 60 | from flask import current_app 61 | return current_app.config['r'] 62 | except Exception as e: 63 | ErrorLogger.log_error(e) 64 | try: 65 | from backend.database.startup import lazy_get_redis 66 | return lazy_get_redis() 67 | except Exception as e: 68 | ErrorLogger.log_error(e) 69 | -------------------------------------------------------------------------------- /backend/utils/time_related.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | def hour_rounder(t: datetime.datetime): 5 | # Rounds to nearest hour by adding a timedelta hour if minute >= 30 6 | return (t.replace(second=0, microsecond=0, minute=0, hour=t.hour) 7 | + datetime.timedelta(hours=t.minute//30)) 8 | 9 | 10 | def convert_to_datetime(timestamp: str): 11 | return datetime.datetime.fromtimestamp(int(timestamp)) 12 | -------------------------------------------------------------------------------- /celery2.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | sudo -u postgres /home/postgres/venv3.6/bin/celery -A backend.tasks.celery_worker.celery worker --loglevel=INFO --concurrency=4 -n worker1@%h 3 | -------------------------------------------------------------------------------- /celerycron.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | sudo -u postgres /home/postgres/venv3.6/bin/celery -A backend.tasks.celery_worker.celery beat -l debug -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "70...100" 9 | 10 | status: 11 | status: 12 | project: 13 | default: 14 | threshold: 1 15 | server: 16 | threshold: 0.5 17 | paths: 18 | - backend/* 19 | - tests/server_tests/* 20 | - tests/integration_tests/* 21 | website: 22 | threshold: 0.5 23 | paths: 24 | - webapp/* 25 | patch: off 26 | changes: no 27 | 28 | parsers: 29 | gcov: 30 | branch_detection: 31 | conditional: yes 32 | loop: yes 33 | method: no 34 | macro: no 35 | 36 | comment: 37 | layout: "header, diff" 38 | behavior: default 39 | require_changes: no 40 | 41 | ingore: 42 | - "helpers/*" 43 | - "redis/*" 44 | - "loader.py" 45 | - "imports_test.py" 46 | - "**/training_packs/parsing/*" 47 | -------------------------------------------------------------------------------- /data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/data/__init__.py -------------------------------------------------------------------------------- /gunicorn_conf.py: -------------------------------------------------------------------------------- 1 | bind = '0.0.0.0:5000' 2 | workers = 9 3 | worker_connections = 1000 4 | keepalive = 5 5 | timeout = 60 6 | keyfile = 'keys/calculated2.key' 7 | certfile = 'keys/calculated_cert2.pem' 8 | ca_certs = 'keys/chain2.pem' 9 | 10 | 11 | def child_exit(server, worker): 12 | from prometheus_client import multiprocess 13 | multiprocess.mark_process_dead(worker.pid) 14 | -------------------------------------------------------------------------------- /helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/helpers/__init__.py -------------------------------------------------------------------------------- /helpers/clean_database.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | 4 | from backend.database.objects import Game 5 | from backend.database.startup import lazy_startup 6 | 7 | 8 | def clean_database(sess, ignored_parsed=False): 9 | s = sess() 10 | # delete games that don't exist in parsed form 11 | pkls = [os.path.basename(p).split('.')[0] for p in glob.glob('parsed/*.pkl')] 12 | print(pkls) 13 | games = s.query(Game).all() 14 | for game in games: 15 | if ignored_parsed or game.hash not in pkls: 16 | print('delete', game.hash) 17 | s.delete(game) 18 | s.commit() 19 | # delete duplicates 20 | current_hashes = [] 21 | games = s.query(Game)[::-1] 22 | for game in games: 23 | if game.hash not in current_hashes: 24 | current_hashes.append(game.hash) 25 | else: 26 | s.delete(game) 27 | s.commit() 28 | 29 | 30 | if __name__ == '__main__': 31 | session = lazy_startup() 32 | clean_database(session) 33 | -------------------------------------------------------------------------------- /helpers/convert_existing_replays.py: -------------------------------------------------------------------------------- 1 | import glob 2 | 3 | import os 4 | from sqlalchemy import create_engine, exists 5 | from sqlalchemy.orm import sessionmaker 6 | 7 | try: 8 | import config 9 | except ImportError: 10 | config = {'db_user': None, 'db_password': None } 11 | from backend.database.objects import DBObjectBase, User, Replay, Model 12 | 13 | connection_string = 'postgresql:///saltie'.format(config.db_user, config.db_password) 14 | print (connection_string) 15 | engine = create_engine(connection_string, echo=True) 16 | DBObjectBase.metadata.create_all(engine) 17 | Session = sessionmaker(bind=engine) 18 | 19 | session = session() 20 | 21 | for replay in glob.glob(os.path.join('replays', '*.gz')): 22 | base = os.path.basename(replay) 23 | uuid = base.split('_')[-1].split('.')[0] 24 | ip = base.split('_')[0] 25 | user = -1 26 | print (uuid, ip, user) 27 | if not session.query(exists().where(User.id == -1)).scalar(): 28 | u = User(id=-1, name='Undefined', password='') 29 | session.add(u) 30 | session.commit() 31 | 32 | if not session.query(exists().where(Model.model_hash == '0')).scalar(): 33 | u = Model(model_hash='0') 34 | session.add(u) 35 | session.commit() 36 | if not session.query(exists().where(Replay.uuid == uuid)).scalar(): 37 | r = Replay(uuid=uuid, ip=ip, user=user, model_hash='0', num_team0=1, num_players=1, is_eval=False) 38 | session.add(r) 39 | print('Added', uuid, ip, user) 40 | session.commit() 41 | -------------------------------------------------------------------------------- /helpers/gcp_reparse.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | import datetime 4 | 5 | import requests 6 | import sys 7 | 8 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 9 | from backend.database.objects import Game 10 | from backend.database.startup import lazy_startup 11 | import json 12 | 13 | try: 14 | import config 15 | 16 | GCP_URL = config.GCP_URL 17 | except: 18 | print('Not using GCP') 19 | GCP_URL = '' 20 | 21 | 22 | def main(s): 23 | sess = s() 24 | games = sess.query(Game.hash).all() 25 | with open(datetime.datetime.now().strftime('%H-%m-%s') + '.json', 'w') as f: 26 | json.dump(list(games), f) 27 | for game in games: 28 | path = os.path.abspath(os.path.join('data', 'rlreplays', game[0] + '.replay')) 29 | try: 30 | with open(path, 'rb') as f: 31 | encoded_file = base64.b64encode(f.read()) 32 | except FileNotFoundError: 33 | continue 34 | try: 35 | r = requests.post(GCP_URL, data=encoded_file, timeout=0.5) 36 | except requests.exceptions.ReadTimeout as e: 37 | print('Delayed', game[0]) 38 | sess.close() 39 | 40 | 41 | if __name__ == '__main__': 42 | session = lazy_startup() 43 | main(session) 44 | -------------------------------------------------------------------------------- /helpers/insert_pickled_replays.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import logging 3 | import os 4 | import pickle 5 | import traceback 6 | from concurrent.futures import ThreadPoolExecutor 7 | from functools import partial 8 | 9 | from sqlalchemy.orm import sessionmaker, Session 10 | 11 | from backend.database.objects import Game 12 | from backend.database.startup import lazy_startup, lazy_get_redis 13 | from backend.database.utils.utils import convert_pickle_to_db, add_objs_to_db 14 | 15 | logger = logging.getLogger(__name__) 16 | session = lazy_startup() # type: sessionmaker 17 | 18 | r = lazy_get_redis() 19 | pickled_location = os.path.join(os.path.dirname(__file__), '..', 'data', 'parsed') 20 | pickles = glob.glob(os.path.join(pickled_location, '*.pkl')) 21 | 22 | s = session() 23 | games = s.query(Game.hash).all() 24 | 25 | 26 | def main(): 27 | with ThreadPoolExecutor() as executor: 28 | fn = partial(parse_pickle) 29 | executor.map(fn, pickles, timeout=120) 30 | 31 | 32 | def parse_pickle(p): 33 | s = session() # type: Session 34 | with open(p, 'rb') as f: 35 | try: 36 | g = pickle.load(f) # type: ReplayGame 37 | if g.api_game.id in games: 38 | print('skipping', g.api_game.id) 39 | return 40 | except EOFError: 41 | print ('what error') 42 | return 43 | try: 44 | game, player_games, players, teamstats = convert_pickle_to_db(g, offline_redis=r) 45 | add_objs_to_db(game, player_games, players, teamstats, s) 46 | except Exception as e: 47 | print(e) 48 | traceback.print_exc() 49 | s.commit() 50 | 51 | 52 | if __name__ == '__main__': 53 | main() 54 | -------------------------------------------------------------------------------- /helpers/migrate_database.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.append(os.path.abspath('.')) 4 | from backend.database.startup import lazy_startup 5 | 6 | 7 | def clear_database(sess): 8 | s = sess() 9 | s.execute('DROP TABLE GAMES CASCADE;') 10 | s.execute('DROP TABLE PLAYERGAMES CASCADE;') 11 | s.execute('DROP TABLE game_visibility CASCADE;') 12 | s.execute('DROP TABLE game_tags CASCADE;') 13 | s.execute('DROP TABLE tags CASCADE;') 14 | s.commit() 15 | s.close() 16 | 17 | 18 | if __name__ == '__main__': 19 | Session = lazy_startup() 20 | clear_database(Session) 21 | -------------------------------------------------------------------------------- /helpers/modify_existing_pickles.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import pickle 4 | 5 | from carball.analysis.saltie_game.saltie_game import SaltieGame 6 | from carball.analysis.stats.possession.turnovers import TurnoverStat 7 | 8 | 9 | def should_process_file(obj: SaltieGame): 10 | return 'turnovers' not in obj.stats 11 | 12 | 13 | def process_file(obj: SaltieGame): 14 | turnovers = TurnoverStat.get_player_turnovers(obj) 15 | obj.stats['turnovers'] = turnovers 16 | return obj 17 | 18 | 19 | if __name__ == '__main__': 20 | files = glob.glob(os.path.join(os.path.dirname(__file__), '..', 'data', 'parsed', '*.pkl')) 21 | print('Processing', len(files), 'files') 22 | for i, f in enumerate(files): 23 | print(i, '/', len(files)) 24 | with open(f, 'rb') as fo: 25 | try: 26 | pkl = pickle.load(fo) 27 | except EOFError: 28 | print('error opening', os.path.basename(f)) 29 | fo.close() 30 | continue 31 | if should_process_file(pkl): 32 | result = process_file(pkl) 33 | with open(f, 'wb') as out: 34 | pickle.dump(result, out) 35 | -------------------------------------------------------------------------------- /helpers/reparse_all_replays.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import datetime 4 | from backend.database.objects import Game 5 | from backend.database.startup import lazy_startup 6 | from backend.tasks.celery_tasks import parse_replay_task_low_priority 7 | import json 8 | 9 | 10 | def main(s): 11 | sess = s() 12 | games = sess.query(Game.hash).all() 13 | with open(datetime.datetime.now().strftime('%H-%m-%s') + '.json', 'w') as f: 14 | json.dump(list(games), f) 15 | games = glob.glob(os.path.abspath(os.path.join('..', 'data', 'rlreplays', '*.replay'))) 16 | for game in games: 17 | # path = os.path.abspath(os.path.join('..', 'data', 'rlreplays', game + '.replay')) 18 | # if os.path.isfile(path): 19 | # parse_replay_task_low_priority.delay(path) 20 | # print('Delayed ' + game) 21 | if os.path.isfile(game): 22 | parse_replay_task_low_priority.delay(game) 23 | print('Delayed ' + game) 24 | 25 | 26 | if __name__ == '__main__': 27 | session = lazy_startup() 28 | main(Session) 29 | -------------------------------------------------------------------------------- /helpers/storage_transfer.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import sys 4 | 5 | from google.api_core.exceptions import NotFound 6 | from google.cloud import storage 7 | 8 | sys.path.append(os.path.abspath('.')) 9 | 10 | from backend.database.objects import Game, ObjectLocation 11 | from backend.database.startup import lazy_startup 12 | 13 | storage_client = storage.Client.from_service_account_json('creds.json') 14 | replay_bucket_name = "calculatedgg-replays" 15 | df_bucket_name = "calculatedgg-parsed" 16 | 17 | buckets = [storage_client.get_bucket(name) for name in [replay_bucket_name, df_bucket_name]] 18 | cold_buckets = [storage_client.get_bucket(name + '-cold') for name in [replay_bucket_name, df_bucket_name]] 19 | session_factory = lazy_startup() 20 | 21 | _session = session_factory() 22 | 23 | q = _session.query(Game).filter(Game.upload_date < (datetime.datetime.now() - datetime.timedelta(days=30 * 1))) 24 | num_per_page = 50 25 | last_count = num_per_page 26 | page_num = 0 27 | while last_count == num_per_page: 28 | games = q[page_num * num_per_page:(page_num + 1) * num_per_page] 29 | last_count = len(games) 30 | 31 | for game in games: 32 | obl = _session.query(ObjectLocation).filter(ObjectLocation.hash == game.hash).first() 33 | if obl is not None and obl.archive: 34 | # we're already in the archive 35 | continue 36 | 37 | for bucket, cold_bucket in zip(buckets, cold_buckets): 38 | blob_name = game.hash + '.replay' + ('.gzip' if bucket.name.endswith('parsed') else '') 39 | blob = bucket.blob(blob_name) 40 | try: 41 | bucket.copy_blob(blob, cold_bucket) 42 | bucket.delete_blob(blob_name) 43 | except NotFound: 44 | print(f"Object {game.hash} not found.") 45 | 46 | obl = ObjectLocation(hash=game.hash, archive=True) 47 | _session.add(obl) 48 | _session.commit() 49 | page_num += 1 50 | _session.close() 51 | -------------------------------------------------------------------------------- /iptables.conf: -------------------------------------------------------------------------------- 1 | # Generated by iptables-save v1.6.0 on Thu Aug 16 18:09:04 2018 2 | *raw 3 | :PREROUTING ACCEPT [1829:126322] 4 | :OUTPUT ACCEPT [1647:157236] 5 | COMMIT 6 | # Completed on Thu Aug 16 18:09:04 2018 7 | # Generated by iptables-save v1.6.0 on Thu Aug 16 18:09:04 2018 8 | *nat 9 | :PREROUTING ACCEPT [37:1904] 10 | :INPUT ACCEPT [37:1904] 11 | :OUTPUT ACCEPT [0:0] 12 | :POSTROUTING ACCEPT [0:0] 13 | -A PREROUTING -i eth0 -p tcp -m tcp --dport 443 -j REDIRECT --to-ports 5000 14 | COMMIT 15 | # Completed on Thu Aug 16 18:09:04 2018 16 | # Generated by iptables-save v1.6.0 on Thu Aug 16 18:09:04 2018 17 | *mangle 18 | :PREROUTING ACCEPT [1829:126322] 19 | :INPUT ACCEPT [1829:126322] 20 | :FORWARD ACCEPT [0:0] 21 | :OUTPUT ACCEPT [1647:157236] 22 | :POSTROUTING ACCEPT [1647:157236] 23 | COMMIT 24 | # Completed on Thu Aug 16 18:09:04 2018 25 | # Generated by iptables-save v1.6.0 on Thu Aug 16 18:09:04 2018 26 | *filter 27 | :INPUT ACCEPT [1829:126322] 28 | :FORWARD ACCEPT [0:0] 29 | :OUTPUT ACCEPT [1647:157236] 30 | COMMIT 31 | # Completed on Thu Aug 16 18:09:04 2018 -------------------------------------------------------------------------------- /loader.py: -------------------------------------------------------------------------------- 1 | app = None 2 | 3 | 4 | def start_app(): 5 | from backend.initial_setup import CalculatedServer 6 | global app 7 | server = CalculatedServer() 8 | app = server.app 9 | return server 10 | 11 | 12 | def start_server(): 13 | server = start_app() 14 | server.app.run(host='0.0.0.0', port=8000) 15 | 16 | 17 | if __name__ == '__main__': 18 | start_server() 19 | else: 20 | start_app() 21 | -------------------------------------------------------------------------------- /mac_run.sh: -------------------------------------------------------------------------------- 1 | echo ************************** REDIS ************************* 2 | redis-server & 3 | sleep 2 4 | echo ************************** POSTGRES ************************* 5 | pg_ctl -D /usr/local/var/postgres start & 6 | sleep 2 7 | echo ************************** CELERY ************************* 8 | celery -A backend.tasks.celery_worker.celery worker --pool=solo -l info 9 | sleep 2 10 | echo ************************** FLOWER ************************* 11 | flower --port=5555 & 12 | sleep 2 13 | echo ************************** PYTHON SERVER ************************* 14 | python3 RLBotServer.py 15 | echo ************************** SUCCESSFULL ************************* 16 | -------------------------------------------------------------------------------- /mac_stop.sh: -------------------------------------------------------------------------------- 1 | redis-cli shutdown & 2 | pg_ctl -D /usr/local/var/postgres stop & 3 | pkill flower 4 | -------------------------------------------------------------------------------- /redis/EventLog.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/redis/EventLog.dll -------------------------------------------------------------------------------- /redis/redis-server.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/redis/redis-server.exe -------------------------------------------------------------------------------- /requirements-ml.txt: -------------------------------------------------------------------------------- 1 | torch -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | twine 2 | pytest 3 | requests 4 | pytest-cov>=2.4.0,<2.6 5 | coverage 6 | codecov 7 | pep8 8 | autopep8 9 | httmock 10 | alchemy-mock 11 | mock==2.0.0 12 | fakeredis 13 | git+https://github.com/tk0miya/testing.postgresql.git@master 14 | urllib3==1.25.2 15 | pytest-mock 16 | requests-mock 17 | responses 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | amqp==2.4.0 2 | arrow==0.12.1 3 | Babel==2.6.0 4 | beautifulsoup4==4.7.1 5 | billiard==3.6 6 | carball==0.6.36 7 | celery==4.3 8 | certifi==2018.8.24 9 | chardet==3.0.4 10 | click==6.7 11 | cycler==0.10.0 12 | Flask==1.0.2 13 | Flask-Celery-Helper==1.1.0 14 | Flask-Compress==1.4.0 15 | Flask-Cors==3.0.6 16 | Flask-Login==0.4.1 17 | flower==0.9.2 18 | gunicorn==19.9.0 19 | idna==2.7 20 | itsdangerous==0.24 21 | Jinja2==2.10.1 22 | kiwisolver==1.0.1 23 | kombu==4.4 24 | MarkupSafe==1.1 25 | matplotlib==2.2.3 26 | numpy==1.17.0 27 | pandas==0.24.2 28 | proj==0.1.0 29 | protobuf==3.15.0 30 | psycopg2==2.7.5 31 | pyparsing==2.2.0 32 | python-dateutil==2.7.3 33 | pytz==2018.5 34 | redis==3.2.0 35 | requests==2.22.0 36 | six==1.12.0 37 | SQLAlchemy==1.3.3 38 | tornado==5.1 39 | urllib3==1.25.2 40 | vine==1.3.0 41 | Werkzeug==0.15.3 42 | xlrd==1.1.0 43 | alembic==1.0.10 44 | prometheus_client 45 | pycryptodome 46 | flask-httpauth 47 | sqlalchemy-utils 48 | -------------------------------------------------------------------------------- /run-ssl.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | mkdir -p metrics 3 | rm metrics/* 4 | sudo -u postgres /home/postgres/venv3.6/bin/gunicorn -c gunicorn_conf.py loader:app --env prometheus_multiproc_dir="./metrics" --log-level debug -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | mkdir -p metrics 3 | rm metrics/* 4 | /home/postgres/venv/bin/gunicorn -w 4 -b 0.0.0.0:5000 loader:app --env prometheus_multiproc_dir="./metrics" 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.engine import create_engine 2 | from sqlalchemy.orm.session import Session 3 | 4 | 5 | 6 | def setup_module(): 7 | global transaction, connection, engine 8 | 9 | # Connect to the database and create the schema within a transaction 10 | engine = create_engine('postgresql:///yourdb') 11 | connection = engine.connect() 12 | transaction = connection.begin() 13 | 14 | # If you want to insert fixtures to the DB, do it here 15 | 16 | 17 | def teardown_module(): 18 | # Roll back the top level transaction and disconnect from the database 19 | transaction.rollback() 20 | connection.close() 21 | engine.dispose() 22 | 23 | 24 | class DatabaseTest: 25 | def setup(self): 26 | self.__transaction = connection.begin_nested() 27 | self.session = Session(connection) 28 | 29 | def teardown(self): 30 | self.session.close() 31 | self.__transaction.rollback() -------------------------------------------------------------------------------- /tests/integration_tests/__init__.py: -------------------------------------------------------------------------------- 1 | # https://pythonhosted.org/Flask-Testing/ -------------------------------------------------------------------------------- /tests/integration_tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from backend.database.startup import EngineStartup 4 | from tests.utils.database_utils import empty_database 5 | 6 | 7 | @pytest.fixture(autouse=True, scope="class") 8 | def kill_database(request): 9 | def kill_database(): 10 | engine, session = EngineStartup.login_db() 11 | empty_database(engine, session) 12 | 13 | request.addfinalizer(kill_database) 14 | -------------------------------------------------------------------------------- /tests/integration_tests/no_react/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/integration_tests/no_react/__init__.py -------------------------------------------------------------------------------- /tests/server_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/server_tests/__init__.py -------------------------------------------------------------------------------- /tests/server_tests/api_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/server_tests/api_tests/__init__.py -------------------------------------------------------------------------------- /tests/server_tests/api_tests/conftest.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | import pytest 4 | from werkzeug.wrappers import Response, Request 5 | 6 | 7 | class TestClient: 8 | def __init__(self, app): 9 | self.app = app 10 | 11 | def get_function_type(self, request: Request): 12 | if request.method == "POST": 13 | return self.app.post 14 | if request.method == "PUT": 15 | return self.app.put 16 | if request.method == "GET": 17 | return self.app.get 18 | 19 | def send(self, request: Request) -> Response: 20 | 21 | call = self.get_function_type(request) 22 | 23 | # build it 24 | prepped = request.prepare() 25 | 26 | # extract data needed for content 27 | content_data = list(prepped.headers._store.items()) 28 | try: 29 | content_length = content_data[0][1][1] 30 | except: 31 | content_length = 0 32 | if int(content_length) > 0: 33 | content_type = content_data[1][1][1] 34 | 35 | # add the body as an input stream and use the existing values 36 | return call(prepped.path_url, input_stream=io.BytesIO(prepped.body), 37 | content_length=content_length, content_type=content_type) 38 | else: 39 | return call(prepped.path_url) 40 | 41 | 42 | @pytest.fixture() 43 | def test_client(app) -> TestClient: 44 | return TestClient(app) 45 | -------------------------------------------------------------------------------- /tests/server_tests/api_tests/documentation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/server_tests/api_tests/documentation/__init__.py -------------------------------------------------------------------------------- /tests/server_tests/api_tests/documentation/api_documentation_test.py: -------------------------------------------------------------------------------- 1 | 2 | from requests import Request 3 | 4 | from tests.utils.location_utils import LOCAL_URL 5 | 6 | 7 | class Test_api_documentation: 8 | def test_api_documentation(self, test_client): 9 | r = Request('GET', LOCAL_URL + '/api/documentation') 10 | 11 | response = test_client.send(r) 12 | 13 | assert(response.status_code == 200) 14 | data = response.json 15 | print(data) 16 | assert 'get_endpoint_documentation' in data 17 | assert data['get_endpoint_documentation']['path'] == '/documentation' 18 | assert data['get_endpoint_documentation']['name'] == 'get_endpoint_documentation' 19 | -------------------------------------------------------------------------------- /tests/server_tests/api_tests/download/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/server_tests/api_tests/download/__init__.py -------------------------------------------------------------------------------- /tests/server_tests/api_tests/stats/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/server_tests/api_tests/stats/__init__.py -------------------------------------------------------------------------------- /tests/server_tests/api_tests/stats/get_replay_stats_test.py: -------------------------------------------------------------------------------- 1 | from requests import Request 2 | 3 | from backend.database.objects import Game 4 | from backend.database.startup import get_current_session 5 | from tests.utils.database_utils import initialize_db_with_replays 6 | from tests.utils.location_utils import LOCAL_URL 7 | 8 | 9 | class TestGetReplayStats: 10 | replay_status = [] 11 | 12 | def setup_method(self): 13 | session, protos, ids = initialize_db_with_replays(['crossplatform_party.replay']) 14 | self.replay_proto = protos[0] 15 | self.replay_id = ids[0] 16 | 17 | def test_replay_get_basic_stats(self, test_client, mock_user, mock_get_proto): 18 | mock_user.logout() 19 | game = get_current_session().query(Game).first() 20 | assert game is not None 21 | mock_get_proto(self.replay_proto) 22 | 23 | r = Request('GET', LOCAL_URL + '/api/replay/'+str(self.replay_id)+'/basic_player_stats') 24 | 25 | response = test_client.send(r) 26 | 27 | assert(response.status_code == 200) 28 | data = response.json 29 | 30 | # in the future assert each new stat that is being added. 31 | assert len(data) == 67 32 | 33 | def test_replay_get_team_stats(self, test_client, mock_user, mock_get_proto): 34 | mock_user.logout() 35 | game = get_current_session().query(Game).first() 36 | assert game is not None 37 | mock_get_proto(self.replay_proto) 38 | 39 | r = Request('GET', LOCAL_URL + '/api/replay/'+str(self.replay_id)+'/basic_team_stats') 40 | 41 | response = test_client.send(r) 42 | 43 | assert(response.status_code == 200) 44 | data = response.json 45 | 46 | # in the future assert each new stat that is being added. 47 | assert len(data) == 12 48 | -------------------------------------------------------------------------------- /tests/server_tests/api_tests/upload/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/server_tests/api_tests/upload/__init__.py -------------------------------------------------------------------------------- /tests/server_tests/backend_utils_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/server_tests/backend_utils_tests/__init__.py -------------------------------------------------------------------------------- /tests/server_tests/backend_utils_tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture() 7 | def mock_bracket(requests_mock): 8 | from backend.utils.braacket_connection import Braacket 9 | player_html_file = open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'RLBot_Player.html'), 'r').read() 10 | requests_mock.get('https://braacket.com/league/' 11 | f'{Braacket().league}/player?rows=200', text=player_html_file) 12 | 13 | requests_mock.get('https://braacket.com/league/rlbot/player/notABot', text=player_html_file) 14 | 15 | skybot_html_file = open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 16 | 'RLBot_Player_SkyBot.html'), 'r').read() 17 | uuid = '54FB8C16-6FA9-4C4A-AAD5-3DB8A6AE169B' 18 | requests_mock.get( 19 | 'https://braacket.com/league/' 20 | f'{Braacket().league}/player/{uuid}', text=skybot_html_file) 21 | -------------------------------------------------------------------------------- /tests/server_tests/backend_utils_tests/initial_setup_test.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from backend.initial_setup import CalculatedServer 4 | 5 | 6 | class Test_initial_setup: 7 | def test_get_version(self): 8 | # git needs to be in the environment for this 9 | assert re.compile(r"[0-9a-fA-F]{7}").match( 10 | CalculatedServer.get_version() 11 | ) 12 | -------------------------------------------------------------------------------- /tests/server_tests/database_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/server_tests/database_tests/__init__.py -------------------------------------------------------------------------------- /tests/server_tests/database_tests/wrapper/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/server_tests/database_tests/wrapper/__init__.py -------------------------------------------------------------------------------- /tests/server_tests/database_tests/wrapper/query_filter_builder_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from backend.database.wrapper.query_filter_builder import QueryFilterBuilder 4 | 5 | 6 | class QueryTest(unittest.TestCase): 7 | def test_query_builder(self): 8 | query = QueryFilterBuilder().with_relative_start_time(days_ago=10).sticky().clean() 9 | print(query) 10 | -------------------------------------------------------------------------------- /tests/server_tests/database_tests/wrapper/stats/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/server_tests/database_tests/wrapper/stats/__init__.py -------------------------------------------------------------------------------- /tests/server_tests/database_tests/wrapper/stats/shared_stats_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import List 3 | 4 | from backend.database.wrapper.chart.player_chart_metadata import player_stats_metadata 5 | from backend.database.wrapper.field_wrapper import QueryFieldWrapper 6 | from backend.database.wrapper.stats.creation.player_stat_creation import PlayerStatCreation 7 | from backend.database.wrapper.stats.creation.team_stat_creation import TeamStatCreation 8 | 9 | 10 | class SharedStatsTest(unittest.TestCase): 11 | def setUp(self): 12 | self.player_stats = PlayerStatCreation() 13 | self.team_stats = TeamStatCreation() 14 | 15 | def test_create_stats_field(self): 16 | player_stats = self.player_stats.get_math_queries() 17 | team_stats = self.team_stats.get_stat_list() 18 | print(player_stats) 19 | 20 | def test_has_explanations(self): 21 | stat_list: List[QueryFieldWrapper] = self.player_stats.get_stat_list() 22 | self.assertTrue(all([stat.explanation is not None for stat in stat_list]), 23 | msg="The following stats do not have explanations: {}".format( 24 | str([stat.dynamic_field.field_name for stat in stat_list if stat.explanation is None]))) 25 | 26 | def test_player_charts(self): 27 | # test for duplicates 28 | stat_names: List[str] = [p.stat_name + str(p.subcategory) for p in player_stats_metadata] 29 | self.assertTrue(len(stat_names) == len(set(stat_names)), 30 | msg="There are duplicate stat names in stat metadata list.") 31 | -------------------------------------------------------------------------------- /tests/server_tests/database_tests/wrapper/utils_test.py: -------------------------------------------------------------------------------- 1 | from backend.database.objects import Game, PlayerGame 2 | from backend.database.utils.utils import add_objects 3 | from tests.utils.database_utils import initialize_db_with_replays, empty_database 4 | from tests.utils.replay_utils import get_complex_replay_list 5 | 6 | # test the database utils 7 | 8 | class Test_Utils: 9 | 10 | def setup_method(self, method): 11 | self.session, self.protos, self.guids = initialize_db_with_replays([get_complex_replay_list()[0]]) 12 | self.guid = self.guids[0] 13 | self.proto = self.protos[0] 14 | 15 | def test_add(self): 16 | match = self.session.query(PlayerGame).filter(PlayerGame.game == self.guid).first() 17 | assert(match is not None) 18 | 19 | def test_same_upload_date(self): 20 | match: Game = self.session.query(Game).filter(Game.hash == self.guid).first() 21 | assert(match is not None) 22 | 23 | upload_date = match.upload_date 24 | 25 | add_objects(self.proto, session=self.session) 26 | match: Game = self.session.query(Game).filter(Game.hash == self.guid).first() 27 | assert(upload_date == match.upload_date) 28 | 29 | def test_same_ranks(self): 30 | match: PlayerGame = self.session.query(PlayerGame).filter(PlayerGame.game == self.guid).first() 31 | assert(match is not None) 32 | 33 | rank = match.rank 34 | 35 | add_objects(self.proto, session=self.session) 36 | match: PlayerGame = self.session.query(PlayerGame).filter(PlayerGame.game == self.guid).first() 37 | assert(rank == match.rank) 38 | 39 | def test_clear_database(self, engine): 40 | match = self.session.query(PlayerGame).filter(PlayerGame.game == self.guid).first() 41 | assert(match is not None) 42 | empty_database(engine) 43 | 44 | match = self.session.query(PlayerGame).filter(PlayerGame.game == self.guid).first() 45 | assert(match is None) 46 | -------------------------------------------------------------------------------- /tests/server_tests/task_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/server_tests/task_tests/__init__.py -------------------------------------------------------------------------------- /tests/server_tests/task_tests/celery_test.py: -------------------------------------------------------------------------------- 1 | from backend.tasks.celery_tasks import parse_replay_task, calculate_global_stats_by_rank 2 | from tests.utils.replay_utils import write_files_to_disk, get_test_file, get_complex_replay_list 3 | 4 | 5 | class TestCelerytasks(): 6 | 7 | def test_parse_replay(self, temp_folder): 8 | write_files_to_disk([get_complex_replay_list()[0]], temp_folder=temp_folder) 9 | result = parse_replay_task.apply(throw=True, 10 | kwargs={'replay_to_parse_path': get_test_file('3_KICKOFFS_4_SHOTS.replay', 11 | temp_folder=temp_folder), 12 | 'force_reparse': True}).get() 13 | assert(result == '70DDECEA4653AC55EA77DBA0DB497995') 14 | 15 | def test_global_stats_by_rank_dont_crash_null_data(self): 16 | calculate_global_stats_by_rank.apply(throw=True).get() 17 | -------------------------------------------------------------------------------- /tests/test_replays/3_DRIBBLES_2_FLICKS.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/3_DRIBBLES_2_FLICKS.replay -------------------------------------------------------------------------------- /tests/test_replays/3_KICKOFFS_4_SHOTS.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/3_KICKOFFS_4_SHOTS.replay -------------------------------------------------------------------------------- /tests/test_replays/ALL_STAR.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/ALL_STAR.replay -------------------------------------------------------------------------------- /tests/test_replays/ALL_STAR_SCOUT.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/ALL_STAR_SCOUT.replay -------------------------------------------------------------------------------- /tests/test_replays/FAKE_BOTS_SkyBot.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/FAKE_BOTS_SkyBot.replay -------------------------------------------------------------------------------- /tests/test_replays/NO_KICKOFF.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/NO_KICKOFF.replay -------------------------------------------------------------------------------- /tests/test_replays/RUMBLE_FULL.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/RUMBLE_FULL.replay -------------------------------------------------------------------------------- /tests/test_replays/SKYBOT_DRIBBLE_INFO.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/SKYBOT_DRIBBLE_INFO.replay -------------------------------------------------------------------------------- /tests/test_replays/TRAINING_PACK.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/TRAINING_PACK.replay -------------------------------------------------------------------------------- /tests/test_replays/WASTED_BOOST_WHILE_SUPER_SONIC.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/WASTED_BOOST_WHILE_SUPER_SONIC.replay -------------------------------------------------------------------------------- /tests/test_replays/ZEROED_STATS.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/ZEROED_STATS.replay -------------------------------------------------------------------------------- /tests/test_replays/crossplatform_party.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/crossplatform_party.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/0_JUMPS.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/0_JUMPS.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/100_BOOST_PAD_0_USED.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/100_BOOST_PAD_0_USED.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/100_BOOST_PAD_100_USED.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/100_BOOST_PAD_100_USED.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/12_AND_100_BOOST_PADS_0_USED.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/12_AND_100_BOOST_PADS_0_USED.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/12_BOOST_PAD_0_USED.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/12_BOOST_PAD_0_USED.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/12_BOOST_PAD_45_USED.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/12_BOOST_PAD_45_USED.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/1_AERIAL.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/1_AERIAL.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/1_CLEAR.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/1_CLEAR.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/1_DEMO.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/1_DEMO.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/1_DOUBLE_JUMP.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/1_DOUBLE_JUMP.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/1_EPIC_SAVE.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/1_EPIC_SAVE.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/1_JUMP.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/1_JUMP.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/1_NORMAL_SAVE.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/1_NORMAL_SAVE.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/3_KICKOFFS_4_SHOTS.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/3_KICKOFFS_4_SHOTS.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/CALCULATE_USED_BOOST_DEMO_WITH_FLIPS.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/CALCULATE_USED_BOOST_DEMO_WITH_FLIPS.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/CALCULATE_USED_BOOST_WITH_DEMO.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/CALCULATE_USED_BOOST_WITH_DEMO.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/GROUNDED_PASS_GOAL.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/GROUNDED_PASS_GOAL.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/HIGH_AIR_PASS_GOAL.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/HIGH_AIR_PASS_GOAL.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/LAST_KICKOFF_NO_TOUCH.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/LAST_KICKOFF_NO_TOUCH.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/MID_AIR_PASS_GOAL.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/MID_AIR_PASS_GOAL.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/MORE_THAN_100_BOOST.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/MORE_THAN_100_BOOST.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/NO_BOOST_PAD_0_USED.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/NO_BOOST_PAD_0_USED.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/NO_BOOST_PAD_33_USED.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/NO_BOOST_PAD_33_USED.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/NO_KICKOFF.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/NO_KICKOFF.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/USE_BOOST_AFTER_GOAL.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/USE_BOOST_AFTER_GOAL.replay -------------------------------------------------------------------------------- /tests/test_replays/small_replays/WASTED_BOOST_WHILE_SUPER_SONIC.replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/test_replays/small_replays/WASTED_BOOST_WHILE_SUPER_SONIC.replay -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/tests/utils/__init__.py -------------------------------------------------------------------------------- /tests/utils/location_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | LOCAL_URL = 'http://localhost:8000' 4 | 5 | 6 | default_test_data_folder_location = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'test_data') 7 | 8 | 9 | def get_test_replay_folder(): 10 | """ 11 | :return: The folder where downloaded replays are stored for testing. 12 | """ 13 | return os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'test_replays') 14 | 15 | 16 | class TestFolderManager: 17 | @staticmethod 18 | def get_internal_default_test_folder_location(): 19 | return default_test_data_folder_location 20 | 21 | @staticmethod 22 | def get_test_folder(temp_folder=None): 23 | if temp_folder is not None: 24 | return temp_folder 25 | if not os.path.exists(TestFolderManager.get_internal_default_test_folder_location()): 26 | os.mkdir(TestFolderManager.get_internal_default_test_folder_location()) 27 | return TestFolderManager.get_internal_default_test_folder_location() 28 | -------------------------------------------------------------------------------- /tests/utils/test_utils.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | 4 | def function_result_creator(): 5 | stored_result = "" 6 | 7 | def set_result(result): 8 | nonlocal stored_result 9 | stored_result = result 10 | 11 | def get_result(): 12 | return stored_result 13 | 14 | return set_result, get_result 15 | 16 | 17 | def check_array_equal(l1: List, l2: List): 18 | assert sorted(l1) == sorted(l2) 19 | -------------------------------------------------------------------------------- /tmuxinator.yml: -------------------------------------------------------------------------------- 1 | # Expects a python 3 virtualenv to be set up and pip requirements to be installed to the virtualenv 2 | 3 | # Before running for the first time run these: 4 | # python3 -m pip install --user virtualenv 5 | # python3 -m virtualenv env 6 | # source env/bin/activate 7 | # pip install -r requirements.txt 8 | # deactivate 9 | # cd webapp && npm install && cd .. 10 | 11 | name: calculated 12 | 13 | windows: 14 | - frontend_api: 15 | layout: even-horizontal 16 | panes: 17 | - webapp: 18 | - cd webapp 19 | - npm start 20 | - rlbot_server: 21 | - source env/bin/activate 22 | - export FLASK_ENV=development 23 | - python RLBotServer.py 24 | - backend: 25 | layout: main-vertical 26 | panes: 27 | - celery: 28 | - source env/bin/activate 29 | - celery -A backend.tasks.celery_tasks.celery worker --pool=solo -l info 30 | - flower: 31 | - source env/bin/activate 32 | - flower --port=5555 33 | - redis/redis-server.exe 34 | -------------------------------------------------------------------------------- /update_run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | sleep 3 3 | echo "killing server" 4 | kill -9 `ps aux |grep gunicorn |grep app | awk '{ print $2 }'` # will kill all of the workers 5 | sleep 10 6 | echo "server should be dead" 7 | 8 | 9 | [["$0" == "test"]] && IS_TEST=0 || IS_TEST=1 10 | 11 | # check that server is dead 12 | wget localhost:5000 -O /dev/null 13 | if [ $? -eq 0 ]; then 14 | echo "Server still running stopping upgrade" 15 | exit 1 16 | else 17 | echo "Server is successfully dead" 18 | fi 19 | 20 | # update server 21 | git checkout master 22 | git pull 23 | 24 | # build the frontend 25 | npm run build 26 | 27 | # run client 28 | 29 | if [ $IS_TEST -eq 1 ]; then 30 | echo "running test instance" 31 | sh ./run.sh 32 | else 33 | echo "running real instance" 34 | sh ./run-ssl.sh 35 | fi 36 | -------------------------------------------------------------------------------- /webapp/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 4, 4 | "semi": false, 5 | "bracketSpacing": false, 6 | "arrowParens": "always", 7 | "printWidth": 120 8 | } 9 | -------------------------------------------------------------------------------- /webapp/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" 2 | declare module "*.png" 3 | declare module "*.jpg" 4 | -------------------------------------------------------------------------------- /webapp/public/ai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ai.jpg -------------------------------------------------------------------------------- /webapp/public/calculated-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/calculated-icon.png -------------------------------------------------------------------------------- /webapp/public/draco/draco_decoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/draco/draco_decoder.wasm -------------------------------------------------------------------------------- /webapp/public/fieldblack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/fieldblack.png -------------------------------------------------------------------------------- /webapp/public/fieldrblack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/fieldrblack.png -------------------------------------------------------------------------------- /webapp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /webapp/public/models/FieldTest1.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'Field.blend' 2 | # Material Count: 1 3 | 4 | newmtl material_1 5 | Ns 96.078431 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 0.640000 0.640000 0.640000 8 | Ks 0.500000 0.500000 0.500000 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.000000 11 | d 1.000000 12 | illum 2 13 | -------------------------------------------------------------------------------- /webapp/public/models/FieldTest2.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'Field.blend' 2 | # Material Count: 1 3 | 4 | newmtl material_1 5 | Ns 96.078431 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 0.640000 0.640000 0.640000 8 | Ks 0.500000 0.500000 0.500000 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.000000 11 | d 1.000000 12 | illum 2 13 | -------------------------------------------------------------------------------- /webapp/public/psynet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/psynet.jpg -------------------------------------------------------------------------------- /webapp/public/ranks/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/0.png -------------------------------------------------------------------------------- /webapp/public/ranks/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/1.png -------------------------------------------------------------------------------- /webapp/public/ranks/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/10.png -------------------------------------------------------------------------------- /webapp/public/ranks/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/11.png -------------------------------------------------------------------------------- /webapp/public/ranks/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/12.png -------------------------------------------------------------------------------- /webapp/public/ranks/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/13.png -------------------------------------------------------------------------------- /webapp/public/ranks/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/14.png -------------------------------------------------------------------------------- /webapp/public/ranks/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/15.png -------------------------------------------------------------------------------- /webapp/public/ranks/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/16.png -------------------------------------------------------------------------------- /webapp/public/ranks/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/17.png -------------------------------------------------------------------------------- /webapp/public/ranks/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/18.png -------------------------------------------------------------------------------- /webapp/public/ranks/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/19.png -------------------------------------------------------------------------------- /webapp/public/ranks/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/2.png -------------------------------------------------------------------------------- /webapp/public/ranks/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/20.png -------------------------------------------------------------------------------- /webapp/public/ranks/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/21.png -------------------------------------------------------------------------------- /webapp/public/ranks/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/22.png -------------------------------------------------------------------------------- /webapp/public/ranks/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/3.png -------------------------------------------------------------------------------- /webapp/public/ranks/31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/31.png -------------------------------------------------------------------------------- /webapp/public/ranks/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/32.png -------------------------------------------------------------------------------- /webapp/public/ranks/33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/33.png -------------------------------------------------------------------------------- /webapp/public/ranks/34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/34.png -------------------------------------------------------------------------------- /webapp/public/ranks/35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/35.png -------------------------------------------------------------------------------- /webapp/public/ranks/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/4.png -------------------------------------------------------------------------------- /webapp/public/ranks/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/5.png -------------------------------------------------------------------------------- /webapp/public/ranks/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/6.png -------------------------------------------------------------------------------- /webapp/public/ranks/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/7.png -------------------------------------------------------------------------------- /webapp/public/ranks/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/8.png -------------------------------------------------------------------------------- /webapp/public/ranks/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/ranks/9.png -------------------------------------------------------------------------------- /webapp/public/replay_page_background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/replay_page_background.jpg -------------------------------------------------------------------------------- /webapp/public/replay_page_background_black.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/replay_page_background_black.jpg -------------------------------------------------------------------------------- /webapp/public/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/splash.png -------------------------------------------------------------------------------- /webapp/public/splash_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/public/splash_black.png -------------------------------------------------------------------------------- /webapp/src/CodeSplitComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, {Component, ComponentType} from "react" 2 | 3 | import {Typography} from "@material-ui/core" 4 | 5 | type Props = any 6 | 7 | interface State { 8 | errorOnDynamicLoad: boolean 9 | component: ComponentType | null 10 | } 11 | 12 | export const codeSplit = (importComponent: () => Promise, importKey: string) => { 13 | class AsyncComponent extends Component { 14 | constructor(props: Props) { 15 | super(props) 16 | 17 | this.state = { 18 | errorOnDynamicLoad: false, 19 | component: null 20 | } 21 | } 22 | 23 | public async componentDidMount() { 24 | try { 25 | const importedModule = await importComponent() 26 | 27 | this.setState({ 28 | component: importedModule[importKey] 29 | }) 30 | } catch (error) { 31 | this.setState({ 32 | errorOnDynamicLoad: true 33 | }) 34 | } 35 | } 36 | 37 | public render() { 38 | const {errorOnDynamicLoad, component: C} = this.state 39 | 40 | if (errorOnDynamicLoad) { 41 | return ( 42 | 43 | Error loading {importKey}. Please check your network connection and reload this page. 44 | 45 | ) 46 | } 47 | 48 | return C ? : null 49 | } 50 | } 51 | 52 | return AsyncComponent 53 | } 54 | -------------------------------------------------------------------------------- /webapp/src/Components/Home/HomePageAppBar.tsx: -------------------------------------------------------------------------------- 1 | import {faLightbulb} from "@fortawesome/free-solid-svg-icons" 2 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" 3 | import {AppBar, IconButton, Toolbar, Tooltip} from "@material-ui/core" 4 | import Menu from "@material-ui/icons/Menu" 5 | import * as React from "react" 6 | import {ThemeContext} from "../../Contexts/ThemeContext" 7 | 8 | interface Props { 9 | toggleSideBar: () => void 10 | } 11 | 12 | export class HomePageAppBar extends React.PureComponent { 13 | public render() { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {this.props.children} 23 | 24 | 25 | {(themeValue) => ( 26 | 27 | 28 | 29 | 30 | 31 | )} 32 | 33 | 34 | 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /webapp/src/Components/Leaderboards/LeaderListItem.tsx: -------------------------------------------------------------------------------- 1 | import {Avatar, ListItem, ListItemAvatar, ListItemText} from "@material-ui/core" 2 | import * as React from "react" 3 | import {Link, LinkProps} from "react-router-dom" 4 | import {PLAYER_PAGE_LINK} from "../../Globals" 5 | 6 | interface Props { 7 | leader: Leader 8 | } 9 | 10 | export class LeaderListItem extends React.PureComponent { 11 | private readonly createLink = React.forwardRef>( 12 | (props, ref) => 13 | ) 14 | 15 | public render() { 16 | const {leader} = this.props 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /webapp/src/Components/Leaderboards/LeaderboardList.tsx: -------------------------------------------------------------------------------- 1 | import {List, Typography} from "@material-ui/core" 2 | import * as React from "react" 3 | import {LeaderListItem} from "./LeaderListItem" 4 | import {LeaderboardWithMetadata} from "./PlaylistLeaderboardGrid" 5 | 6 | interface Props { 7 | leaderboard: LeaderboardWithMetadata 8 | } 9 | 10 | export class LeaderboardList extends React.PureComponent { 11 | public render() { 12 | const {leaderboard} = this.props 13 | return ( 14 |
15 | 16 | {leaderboard.playlistMetadata.name} 17 | 18 | 19 | {leaderboard.leaders.month.map((leader, i) => ( 20 | 21 | ))} 22 | 23 |
24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /webapp/src/Components/Pages/LeaderboardsPage.tsx: -------------------------------------------------------------------------------- 1 | import {Grid} from "@material-ui/core" 2 | import * as React from "react" 3 | import {getLeaderboards} from "../../Requests/Global" 4 | import {PlaylistLeaderboardGrid} from "../Leaderboards/PlaylistLeaderboardGrid" 5 | import {LoadableWrapper} from "../Shared/LoadableWrapper" 6 | import {BasePage} from "./BasePage" 7 | 8 | interface Props {} 9 | 10 | interface State { 11 | leaderboards?: PlaylistLeaderboard[] 12 | } 13 | 14 | export class LeaderboardsPage extends React.PureComponent { 15 | constructor(props: Props) { 16 | super(props) 17 | this.state = {} 18 | } 19 | 20 | public render() { 21 | return ( 22 | 23 | 24 | 25 | {this.state.leaderboards && } 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | private readonly getLeaderboards = (): Promise => { 33 | return getLeaderboards().then((leaderboards) => this.setState({leaderboards})) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /webapp/src/Components/Pages/TagsPage.tsx: -------------------------------------------------------------------------------- 1 | import {Grid, List, Paper} from "@material-ui/core" 2 | import * as React from "react" 3 | import {connect} from "react-redux" 4 | import {Dispatch} from "redux" 5 | import {StoreState, TagsAction} from "../../Redux" 6 | import {getAllTags} from "../../Requests/Tag" 7 | import {TagPageListItem} from "../Shared/Tag/TagPageListItem" 8 | import {BasePage} from "./BasePage" 9 | 10 | const mapStateToProps = (state: StoreState) => ({ 11 | tags: state.tags 12 | }) 13 | 14 | const mapDispatchToProps = (dispatch: Dispatch) => ({ 15 | setTags: (tags: Tag[]) => dispatch(TagsAction.setTagsAction(tags)) 16 | }) 17 | 18 | type Props = ReturnType & ReturnType 19 | 20 | class TagsPageComponent extends React.PureComponent { 21 | public componentDidMount() { 22 | getAllTags().then((tags) => this.props.setTags(tags)) 23 | } 24 | 25 | public render() { 26 | const {tags} = this.props 27 | return ( 28 | 29 | 30 | 31 | {tags !== null && ( 32 | 33 | 34 | 35 | {tags.map((tag) => ( 36 | 37 | ))} 38 | 39 | 40 | 41 | )} 42 | 43 | 44 | 45 | ) 46 | } 47 | } 48 | 49 | export const TagsPage = connect(mapStateToProps, mapDispatchToProps)(TagsPageComponent) 50 | -------------------------------------------------------------------------------- /webapp/src/Components/Pages/UploadPage.tsx: -------------------------------------------------------------------------------- 1 | import {Card, Divider, Grid} from "@material-ui/core" 2 | import * as React from "react" 3 | import {PreviousUploads} from "../Shared/Upload/PreviousUploads" 4 | import {UploadForm} from "../Shared/Upload/UploadForm" 5 | import {UploadTab, UploadTabs} from "../Shared/Upload/UploadTabs" 6 | import {BasePage} from "./BasePage" 7 | 8 | interface State { 9 | selectedTab: UploadTab 10 | } 11 | 12 | export class UploadPage extends React.PureComponent<{}, State> { 13 | constructor(props: {}) { 14 | super(props) 15 | this.state = {selectedTab: "Upload Replays"} 16 | } 17 | 18 | public render() { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | {this.state.selectedTab === "Upload Replays" ? : } 27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | private readonly handleTabChange = (_: React.ChangeEvent<{}>, selectedTab: UploadTab) => { 35 | this.setState({selectedTab}) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /webapp/src/Components/Player/Compare/AddPlayerInput.tsx: -------------------------------------------------------------------------------- 1 | import {IconButton, TextField, Tooltip} from "@material-ui/core" 2 | import Add from "@material-ui/icons/Add" 3 | import * as React from "react" 4 | 5 | interface Props { 6 | onSubmit: () => void 7 | value: string 8 | onChange: React.ChangeEventHandler 9 | } 10 | 11 | export class AddPlayerInput extends React.PureComponent { 12 | public render() { 13 | return ( 14 |
15 | 23 | 24 | 25 | 26 | 27 | ) 28 | }} 29 | /> 30 | 31 | ) 32 | } 33 | 34 | private readonly handleFormSubmit: React.ChangeEventHandler = (event) => { 35 | event.preventDefault() 36 | this.props.onSubmit() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /webapp/src/Components/Player/Compare/PlayerChip.tsx: -------------------------------------------------------------------------------- 1 | import {Avatar, Chip, createStyles, Typography, withStyles, WithStyles} from "@material-ui/core" 2 | import * as React from "react" 3 | import {RouteComponentProps, withRouter} from "react-router" 4 | import {PLAYER_PAGE_LINK} from "../../../Globals" 5 | 6 | const styles = createStyles({ 7 | root: { 8 | maxWidth: "100%" 9 | }, 10 | label: { 11 | maxWidth: "calc(100% - 48px)" 12 | } 13 | }) 14 | 15 | interface OwnProps extends Player { 16 | onDelete: () => void 17 | } 18 | 19 | type Props = OwnProps & RouteComponentProps<{}> & WithStyles 20 | 21 | class PlayerChipComponent extends React.PureComponent { 22 | public render() { 23 | return ( 24 | } 26 | label={ 27 | 28 | {this.props.name} 29 | 30 | } 31 | onDelete={this.props.onDelete} 32 | onClick={this.onClick} 33 | classes={{label: this.props.classes.label}} 34 | className={this.props.classes.root} 35 | /> 36 | ) 37 | } 38 | 39 | private readonly onClick = () => { 40 | this.props.history.push(PLAYER_PAGE_LINK(this.props.id)) 41 | } 42 | } 43 | 44 | export const PlayerChip = withStyles(styles)(withRouter(PlayerChipComponent)) 45 | -------------------------------------------------------------------------------- /webapp/src/Components/Player/Overview/MatchHistory/FullMatchHistoryLinkButton.tsx: -------------------------------------------------------------------------------- 1 | import ViewList from "@material-ui/icons/ViewList" 2 | import * as React from "react" 3 | import {PLAYER_MATCH_HISTORY_PAGE_LINK} from "../../../../Globals" 4 | import {LinkButton} from "../../../Shared/LinkButton" 5 | 6 | interface Props { 7 | player: Player 8 | } 9 | 10 | export class FullMatchHistoryLinkButton extends React.PureComponent { 11 | public render() { 12 | return ( 13 |
14 | 20 | View full 21 | 22 |
23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /webapp/src/Components/Player/Overview/MatchHistory/OverviewMatchHistoryRow.tsx: -------------------------------------------------------------------------------- 1 | import {createStyles, ExpansionPanel, ExpansionPanelDetails, WithStyles, withStyles} from "@material-ui/core" 2 | import * as React from "react" 3 | import {Replay} from "../../../../Models" 4 | import {ReplayBoxScore} from "../../../Replay/ReplayBoxScore" 5 | import {ReplayChart} from "../../../Replay/ReplayChart" 6 | import {ReplayExpansionPanelSummary} from "./ReplayExpansionPanelSummary" 7 | 8 | const styles = createStyles({ 9 | panelDetails: { 10 | overflowX: "auto", 11 | maxWidth: "95vw", 12 | margin: "auto" 13 | } 14 | }) 15 | 16 | interface OwnProps { 17 | replay: Replay 18 | player: Player 19 | useBoxScore?: boolean 20 | } 21 | 22 | type Props = OwnProps & WithStyles 23 | 24 | class OverviewMatchHistoryRowComponent extends React.PureComponent { 25 | public render() { 26 | const {classes} = this.props 27 | 28 | const {replay, player} = this.props 29 | 30 | return ( 31 | 32 | 33 | 34 | {!this.props.useBoxScore ? ( 35 | 36 | ) : ( 37 | 38 | )} 39 | 40 | 41 | ) 42 | } 43 | } 44 | 45 | export const OverviewMatchHistoryRow = withStyles(styles)(OverviewMatchHistoryRowComponent) 46 | -------------------------------------------------------------------------------- /webapp/src/Components/Player/Overview/MatchHistory/PlayerMatchHistoryCard.tsx: -------------------------------------------------------------------------------- 1 | import {Card, CardHeader} from "@material-ui/core" 2 | import * as React from "react" 3 | import {FullMatchHistoryLinkButton} from "./FullMatchHistoryLinkButton" 4 | 5 | interface Props { 6 | player: Player 7 | } 8 | 9 | export class PlayerMatchHistoryCard extends React.PureComponent { 10 | public render() { 11 | return ( 12 | 13 | } /> 14 | {this.props.children} 15 | 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /webapp/src/Components/Player/Overview/PlayStyle/PlayerPlayStyleCard.tsx: -------------------------------------------------------------------------------- 1 | import {Card, CardContent, CardHeader, Typography} from "@material-ui/core" 2 | import * as React from "react" 3 | import {IconTooltip} from "../../../Shared/IconTooltip" 4 | import {PlayStyleActions} from "./PlayStyleActions" 5 | 6 | interface Props { 7 | player: Player 8 | playlist: number 9 | winLossMode: boolean 10 | handlePlaylistChange?: (playlist: number) => void 11 | handleWinsLossesChange?: (winLossMode: boolean) => void 12 | } 13 | 14 | const PlayStyleTitle = () => ( 15 | 16 | Playstyle 17 | 18 | 19 | ) 20 | 21 | export class PlayerPlayStyleCard extends React.PureComponent { 22 | public render() { 23 | return ( 24 | 25 | } 27 | action={ 28 | 35 | } 36 | /> 37 | {this.props.children} 38 | 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /webapp/src/Components/Player/Overview/SideBar/PlayerNameDropdown.tsx: -------------------------------------------------------------------------------- 1 | import {IconButton, Menu, MenuItem} from "@material-ui/core" 2 | import ArrowDropDown from "@material-ui/icons/ArrowDropDown" 3 | import * as React from "react" 4 | 5 | interface Props { 6 | pastNames: string[] 7 | } 8 | 9 | interface State { 10 | open: boolean 11 | anchorElement?: HTMLElement 12 | } 13 | 14 | export class PlayerNameDropdown extends React.PureComponent { 15 | constructor(props: Props) { 16 | super(props) 17 | this.state = {open: false} 18 | } 19 | 20 | public render() { 21 | return ( 22 | <> 23 | 24 | 25 | 26 | 27 | {this.props.pastNames.map((name) => ( 28 | 29 | {name} 30 | 31 | ))} 32 | 33 | 34 | ) 35 | } 36 | 37 | private readonly handleOpen: React.MouseEventHandler = (event) => { 38 | this.setState({ 39 | open: true, 40 | anchorElement: event.currentTarget 41 | }) 42 | } 43 | 44 | private readonly handleClose = () => { 45 | this.setState({ 46 | open: false, 47 | anchorElement: undefined 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /webapp/src/Components/Player/Overview/SideBar/PlayerProfilePicture.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import {CardMedia, createStyles, WithStyles, withStyles} from "@material-ui/core" 4 | 5 | interface OwnProps { 6 | image: string 7 | } 8 | 9 | type Props = OwnProps & WithStyles 10 | 11 | class PlayerProfilePictureComponent extends React.PureComponent { 12 | public render() { 13 | const {image, classes} = this.props 14 | 15 | return ( 16 | <> 17 | 18 | 19 | ) 20 | } 21 | } 22 | 23 | const styles = createStyles({ 24 | itemListCard: { 25 | display: "flex" 26 | }, 27 | avatar: { 28 | flex: "0 0 128px" 29 | }, 30 | content: { 31 | width: "calc(100% - 128px)" 32 | }, 33 | nameWrapper: { 34 | whiteSpace: "nowrap", 35 | display: "flex" 36 | } 37 | }) 38 | 39 | export const PlayerProfilePicture = withStyles(styles)(PlayerProfilePictureComponent) 40 | -------------------------------------------------------------------------------- /webapp/src/Components/Player/Overview/SideBar/PlayerSideBar.tsx: -------------------------------------------------------------------------------- 1 | import {Grid} from "@material-ui/core" 2 | import * as React from "react" 3 | import {PlayerProfile} from "./PlayerProfile" 4 | import {PlayerRanksCard} from "./PlayerRanksCard" 5 | 6 | interface Props { 7 | player: Player 8 | } 9 | 10 | export class PlayerSideBar extends React.PureComponent { 11 | public render() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | {/**/} 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /webapp/src/Components/Player/Overview/SideBar/PlayerStats/FavouriteCar.tsx: -------------------------------------------------------------------------------- 1 | import {createStyles, Grid, Typography, WithStyles, withStyles} from "@material-ui/core" 2 | import DirectionsCar from "@material-ui/core/SvgIcon/SvgIcon" 3 | import * as React from "react" 4 | import {roundNumberToMaxDP} from "../../../../../Utils/String" 5 | 6 | const styles = createStyles({ 7 | percentage: { 8 | padding: 5, 9 | textAlign: "center", 10 | borderTop: "1px solid rgb(70, 70, 70)", 11 | borderBottom: "1px solid rgb(70, 70, 70)" 12 | } 13 | }) 14 | 15 | interface OwnProps { 16 | carStat: CarStat 17 | } 18 | 19 | type Props = OwnProps & WithStyles 20 | 21 | class FavouriteCarComponent extends React.PureComponent { 22 | public render() { 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | favourite car 32 | 33 | 34 | 35 | {this.props.carStat.carName} 36 | 37 | 38 | 39 | {roundNumberToMaxDP(this.props.carStat.carPercentage * 100, 1)}% 40 | 41 | 42 | 43 | 44 | ) 45 | } 46 | } 47 | 48 | export const FavouriteCar = withStyles(styles)(FavouriteCarComponent) 49 | -------------------------------------------------------------------------------- /webapp/src/Components/Player/Overview/SideBar/PlayerStats/LoadoutDialogWrapper.tsx: -------------------------------------------------------------------------------- 1 | import {Dialog, DialogTitle, Grid, IconButton, Typography} from "@material-ui/core" 2 | import CardTravel from "@material-ui/icons/CardTravel" 3 | import OpenInBrowser from "@material-ui/icons/OpenInBrowser" 4 | 5 | import * as React from "react" 6 | import {LoadoutDisplay} from "../../../../Replay/ReplayTeamCard/LoadoutDisplay" 7 | 8 | interface Props { 9 | playerStats: PlayerStats 10 | handleShowLoadout: () => void 11 | handleCloseLoadout: () => void 12 | loadoutOpen: boolean 13 | } 14 | 15 | export class LoadoutDialogWrapper extends React.PureComponent { 16 | public render() { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | loadout 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Loadout 32 | 33 | 34 | 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /webapp/src/Components/Replay/BasicStats/PlayerStats/PlayerStatsContent.tsx: -------------------------------------------------------------------------------- 1 | import {CardContent, Divider, Grid} from "@material-ui/core" 2 | import * as React from "react" 3 | import {PlayerStatsSubcategory, Replay} from "../../../../Models" 4 | import {PlayerStatsCharts} from "./PlayerStatsCharts" 5 | import {PlayerStatsTabs} from "./PlayerStatsTabs" 6 | 7 | interface Props { 8 | replay: Replay 9 | explanations: Record | undefined 10 | } 11 | 12 | interface State { 13 | selectedTab: PlayerStatsSubcategory 14 | exclude: string[] 15 | } 16 | 17 | export class PlayerStatsContent extends React.PureComponent { 18 | constructor(props: Props) { 19 | super(props) 20 | this.state = {selectedTab: PlayerStatsSubcategory.HITS, exclude: ["MAIN_STATS"]} 21 | } 22 | 23 | public render() { 24 | return ( 25 | <> 26 | 27 | 32 | 33 | 34 | 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | private readonly handleSelectTab = (event: React.ChangeEvent, selectedTab: PlayerStatsSubcategory) => { 46 | this.setState({selectedTab}) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /webapp/src/Components/Replay/BasicStats/TeamStats/TeamStatsContent.tsx: -------------------------------------------------------------------------------- 1 | import {CardContent, Divider, Grid} from "@material-ui/core" 2 | import * as React from "react" 3 | import {Replay, TeamStatsSubcategory} from "../../../../Models" 4 | import {TeamStatsCharts} from "./TeamStatsCharts" 5 | import {TeamStatsTabs} from "./TeamStatsTabs" 6 | 7 | interface Props { 8 | replay: Replay 9 | explanations: Record | undefined 10 | } 11 | 12 | interface State { 13 | selectedTab: TeamStatsSubcategory 14 | } 15 | 16 | export class TeamStatsContent extends React.PureComponent { 17 | constructor(props: Props) { 18 | super(props) 19 | this.state = {selectedTab: TeamStatsSubcategory.POSITIONING} 20 | } 21 | 22 | public render() { 23 | return ( 24 | <> 25 | 26 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | ) 38 | } 39 | 40 | private readonly handleSelectTab = (event: React.ChangeEvent, selectedTab: TeamStatsSubcategory) => { 41 | this.setState({selectedTab}) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /webapp/src/Components/Replay/BasicStats/TeamStats/TeamStatsTabs.tsx: -------------------------------------------------------------------------------- 1 | import {faBraille, faHandshake, IconDefinition} from "@fortawesome/free-solid-svg-icons" 2 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" 3 | import {Tab, Tabs, withWidth} from "@material-ui/core" 4 | import {isWidthDown, WithWidth} from "@material-ui/core/withWidth" 5 | import * as React from "react" 6 | import {TeamStatsSubcategory} from "../../../../Models" 7 | 8 | interface OwnProps { 9 | selectedTab: TeamStatsSubcategory 10 | handleChange: (event: any, selectedTab: TeamStatsSubcategory) => void 11 | } 12 | 13 | type Props = OwnProps & WithWidth 14 | 15 | class TeamStatsTabsComponent extends React.PureComponent { 16 | public render() { 17 | const categoryToIcon: Record = { 18 | Positioning: faBraille, 19 | "Center of Mass": faHandshake 20 | } 21 | const belowXs = isWidthDown("xs", this.props.width) 22 | 23 | return ( 24 | 31 | {Object.values(TeamStatsSubcategory).map((subcategoryValue) => ( 32 | } 37 | /> 38 | ))} 39 | 40 | ) 41 | } 42 | } 43 | 44 | export const TeamStatsTabs = withWidth()(TeamStatsTabsComponent) 45 | -------------------------------------------------------------------------------- /webapp/src/Components/Replay/Kickoffs/KickoffTabs.tsx: -------------------------------------------------------------------------------- 1 | import {createStyles, Tab, Tabs, Theme, WithStyles, withStyles} from "@material-ui/core" 2 | import * as React from "react" 3 | 4 | const styles = (theme: Theme) => 5 | createStyles({ 6 | verticalTabs: { 7 | minWidth: 150, 8 | borderRight: `1px solid ${theme.palette.divider}` 9 | } 10 | }) 11 | 12 | interface OwnProps { 13 | selectedTab: number 14 | handleChange: (event: React.ChangeEvent<{}>, selectedTab: number) => void 15 | kickoffData: KickoffData 16 | orientation: "horizontal" | "vertical" 17 | } 18 | 19 | type Props = OwnProps & WithStyles 20 | 21 | class KickoffTabsComponent extends React.PureComponent { 22 | public render() { 23 | const {classes, orientation, handleChange, selectedTab} = this.props 24 | 25 | return ( 26 | 34 | {this.createTabList().map((kickoff: string, index: number) => ( 35 | 36 | ))} 37 | 38 | ) 39 | } 40 | 41 | private readonly createTabList = () => { 42 | const modifiedKickoffData = this.props.kickoffData.kickoffs.map((_, index: number) => "Kickoff " + index) 43 | modifiedKickoffData.unshift("Overall") // Add to beginning of array 44 | return modifiedKickoffData 45 | } 46 | } 47 | 48 | export const KickoffTabs = withStyles(styles)(KickoffTabsComponent) 49 | -------------------------------------------------------------------------------- /webapp/src/Components/Replay/Predictions/PredictedRanksTable.tsx: -------------------------------------------------------------------------------- 1 | import {Table, TableBody, TableCell, TableHead, TableRow} from "@material-ui/core" 2 | import * as React from "react" 3 | import {Replay} from "../../../Models" 4 | import {PredictedRanksRow} from "./PredictedRanksRow" 5 | 6 | interface Props { 7 | replay: Replay 8 | predictedRanks: PredictedRank[] 9 | } 10 | 11 | export class PredictedRanksTable extends React.PureComponent { 12 | public render() { 13 | const {replay, predictedRanks} = this.props 14 | const blueTeam = replay.players.filter((player) => !player.isOrange) 15 | const orangeTeam = replay.players.filter((player) => player.isOrange) 16 | const maxLength = Math.max(blueTeam.length, orangeTeam.length) 17 | const rows: JSX.Element[] = [] 18 | for (let i = 0; i < maxLength; i++) { 19 | const playerLeft = i < blueTeam.length ? blueTeam[i] : undefined 20 | const playerRight = i < orangeTeam.length ? orangeTeam[i] : undefined 21 | rows.push( 22 | 28 | ) 29 | } 30 | 31 | return ( 32 | 33 | 34 | 35 | Player (Blue) 36 | 37 | Predicted Play Level 38 | 39 | Player (Orange) 40 | 41 | 42 | {rows} 43 |
44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /webapp/src/Components/Replay/ReplayTeamCard/CameraSettingsDisplay.tsx: -------------------------------------------------------------------------------- 1 | import {Grid, TextField} from "@material-ui/core" 2 | import * as _ from "lodash" 3 | import * as React from "react" 4 | import {convertSnakeAndCamelCaseToReadable, roundNumberToMaxDP} from "../../../Utils/String" 5 | 6 | interface Props { 7 | cameraSettings: CameraSettings 8 | } 9 | 10 | export class CameraSettingsDisplay extends React.PureComponent { 11 | public render() { 12 | return ( 13 |
14 | 15 | {_.toPairs(this.props.cameraSettings).map(([key, value]: [string, number]) => { 16 | return ( 17 | 18 | 24 | 25 | ) 26 | })} 27 | 28 |
29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /webapp/src/Components/Replay/ReplayTeamCard/Loadout/PaintedTriangle.tsx: -------------------------------------------------------------------------------- 1 | import {createStyles, Tooltip, withStyles, WithStyles} from "@material-ui/core" 2 | import React from "react" 3 | import {PAINT_COLOR_MAP, PAINT_MAP} from "./dataMaps" 4 | 5 | const styles = createStyles({ 6 | paintedTriangle: { 7 | position: "absolute", 8 | top: 0, 9 | right: 0, 10 | borderColor: "transparent", 11 | borderStyle: "solid", 12 | borderTopRightRadius: 5, 13 | borderWidth: 15 14 | } 15 | }) 16 | 17 | interface OwnProps { 18 | paintId: number 19 | } 20 | 21 | type Props = OwnProps & WithStyles 22 | 23 | class PaintedTriangleComponent extends React.PureComponent { 24 | public render() { 25 | const paintColor = PAINT_COLOR_MAP[this.props.paintId] + "DD" 26 | 27 | return ( 28 | 29 |
36 | 37 | ) 38 | } 39 | } 40 | export const PaintedTriangle = withStyles(styles)(PaintedTriangleComponent) 41 | -------------------------------------------------------------------------------- /webapp/src/Components/Replay/Visualizations/VisualizationsContent.tsx: -------------------------------------------------------------------------------- 1 | import {Divider, Grid} from "@material-ui/core" 2 | import * as React from "react" 3 | import {Replay} from "../../../Models" 4 | import {getBoostmap} from "../../../Requests/Replay" 5 | import {LoadableWrapper} from "../../Shared/LoadableWrapper" 6 | import {BoostMapWrapper} from "./BoostMapWrapper" 7 | 8 | interface Props { 9 | replay: Replay 10 | } 11 | 12 | interface State { 13 | element: any 14 | reloadSignal: boolean 15 | boostmapData: any 16 | } 17 | 18 | export class VisualizationsContent extends React.PureComponent { 19 | constructor(props: Props) { 20 | super(props) 21 | this.state = {element: null, reloadSignal: false, boostmapData: null} 22 | } 23 | 24 | public render() { 25 | return ( 26 | <> 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ) 37 | } 38 | 39 | private readonly getBoostmapsData = () => { 40 | return getBoostmap(this.props.replay.id).then((data) => this.setState({boostmapData: data})) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /webapp/src/Components/ReplaysGroup/AddReplayInput.tsx: -------------------------------------------------------------------------------- 1 | import {IconButton, TextField, Tooltip} from "@material-ui/core" 2 | import Add from "@material-ui/icons/Add" 3 | import * as React from "react" 4 | 5 | interface Props { 6 | onSubmit: () => void 7 | value: string 8 | onChange: React.ChangeEventHandler 9 | } 10 | 11 | export class AddReplayInput extends React.PureComponent { 12 | public render() { 13 | return ( 14 |
15 | 23 | 24 | 25 | 26 | 27 | ) 28 | }} 29 | /> 30 | 31 | ) 32 | } 33 | 34 | private readonly handleFormSubmit: React.ChangeEventHandler = (event) => { 35 | event.preventDefault() 36 | this.props.onSubmit() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /webapp/src/Components/ReplaysGroup/Charts/ReplaysGroupChartsWrapper.tsx: -------------------------------------------------------------------------------- 1 | import {CardContent, Grid} from "@material-ui/core" 2 | import * as React from "react" 3 | import {BasicStat, PlayerStatsSubcategory, Replay} from "../../../Models" 4 | import {PlayerStatsTabs} from "../../Replay/BasicStats/PlayerStats/PlayerStatsTabs" 5 | import {ReplaysGroupCharts} from "./ReplaysGroupCharts" 6 | 7 | interface OwnProps { 8 | replays: Replay[] 9 | } 10 | 11 | type Props = OwnProps 12 | 13 | interface State { 14 | basicStats?: BasicStat[] 15 | selectedTab: PlayerStatsSubcategory 16 | } 17 | 18 | export class ReplaysGroupChartsWrapper extends React.PureComponent { 19 | constructor(props: Props) { 20 | super(props) 21 | this.state = {selectedTab: PlayerStatsSubcategory.HITS} 22 | } 23 | 24 | public render() { 25 | return ( 26 | <> 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | } 36 | 37 | private readonly handleSelectTab = (event: React.ChangeEvent, selectedTab: PlayerStatsSubcategory) => { 38 | this.setState({selectedTab}) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webapp/src/Components/ReplaysGroup/ReplayChip.tsx: -------------------------------------------------------------------------------- 1 | import {Chip} from "@material-ui/core" 2 | import * as React from "react" 3 | import {RouteComponentProps, withRouter} from "react-router" 4 | import {REPLAY_PAGE_LINK} from "../../Globals" 5 | import {Replay} from "../../Models" 6 | 7 | interface OwnProps extends Replay { 8 | onDelete: () => void 9 | } 10 | 11 | type Props = OwnProps & RouteComponentProps<{}> 12 | 13 | class ReplayChipComponent extends React.PureComponent { 14 | public render() { 15 | return ( 16 | 21 | ) 22 | } 23 | 24 | private readonly onClick = () => { 25 | this.props.history.push(REPLAY_PAGE_LINK(this.props.id)) 26 | } 27 | } 28 | 29 | export const ReplayChip = withRouter(ReplayChipComponent) 30 | -------------------------------------------------------------------------------- /webapp/src/Components/ReplaysGroup/ReplaysGroupContent.tsx: -------------------------------------------------------------------------------- 1 | import {Card, Divider, Tab, Tabs} from "@material-ui/core" 2 | import * as React from "react" 3 | import {Replay} from "../../Models" 4 | import {ReplaysGroupChartsWrapper} from "./Charts/ReplaysGroupChartsWrapper" 5 | import {ReplaysGroupTable} from "./Table/ReplaysGroupTable" 6 | 7 | interface Props { 8 | replays: Replay[] 9 | } 10 | 11 | type ReplaysDetailsTab = "Table" | "Charts" 12 | 13 | interface State { 14 | selectedTab: ReplaysDetailsTab 15 | } 16 | 17 | export class ReplaysGroupContent extends React.PureComponent { 18 | constructor(props: Props) { 19 | super(props) 20 | this.state = {selectedTab: "Charts"} 21 | } 22 | 23 | public render() { 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | {this.state.selectedTab === "Table" ? ( 32 | 33 | ) : ( 34 | 35 | )} 36 | 37 | ) 38 | } 39 | 40 | private readonly handleSelectTab = (_: React.ChangeEvent<{}>, selectedTab: ReplaysDetailsTab) => { 41 | this.setState({selectedTab}) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /webapp/src/Components/ReplaysGroup/Table/TableScrollWrapper.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import {BasicStat} from "../../../Models" 3 | import {BasicStatsTable} from "./BasicStatsTable" 4 | 5 | interface Props { 6 | style: React.CSSProperties 7 | basicStats: BasicStat[] 8 | } 9 | 10 | interface State { 11 | scrollPosition: number 12 | scrollDiv?: any 13 | } 14 | 15 | export class TableScrollWrapper extends React.PureComponent { 16 | constructor(props: Props) { 17 | super(props) 18 | this.state = { 19 | scrollPosition: 0, 20 | scrollDiv: null 21 | } 22 | } 23 | 24 | public render() { 25 | return ( 26 |
{ 29 | this.setState({scrollDiv: e}) 30 | }} 31 | onScroll={this.listenToScroll} 32 | > 33 | 34 |
35 | ) 36 | } 37 | 38 | private readonly listenToScroll = () => { 39 | const winScroll = this.state.scrollDiv.scrollLeft 40 | this.setState({ 41 | scrollPosition: winScroll 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /webapp/src/Components/ReplaysSearch/ReplaysSearchTablePagination.tsx: -------------------------------------------------------------------------------- 1 | import {TablePagination} from "@material-ui/core" 2 | import * as qs from "qs" 3 | import * as React from "react" 4 | import {RouteComponentProps, withRouter} from "react-router" 5 | 6 | interface OwnProps { 7 | totalCount: number 8 | page: number 9 | limit: number 10 | } 11 | 12 | type Props = OwnProps & RouteComponentProps<{}> 13 | 14 | class ReplaysSearchTablePaginationComponent extends React.PureComponent { 15 | public render() { 16 | return ( 17 | 26 | ) 27 | } 28 | 29 | private readonly handleChangePage = (event: unknown, page: number) => { 30 | const currentQueryParams = qs.parse(this.props.location.search, {ignoreQueryPrefix: true}) 31 | this.props.history.replace({ 32 | search: qs.stringify({ 33 | ...currentQueryParams, 34 | page 35 | }) 36 | }) 37 | } 38 | 39 | private readonly handleChangeRowsPerPage: React.ChangeEventHandler = ( 40 | event 41 | ) => { 42 | const currentQueryParams = qs.parse(this.props.location.search, {ignoreQueryPrefix: true}) 43 | this.props.history.replace({ 44 | search: qs.stringify({ 45 | ...currentQueryParams, 46 | page: 0, 47 | limit: Number(event.target.value) 48 | }) 49 | }) 50 | } 51 | } 52 | 53 | export const ReplaysSearchTablePagination = withRouter(ReplaysSearchTablePaginationComponent) 54 | -------------------------------------------------------------------------------- /webapp/src/Components/Shared/Charts/ColoredBarChart.tsx: -------------------------------------------------------------------------------- 1 | import {ChartData, ChartOptions} from "chart.js" 2 | import * as React from "react" 3 | import {Bar} from "react-chartjs-2" 4 | import {BasicStat} from "../../../Models" 5 | import {roundLabelToMaxDPCallback} from "../../../Utils/Chart" 6 | import {convertHexToRgba, getPrimaryColorsForPlayers, primaryColours} from "../../../Utils/Color" 7 | 8 | interface Props { 9 | basicStat: BasicStat 10 | } 11 | 12 | export class ColoredBarChart extends React.PureComponent { 13 | public render() { 14 | return 15 | } 16 | 17 | private readonly getChartData = (): ChartData => { 18 | const chartDataPoints = this.props.basicStat.chartDataPoints 19 | const backgroundColors = 20 | chartDataPoints[0].isOrange !== undefined 21 | ? getPrimaryColorsForPlayers(chartDataPoints.map((chartDataPoint) => chartDataPoint.isOrange)) 22 | : primaryColours.slice(0, chartDataPoints.length).map((hexColor) => convertHexToRgba(hexColor, 0.7)) 23 | 24 | return { 25 | labels: chartDataPoints.map((chartDataPoint) => chartDataPoint.name), 26 | datasets: [ 27 | { 28 | data: chartDataPoints.map((chartDataPoint) => chartDataPoint.value), 29 | backgroundColor: backgroundColors 30 | } 31 | ] 32 | } 33 | } 34 | 35 | private readonly getChartOptions = (): ChartOptions => { 36 | return { 37 | legend: {display: false}, 38 | scales: { 39 | yAxes: [{ticks: {maxTicksLimit: 5, beginAtZero: true}}] 40 | }, 41 | tooltips: { 42 | callbacks: { 43 | label: roundLabelToMaxDPCallback 44 | } 45 | } 46 | } as ChartOptions 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /webapp/src/Components/Shared/ClearableDatePicker.tsx: -------------------------------------------------------------------------------- 1 | import {DatePicker, DatePickerProps} from "@material-ui/pickers" 2 | import * as React from "react" 3 | 4 | type Props = DatePickerProps 5 | 6 | export class ClearableDatePicker extends React.PureComponent { 7 | public render() { 8 | return 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /webapp/src/Components/Shared/ColouredGameScore.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import {ThemeContext} from "../../Contexts/ThemeContext" 3 | import {CompactReplay, Replay} from "../../Models" 4 | 5 | interface Props { 6 | replay: Replay | CompactReplay 7 | } 8 | 9 | export class ColouredGameScore extends React.PureComponent { 10 | public render() { 11 | return ( 12 | 13 | {(themeValue) => ( 14 | <> 15 | {this.props.replay.gameScore.team0Score} 16 | {" - "} 17 | {this.props.replay.gameScore.team1Score} 18 | 19 | )} 20 | 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /webapp/src/Components/Shared/IconTooltip.tsx: -------------------------------------------------------------------------------- 1 | import {createStyles, Theme, Tooltip, WithStyles, withStyles} from "@material-ui/core" 2 | import Info from "@material-ui/icons/Info" 3 | import * as React from "react" 4 | 5 | export const styles = (theme: Theme) => 6 | createStyles({ 7 | infoIcon: { 8 | verticalAlign: "middle", 9 | marginLeft: theme.spacing(1), 10 | marginTop: -4 11 | } 12 | }) 13 | 14 | interface OwnProps { 15 | tooltip: string 16 | } 17 | 18 | type Props = OwnProps & WithStyles 19 | 20 | class IconTooltipComponent extends React.PureComponent { 21 | public render() { 22 | const {classes, tooltip} = this.props 23 | return ( 24 | 25 | 26 | 27 | ) 28 | } 29 | } 30 | 31 | export const IconTooltip = withStyles(styles)(IconTooltipComponent) 32 | -------------------------------------------------------------------------------- /webapp/src/Components/Shared/Logo/Logo.tsx: -------------------------------------------------------------------------------- 1 | import {WithTheme, withTheme} from "@material-ui/core" 2 | import * as React from "react" 3 | import {Link} from "react-router-dom" 4 | import LightLogoImage from "./calculated-logo-light.png" 5 | import LogoImage from "./calculated-logo.png" 6 | 7 | interface OwnProps { 8 | imgStyle?: React.CSSProperties 9 | } 10 | 11 | type Props = OwnProps & WithTheme 12 | 13 | class LogoComponent extends React.PureComponent { 14 | public render() { 15 | const logoImage = this.props.theme.palette.type === "dark" ? LightLogoImage : LogoImage 16 | return ( 17 | 18 | calculated.gg logo 19 | 20 | ) 21 | } 22 | } 23 | 24 | export const Logo = withTheme(LogoComponent) 25 | -------------------------------------------------------------------------------- /webapp/src/Components/Shared/Logo/calculated-logo-birthday-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/src/Components/Shared/Logo/calculated-logo-birthday-light.png -------------------------------------------------------------------------------- /webapp/src/Components/Shared/Logo/calculated-logo-birthday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/src/Components/Shared/Logo/calculated-logo-birthday.png -------------------------------------------------------------------------------- /webapp/src/Components/Shared/Logo/calculated-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/src/Components/Shared/Logo/calculated-logo-light.png -------------------------------------------------------------------------------- /webapp/src/Components/Shared/Logo/calculated-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaltieRL/DistributedReplays/9e4d2d506ab388a944c0ebc7f76b83dc6359195d/webapp/src/Components/Shared/Logo/calculated-logo.png -------------------------------------------------------------------------------- /webapp/src/Components/Shared/Notification/NotificationTestButton.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from "@material-ui/core" 2 | import * as React from "react" 3 | import {connect} from "react-redux" 4 | import {Dispatch} from "redux" 5 | import {NotificationActions} from "../../../Redux" 6 | import {NotificationProps} from "./NotificationSnackbar" 7 | 8 | const mapDispatchToProps = (dispatch: Dispatch) => ({ 9 | showNotification: (notificationProps: NotificationProps) => 10 | dispatch(NotificationActions.showNotifictionAction(notificationProps)) 11 | }) 12 | 13 | type Props = ReturnType 14 | 15 | interface State { 16 | count: number 17 | } 18 | 19 | class NotificationTestButtonComponent extends React.PureComponent { 20 | constructor(props: Props) { 21 | super(props) 22 | this.state = {count: 0} 23 | } 24 | 25 | public render() { 26 | return ( 27 | 30 | ) 31 | } 32 | 33 | private readonly showNotification = () => { 34 | this.props.showNotification(this.createNotification()) 35 | this.setState({count: this.state.count + 1}) 36 | } 37 | 38 | private readonly createNotification = (): NotificationProps => { 39 | return { 40 | variant: "success", 41 | message: `Success notification ${this.state.count}`, 42 | timeout: 1000 43 | } 44 | } 45 | } 46 | 47 | export const NotificationTestButton = connect(null, mapDispatchToProps)(NotificationTestButtonComponent) 48 | -------------------------------------------------------------------------------- /webapp/src/Components/Shared/Notification/NotificationUtils.ts: -------------------------------------------------------------------------------- 1 | import {connect} from "react-redux" 2 | import {Dispatch} from "redux" 3 | import {NotificationActions} from "../../../Redux" 4 | import {NotificationProps} from "./NotificationSnackbar" 5 | 6 | const mapDispatchToProps = (dispatch: Dispatch) => ({ 7 | showNotification: (notificationProps: NotificationProps) => 8 | dispatch(NotificationActions.showNotifictionAction(notificationProps)) 9 | }) 10 | 11 | export type WithNotifications = ReturnType 12 | 13 | export const withNotifications = () => connect(null, mapDispatchToProps) 14 | -------------------------------------------------------------------------------- /webapp/src/Components/Shared/PageContent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import {createStyles, Toolbar, WithStyles, withStyles, withWidth} from "@material-ui/core" 4 | import {isWidthUp, WithWidth} from "@material-ui/core/withWidth" 5 | 6 | const styles = createStyles({ 7 | content: { 8 | display: "flex", 9 | flexDirection: "column", 10 | width: "100%", 11 | flex: 1 12 | } 13 | }) 14 | 15 | type Props = WithStyles & WithWidth 16 | 17 | class PageContentComponent extends React.PureComponent { 18 | public render() { 19 | const {classes} = this.props 20 | const aboveSm = isWidthUp("sm", this.props.width) 21 | return ( 22 |
23 | 24 |
31 | {this.props.children} 32 |
33 |
34 | ) 35 | } 36 | } 37 | 38 | export const PageContent = withWidth()(withStyles(styles)(PageContentComponent)) 39 | -------------------------------------------------------------------------------- /webapp/src/Components/Shared/Tag/UserTagDisplay.tsx: -------------------------------------------------------------------------------- 1 | import {IconButton, List, ListItem, ListItemSecondaryAction, ListItemText} from "@material-ui/core" 2 | import Delete from "@material-ui/icons/Delete" 3 | import * as React from "react" 4 | import {withNotifications, WithNotifications} from "../Notification/NotificationUtils" 5 | import {CreateTagDialog} from "./CreateTagDialog" 6 | 7 | interface OwnProps { 8 | tags: Tag[] 9 | handleCreate: (tag: Tag) => void 10 | deleteTag: (tag: Tag) => () => void 11 | } 12 | 13 | type Props = OwnProps & WithNotifications 14 | 15 | class UserTagDisplayComponent extends React.PureComponent { 16 | public render() { 17 | return ( 18 | <> 19 | 20 | {this.props.tags.map((tag) => ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ))} 30 | 31 | 32 | 33 | ) 34 | } 35 | } 36 | 37 | export const UserTagDisplay = withNotifications()(UserTagDisplayComponent) 38 | -------------------------------------------------------------------------------- /webapp/src/Components/Shared/Upload/BakkesModAd.tsx: -------------------------------------------------------------------------------- 1 | import {Typography} from "@material-ui/core" 2 | import Info from "@material-ui/icons/Info" 3 | import * as React from "react" 4 | import {PLUGINS_LINK} from "../../../Globals" 5 | import {LinkButton} from "../LinkButton" 6 | 7 | export class BakkesModAd extends React.PureComponent { 8 | public render() { 9 | return ( 10 |
11 | A shoutout for BakkesMod 12 | 13 | Finding the upload process a bit tedious? BakkesMod can save and upload your replays automatically 14 | so you don't have to do any manual work to get these amazing stats! 15 | 16 | 17 | More info 18 | 19 |
20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /webapp/src/Components/Shared/Upload/UploadContainedButton.tsx: -------------------------------------------------------------------------------- 1 | import {Button, WithStyles, withStyles} from "@material-ui/core" 2 | import {SvgIconProps} from "@material-ui/core/SvgIcon" 3 | import * as React from "react" 4 | import {buttonStyles} from "../LinkButton" 5 | 6 | interface OwnProps { 7 | handleOpen: () => void 8 | Icon: React.ComponentType 9 | } 10 | 11 | type Props = OwnProps & WithStyles 12 | 13 | class UploadContainedButtonComponent extends React.PureComponent { 14 | public render() { 15 | const {Icon, handleOpen} = this.props 16 | return ( 17 | <> 18 | 22 | 23 | ) 24 | } 25 | } 26 | 27 | export const UploadContainedButton = withStyles(buttonStyles)(UploadContainedButtonComponent) 28 | -------------------------------------------------------------------------------- /webapp/src/Components/Shared/Upload/UploadDialog.tsx: -------------------------------------------------------------------------------- 1 | import {Dialog, DialogTitle, Divider} from "@material-ui/core" 2 | import * as React from "react" 3 | import {PreviousUploads} from "./PreviousUploads" 4 | import {UploadForm} from "./UploadForm" 5 | import {UploadTab, UploadTabs} from "./UploadTabs" 6 | 7 | interface Props { 8 | open: boolean 9 | handleClickOutside: () => void 10 | } 11 | 12 | interface State { 13 | selectedTab: UploadTab 14 | } 15 | 16 | export class UploadDialog extends React.PureComponent { 17 | constructor(props: Props) { 18 | super(props) 19 | this.state = {selectedTab: "Upload Replays"} 20 | } 21 | 22 | public render() { 23 | return ( 24 | 30 | 31 | 32 | 33 | 34 | {this.state.selectedTab === "Upload Replays" ? : } 35 | 36 | ) 37 | } 38 | 39 | private readonly handleTabChange = (_: React.ChangeEvent<{}>, selectedTab: UploadTab) => { 40 | this.setState({selectedTab}) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /webapp/src/Components/Shared/Upload/UploadFloatingButton.tsx: -------------------------------------------------------------------------------- 1 | import {Fab, Tooltip} from "@material-ui/core" 2 | import {SvgIconProps} from "@material-ui/core/SvgIcon" 3 | import * as React from "react" 4 | 5 | interface Props { 6 | handleOpen: () => void 7 | Icon: React.ComponentType 8 | } 9 | 10 | export class UploadFloatingButton extends React.PureComponent { 11 | public render() { 12 | const {Icon, handleOpen} = this.props 13 | const buttonStyle: React.CSSProperties = { 14 | position: "fixed", 15 | bottom: "60px", 16 | right: "60px" 17 | } 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /webapp/src/Components/Shared/Upload/UploadTabs.tsx: -------------------------------------------------------------------------------- 1 | import {Tab, Tabs} from "@material-ui/core" 2 | import * as React from "react" 3 | 4 | export type UploadTab = "Upload Replays" | "Previous Uploads" 5 | const uploadTabs: UploadTab[] = ["Upload Replays", "Previous Uploads"] 6 | 7 | interface Props { 8 | selectedTab: UploadTab 9 | handleChange: (event: React.ChangeEvent<{}>, selectedTab: UploadTab) => void 10 | } 11 | 12 | export class UploadTabs extends React.PureComponent { 13 | public render() { 14 | return ( 15 | 16 | {uploadTabs.map((uploadTab) => ( 17 | 18 | ))} 19 | 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /webapp/src/Contexts/ThemeContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | interface ThemeContextType { 4 | dark: boolean 5 | toggleTheme: () => void 6 | blueColor: string 7 | orangeColor: string 8 | } 9 | 10 | // tslint:disable-next-line:no-empty 11 | export const ThemeContext = React.createContext({ 12 | dark: false, 13 | toggleTheme: () => undefined, 14 | blueColor: "blue", 15 | orangeColor: "orange" 16 | }) 17 | -------------------------------------------------------------------------------- /webapp/src/Models/Admin/Admin.d.ts: -------------------------------------------------------------------------------- 1 | interface AdminLogsResponse { 2 | logs: AdminLog[] 3 | count: number 4 | } 5 | 6 | interface AdminLog { 7 | id: number 8 | uuid: string 9 | result: number 10 | log?: string 11 | params?: string 12 | errorType?: string 13 | game?: string 14 | } 15 | -------------------------------------------------------------------------------- /webapp/src/Models/ChartData.ts: -------------------------------------------------------------------------------- 1 | export interface ChartDataPoint { 2 | name: string 3 | value: number 4 | average?: number 5 | } 6 | 7 | export interface ChartDataResponse { 8 | title: string 9 | chartDataPoints: ChartDataPoint[] 10 | } 11 | 12 | export interface StatDataPoint extends ChartDataPoint { 13 | isOrange: boolean 14 | } 15 | 16 | export interface BasicStat extends ChartDataResponse { 17 | chartDataPoints: StatDataPoint[] 18 | type: "radar" | "bar" | "pie" 19 | subcategory: StatsSubcategory 20 | } 21 | 22 | export enum PlayerStatsSubcategory { 23 | MAIN_STATS = "Main Stats", 24 | POSITIONING = "Positioning", 25 | HITS = "Hits", 26 | BALL = "Ball", 27 | BOOSTS = "Boosts", 28 | PLAYSTYLES = "Playstyles", 29 | POSSESSION = "Possession", 30 | EFFICIENCY = "Efficiency", 31 | TEAM_POSITIONING = "Team Positioning", 32 | BALL_CARRIES = "Ball Carries", 33 | KICKOFFS = "Kickoffs" 34 | } 35 | 36 | export enum TeamStatsSubcategory { 37 | POSITIONING = "Positioning", 38 | CENTER_OF_MASS = "Center of Mass" 39 | } 40 | 41 | export enum HeatmapSubcategory { 42 | POSITIONING = "Positioning", 43 | // HITS = "Hits", 44 | BOOST = "Boost", 45 | BOOST_SPEED = "Boost Speed", 46 | SLOW_SPEED = "Slow Speed" 47 | } 48 | 49 | export type StatsSubcategory = PlayerStatsSubcategory | TeamStatsSubcategory 50 | -------------------------------------------------------------------------------- /webapp/src/Models/ItemStats.d.ts: -------------------------------------------------------------------------------- 1 | interface ItemListResponse { 2 | items: Item[] 3 | count: number 4 | } 5 | 6 | interface Item { 7 | image: string 8 | ingameid: number 9 | name: string 10 | rarity: number 11 | count?: number 12 | } 13 | 14 | export interface ItemFull { 15 | category: number 16 | description: null 17 | dlcpack: null 18 | edition: null 19 | hascoloredicons: number 20 | id: number 21 | image: string 22 | ingameid: number 23 | isteamitem: number 24 | name: string 25 | ownedby: null 26 | parent: null 27 | parentitem: null 28 | photo: string 29 | platform: string 30 | rarity: number 31 | shortname: string 32 | tradable: number 33 | translations: any 34 | unlockmethod: number 35 | } 36 | 37 | export interface ItemUsage { 38 | data: ItemDataPoint[] 39 | } 40 | 41 | export interface ItemDataPoint { 42 | count: number 43 | date: string 44 | total: number 45 | } 46 | -------------------------------------------------------------------------------- /webapp/src/Models/Player/MatchHistory.ts: -------------------------------------------------------------------------------- 1 | import {Replay} from "../Replay/Replay" 2 | 3 | export interface MatchHistoryResponse { 4 | totalCount: number 5 | replays: Replay[] 6 | } 7 | -------------------------------------------------------------------------------- /webapp/src/Models/Player/PlayStyle.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment" 2 | import {ChartDataResponse} from "../ChartData" 3 | 4 | export interface PlayStyleResponse { 5 | showWarning: boolean 6 | chartDatas: ChartDataResponse[] 7 | } 8 | 9 | export interface PlayStyleRawResponse { 10 | dataPoints: RawDataPoint[] 11 | name: string 12 | } 13 | 14 | interface RawDataPoint { 15 | name: string 16 | average: number 17 | } 18 | 19 | interface DataPoint { 20 | average: number 21 | name: string 22 | stdDev: number | null 23 | } 24 | 25 | export interface PlayStyleProgressionPoint { 26 | date: moment.Moment 27 | dataPoints: DataPoint[] 28 | } 29 | 30 | export const parsePlayStyleProgression = (data: any) => { 31 | return { 32 | ...data, 33 | date: moment(data.date) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /webapp/src/Models/Player/Player.d.ts: -------------------------------------------------------------------------------- 1 | interface Player { 2 | name: string 3 | pastNames: string[] 4 | id: string 5 | platform: string 6 | profileLink: string 7 | avatarLink: string 8 | groups: number[] 9 | } 10 | -------------------------------------------------------------------------------- /webapp/src/Models/Player/PlayerStats.d.ts: -------------------------------------------------------------------------------- 1 | interface PlayerInCommonStat { 2 | count: number 3 | id: string 4 | name: string 5 | avatar: string 6 | } 7 | 8 | interface CarStat { 9 | carName: string 10 | carPercentage: number 11 | } 12 | 13 | interface PlayerStats { 14 | car: CarStat 15 | playersInCommon: PlayerInCommonStat[] 16 | loadout: Loadout 17 | } 18 | -------------------------------------------------------------------------------- /webapp/src/Models/Player/TrainingPack.d.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment" 2 | import {CompactReplay} from ".." 3 | 4 | interface TrainingPackResponse { 5 | packs: TrainingPack[] 6 | totalCount: number 7 | games: Record 8 | } 9 | 10 | interface TrainingPack { 11 | guid: string 12 | shots: TrainingPackShot[] 13 | date: moment.Moment 14 | link: string 15 | name: string 16 | } 17 | 18 | interface TrainingPackShot { 19 | frame: number 20 | game: string 21 | timeRemaining: number 22 | } 23 | -------------------------------------------------------------------------------- /webapp/src/Models/Player/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MatchHistory" 2 | export * from "./PlayStyle" 3 | -------------------------------------------------------------------------------- /webapp/src/Models/Replay/Groups.d.ts: -------------------------------------------------------------------------------- 1 | import {Replay} from "./Replay" 2 | 3 | export interface GroupResponse { 4 | ancestors: Entry[] 5 | children: Entry[] 6 | entry: Entry 7 | } 8 | 9 | export interface Entry { 10 | game: null | string 11 | gameObject: null | Replay 12 | name: string 13 | owner: Player 14 | parent: null | string 15 | type: number 16 | uuid: string 17 | descendantCount: number 18 | } 19 | 20 | type Stats = Record 21 | type AllNamedStats = Record // i.e. (Total), (per Game) 22 | 23 | export interface GroupPlayerStatsResponse { 24 | playerStats: AllGroupPlayerStats[] 25 | } 26 | export interface AllGroupPlayerStats { 27 | name: string 28 | player: string 29 | stats: AllNamedStats 30 | } 31 | export interface GroupPlayerStats extends AllGroupPlayerStats { 32 | stats: Stats 33 | } 34 | 35 | // TEAMS 36 | 37 | export interface GroupTeamStatsResponse { 38 | teamStats: TeamStat[] 39 | } 40 | export interface AllTeamStats { 41 | games: string[] 42 | names: string[] 43 | stats: AllNamedStats 44 | team: string[] 45 | } 46 | export interface TeamStats extends AllTeamStats { 47 | stats: Stats 48 | } 49 | export interface UUIDResponse { 50 | uuid: string 51 | } 52 | -------------------------------------------------------------------------------- /webapp/src/Models/Replay/KickoffData.d.ts: -------------------------------------------------------------------------------- 1 | interface KickoffPlayers { 2 | [key: string]: KickoffPlayer 3 | } 4 | 5 | interface KickoffData { 6 | kickoffs: Kickoff[] 7 | players: KickoffPlayers 8 | } 9 | 10 | interface Kickoff { 11 | first_touch: string 12 | kickoff_type: string 13 | players: KickoffPlayerElement[] 14 | time_till_goal: number 15 | touch_time: number 16 | } 17 | 18 | interface KickoffPlayerElement { 19 | ball_distance: number 20 | boost_level: number 21 | end: End 22 | jump_times: number[] 23 | jumps: number 24 | location: Location 25 | player_id: string 26 | start: End 27 | time_to_boost: number 28 | } 29 | 30 | interface End { 31 | x: number 32 | y: number 33 | } 34 | 35 | enum Location { 36 | Ball = "BALL", 37 | Boost = "BOOST", 38 | Cheat = "CHEAT", 39 | Goal = "GOAL", 40 | Unknown = "UNKNOWN" 41 | } 42 | 43 | interface KickoffPlayer { 44 | is_orange: number 45 | name: string 46 | } 47 | -------------------------------------------------------------------------------- /webapp/src/Models/Replay/PredictedRank.d.ts: -------------------------------------------------------------------------------- 1 | interface PredictedRank { 2 | id: string 3 | predictedRank: number 4 | } 5 | -------------------------------------------------------------------------------- /webapp/src/Models/Replay/Replay.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment" 2 | 3 | export type GameMode = "1's" | "2's" | "3's" 4 | 5 | interface GameScore { 6 | team0Score: number 7 | team1Score: number 8 | } 9 | 10 | export enum GameVisibility { 11 | DEFAULT = 0, 12 | PUBLIC = 1, 13 | PRIVATE = 2 14 | } 15 | 16 | export interface Replay { 17 | id: string 18 | name: string 19 | date: moment.Moment 20 | map: string 21 | gameMode: GameMode 22 | gameScore: GameScore 23 | players: ReplayPlayer[] 24 | tags: Tag[] 25 | visibility: GameVisibility 26 | ranks: number[] 27 | mmrs: number[] 28 | groupMap: any 29 | } 30 | 31 | export interface CompactReplay { 32 | id: string 33 | date: moment.Moment 34 | gameMode: GameMode 35 | gameScore: GameScore 36 | } 37 | 38 | export const parseReplay = (data: any) => { 39 | return { 40 | ...data, 41 | date: moment(data.date) 42 | } 43 | } 44 | 45 | type GameResult = "Win" | "Loss" 46 | 47 | export const getReplayResult = (replay: Replay, player: Player): GameResult => { 48 | const playerIsOrange = (replay.players.find((replayPlayer) => replayPlayer.id === player.id) || ({} as any))! 49 | .isOrange 50 | const winnerIsOrange = replay.gameScore.team1Score > replay.gameScore.team0Score 51 | return winnerIsOrange === playerIsOrange ? "Win" : "Loss" 52 | } 53 | -------------------------------------------------------------------------------- /webapp/src/Models/Replay/ReplayPlayer.d.ts: -------------------------------------------------------------------------------- 1 | interface ReplayPlayer { 2 | id: string 3 | name: string 4 | isOrange: boolean 5 | score: number 6 | goals: number 7 | assists: number 8 | saves: number 9 | shots: number 10 | cameraSettings: CameraSettings 11 | loadout: Loadout 12 | mmr: number 13 | rank: number 14 | } 15 | 16 | interface CameraSettings { 17 | distance: number 18 | fieldOfView: number 19 | transitionSpeed: number 20 | pitch: number 21 | swivelSpeed: number 22 | stiffness: number 23 | height: number 24 | } 25 | 26 | interface Loadout { 27 | antenna: LoadoutItem 28 | banner: LoadoutItem 29 | boost: LoadoutItem 30 | car: LoadoutItem 31 | engine_audio: LoadoutItem 32 | goal_explosion: LoadoutItem 33 | skin: LoadoutItem 34 | topper: LoadoutItem 35 | trail: LoadoutItem 36 | wheels: LoadoutItem 37 | } 38 | 39 | interface LoadoutItem { 40 | itemName: string 41 | imageUrl: string 42 | paintId: number 43 | rarity: number 44 | } 45 | -------------------------------------------------------------------------------- /webapp/src/Models/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Player" 2 | export * from "./Replay/Replay" 3 | export * from "./ChartData" 4 | export * from "./ReplaysSearchQueryParams" 5 | -------------------------------------------------------------------------------- /webapp/src/Models/types/Error.d.ts: -------------------------------------------------------------------------------- 1 | interface AppError { 2 | code?: number 3 | message: string 4 | } 5 | -------------------------------------------------------------------------------- /webapp/src/Models/types/GlobalStatsData.d.ts: -------------------------------------------------------------------------------- 1 | interface GlobalStatsGraphDataset { 2 | keys: number[] 3 | values: number[] 4 | name: string 5 | } 6 | 7 | interface GlobalStatsGraph { 8 | name: string 9 | data: GlobalStatsGraphDataset[] 10 | } 11 | -------------------------------------------------------------------------------- /webapp/src/Models/types/Homepage.d.ts: -------------------------------------------------------------------------------- 1 | import {CompactReplay} from ".." 2 | 3 | interface Stream { 4 | name: string 5 | game: string 6 | thumbnail: string 7 | title: string 8 | viewers: number 9 | } 10 | 11 | interface StreamResponse { 12 | streams: Stream[] 13 | } 14 | 15 | interface PatreonResponse { 16 | progress: number 17 | total: number 18 | } 19 | 20 | interface RecentReplaysResponse { 21 | recent: CompactReplay[] 22 | } 23 | -------------------------------------------------------------------------------- /webapp/src/Models/types/Leaderboards.d.ts: -------------------------------------------------------------------------------- 1 | interface Leader { 2 | name: string 3 | id_: string 4 | count: number 5 | avatar: string 6 | } 7 | 8 | interface DurationLeaders { 9 | month: Leader[] 10 | week: Leader[] 11 | } 12 | 13 | interface PlaylistLeaderboard { 14 | playlist: int 15 | leaders: DurationLeaders 16 | } 17 | -------------------------------------------------------------------------------- /webapp/src/Models/types/LoggedInUser.d.ts: -------------------------------------------------------------------------------- 1 | interface LoggedInUser { 2 | name: string 3 | id: string 4 | avatarLink: string 5 | admin: boolean 6 | alpha: boolean 7 | beta: boolean 8 | } 9 | -------------------------------------------------------------------------------- /webapp/src/Models/types/QueueLengths.d.ts: -------------------------------------------------------------------------------- 1 | interface QueueStatus { 2 | name: string 3 | priority: number 4 | count: number 5 | } 6 | -------------------------------------------------------------------------------- /webapp/src/Models/types/Tag.d.ts: -------------------------------------------------------------------------------- 1 | interface Tag { 2 | name: string 3 | ownerId: string 4 | privateKey: null | string 5 | } 6 | -------------------------------------------------------------------------------- /webapp/src/Models/types/UploadStatus.d.ts: -------------------------------------------------------------------------------- 1 | type UploadStatus = "PENDING" | "STARTED" | "RETRY" | "FAILURE" | "SUCCESS" 2 | -------------------------------------------------------------------------------- /webapp/src/Models/types/VisibilityResponse.d.ts: -------------------------------------------------------------------------------- 1 | import {GameVisibility} from ".." 2 | 3 | interface VisibilityResponse { 4 | id: string 5 | visibility: GameVisibility 6 | } 7 | -------------------------------------------------------------------------------- /webapp/src/Redux/index.ts: -------------------------------------------------------------------------------- 1 | import {combineReducers, createStore, Reducer} from "redux" 2 | import {devToolsEnhancer} from "redux-devtools-extension" 3 | import {loggedInUserReducer, LoggedInUserState} from "./loggedInUser/reducer" 4 | import {notificationsReducer, NotificationsState} from "./notifications/reducer" 5 | import {tagsReducer, TagsState} from "./tags/reducer" 6 | 7 | export interface StoreState { 8 | loggedInUser: LoggedInUserState 9 | notifications: NotificationsState 10 | tags: TagsState 11 | } 12 | 13 | const rootReducer: Reducer = combineReducers({ 14 | loggedInUser: loggedInUserReducer as any, 15 | notifications: notificationsReducer as any, 16 | tags: tagsReducer as any 17 | }) 18 | 19 | export const store = createStore(rootReducer, devToolsEnhancer({})) 20 | 21 | export * from "./loggedInUser/actions" 22 | export * from "./notifications/actions" 23 | export * from "./tags/actions" 24 | -------------------------------------------------------------------------------- /webapp/src/Redux/loggedInUser/actions.ts: -------------------------------------------------------------------------------- 1 | import {createAction} from "redux-actions" 2 | 3 | enum Type { 4 | SET_LOGGED_IN_USER = "SET_LOGGED_IN_USER" 5 | } 6 | 7 | export const LoggedInUserActions = { 8 | Type, 9 | setLoggedInUserAction: createAction(Type.SET_LOGGED_IN_USER) 10 | } 11 | -------------------------------------------------------------------------------- /webapp/src/Redux/loggedInUser/reducer.ts: -------------------------------------------------------------------------------- 1 | import {Action, handleActions} from "redux-actions" 2 | import {LoggedInUserActions} from "./actions" 3 | 4 | export type LoggedInUserState = LoggedInUser | null 5 | 6 | const initialState: LoggedInUserState = null 7 | 8 | export const loggedInUserReducer = handleActions( 9 | { 10 | [LoggedInUserActions.Type.SET_LOGGED_IN_USER]: (state, action: Action): LoggedInUserState => { 11 | return action.payload || null 12 | } 13 | }, 14 | initialState 15 | ) 16 | -------------------------------------------------------------------------------- /webapp/src/Redux/notifications/actions.ts: -------------------------------------------------------------------------------- 1 | import {createAction} from "redux-actions" 2 | import {NotificationProps} from "../../Components/Shared/Notification/NotificationSnackbar" 3 | 4 | enum Type { 5 | SHOW_NOTIFICATION = "SHOW_NOTIFICATION", 6 | DISMISS_NOTIFICATION = "DISMISS_NOTIFICATION" 7 | } 8 | 9 | export const NotificationActions = { 10 | Type, 11 | showNotifictionAction: createAction(Type.SHOW_NOTIFICATION), 12 | dismissNotificationAction: createAction(Type.DISMISS_NOTIFICATION) 13 | } 14 | -------------------------------------------------------------------------------- /webapp/src/Redux/notifications/reducer.ts: -------------------------------------------------------------------------------- 1 | import {Action, handleActions} from "redux-actions" 2 | import {NotificationProps} from "../../Components/Shared/Notification/NotificationSnackbar" 3 | import {NotificationActions} from "./actions" 4 | 5 | export type NotificationsState = NotificationProps[] 6 | 7 | const initialState: NotificationsState = [] 8 | 9 | export const notificationsReducer = handleActions( 10 | { 11 | [NotificationActions.Type.SHOW_NOTIFICATION]: ( 12 | state: NotificationsState, 13 | action: Action 14 | ): NotificationsState => { 15 | if (action.payload) { 16 | return [...state, action.payload] 17 | } 18 | return state 19 | }, 20 | [NotificationActions.Type.DISMISS_NOTIFICATION]: (state: NotificationsState, _): NotificationsState => { 21 | return state.slice(1) 22 | } 23 | }, 24 | initialState 25 | ) 26 | -------------------------------------------------------------------------------- /webapp/src/Redux/tags/actions.ts: -------------------------------------------------------------------------------- 1 | import {createAction} from "redux-actions" 2 | 3 | enum Type { 4 | SET_TAGS = "SET_TAGS", 5 | ADD_PRIVATE_KEY_TO_TAG = "ADD_PRIVATE_KEY_TO_TAG" 6 | } 7 | 8 | export const TagsAction = { 9 | Type, 10 | setTagsAction: createAction(Type.SET_TAGS), 11 | addPrivateKeyToTagAction: createAction(Type.ADD_PRIVATE_KEY_TO_TAG) 12 | } 13 | -------------------------------------------------------------------------------- /webapp/src/Redux/tags/reducer.ts: -------------------------------------------------------------------------------- 1 | import {Action, handleActions} from "redux-actions" 2 | import {TagsAction} from "./actions" 3 | 4 | export type TagsState = Tag[] | null 5 | 6 | const initialState: TagsState = null 7 | 8 | export const tagsReducer = handleActions( 9 | { 10 | [TagsAction.Type.SET_TAGS]: (state, action: Action): TagsState => { 11 | if (action.payload) { 12 | return action.payload 13 | } 14 | return state 15 | }, 16 | [TagsAction.Type.ADD_PRIVATE_KEY_TO_TAG]: (state, action: Action): TagsState => { 17 | if (action.payload !== undefined && state !== null) { 18 | return state.map((tag) => { 19 | if (tag.name === action.payload!.name && tag.ownerId === action.payload!.ownerId) { 20 | return {...tag, privateKey: action.payload!.privateKey} 21 | } else { 22 | return tag 23 | } 24 | }) 25 | } 26 | return state 27 | } 28 | }, 29 | initialState 30 | ) 31 | -------------------------------------------------------------------------------- /webapp/src/Requests/Config.ts: -------------------------------------------------------------------------------- 1 | export const baseUrl = "/api" 2 | export const useLiveQueries = false 3 | -------------------------------------------------------------------------------- /webapp/src/Requests/Documentation.ts: -------------------------------------------------------------------------------- 1 | import {doGet} from "../apiHandler/apiHandler" 2 | 3 | export const getDocumentation = (): Promise => { 4 | return doGet(`/documentation`) 5 | } 6 | -------------------------------------------------------------------------------- /webapp/src/Requests/Home.ts: -------------------------------------------------------------------------------- 1 | import {doGet} from "../apiHandler/apiHandler" 2 | import {parseReplay} from "../Models" 3 | import {PatreonResponse, RecentReplaysResponse, StreamResponse} from "../Models/types/Homepage" 4 | 5 | export const getTwitchStreams = (): Promise => doGet("/home/twitch") 6 | 7 | export const getPatreonProgress = (): Promise => doGet("/home/patreon") 8 | 9 | export const getRecentReplays = async (): Promise => { 10 | const data = await doGet("/home/recent") 11 | if (data) { 12 | return { 13 | recent: data.recent.map(parseReplay) 14 | } 15 | } 16 | return {recent: []} 17 | } 18 | -------------------------------------------------------------------------------- /webapp/src/Requests/Player/getMatchHistory.ts: -------------------------------------------------------------------------------- 1 | import {doGet} from "../../apiHandler/apiHandler" 2 | import {MatchHistoryResponse, parseReplay} from "../../Models" 3 | 4 | export const getMatchHistory = async (id: string, page: number, limit: number): Promise => { 5 | const data = await doGet(`/player/${id}/match_history?page=${page}&limit=${limit}`) 6 | return {...data, replays: data.replays.map(parseReplay)} 7 | } 8 | -------------------------------------------------------------------------------- /webapp/src/Requests/Player/getPlayStyle.ts: -------------------------------------------------------------------------------- 1 | import qs from "qs" 2 | import {doGet} from "../../apiHandler/apiHandler" 3 | import {PlayStyleRawResponse, PlayStyleResponse} from "../../Models" 4 | 5 | export const getPlayStyle = ( 6 | id: string, 7 | rank?: number, 8 | playlist?: number, 9 | result?: boolean 10 | ): Promise => { 11 | let params 12 | if (result === undefined) { 13 | params = {rank, playlist} 14 | } else { 15 | params = {rank, playlist, result: result ? "win" : "loss"} 16 | } 17 | const url = qs.stringify(params, {addQueryPrefix: true, indices: false}) 18 | return doGet(`/player/${id}/play_style` + url) 19 | } 20 | 21 | export const getPlayStyleRaw = (id: string, playlist?: number): Promise => { 22 | const url = qs.stringify({playlist}, {addQueryPrefix: true, indices: false}) 23 | return doGet(`/player/${id}/play_style/all` + url) 24 | } 25 | -------------------------------------------------------------------------------- /webapp/src/Requests/Player/getPlayer.ts: -------------------------------------------------------------------------------- 1 | import {doGet} from "../../apiHandler/apiHandler" 2 | 3 | export const getPlayer = (id: string): Promise => doGet(`/player/${id}/profile`) 4 | -------------------------------------------------------------------------------- /webapp/src/Requests/Player/getProgression.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment" 2 | import {doGet} from "../../apiHandler/apiHandler" 3 | import {TimeUnit} from "../../Components/Player/Compare/Progression/PlayerProgressionCharts" 4 | import {parsePlayStyleProgression, PlayStyleProgressionPoint} from "../../Models" 5 | import {QueryParamMetadata, stringifyQueryParams} from "../Utils" 6 | 7 | interface ProgressionQueryParams { 8 | timeUnit?: TimeUnit 9 | startDate?: moment.Moment 10 | endDate?: moment.Moment 11 | playlist?: number 12 | } 13 | 14 | const progressionQueryParamMetadatas: QueryParamMetadata[] = [ 15 | {name: "timeUnit", optional: true}, 16 | {name: "startDate", isDate: true, optional: true}, 17 | {name: "endDate", isDate: true, optional: true}, 18 | {name: "playlist", optional: true} 19 | ] 20 | export const getProgression = async ( 21 | id: string, 22 | queryParams: ProgressionQueryParams 23 | ): Promise => { 24 | return doGet( 25 | `/player/${id}/play_style/progression` + stringifyQueryParams(queryParams, progressionQueryParamMetadatas) 26 | ).then((data) => data.map(parsePlayStyleProgression)) 27 | } 28 | -------------------------------------------------------------------------------- /webapp/src/Requests/Player/getRanks.ts: -------------------------------------------------------------------------------- 1 | import {doGet} from "../../apiHandler/apiHandler" 2 | import {PlayerRanks} from "../../Components/Player/Overview/SideBar/PlayerRanksCard" 3 | 4 | export const getRanks = (id: string): Promise => doGet(`/player/${id}/ranks`) 5 | -------------------------------------------------------------------------------- /webapp/src/Requests/Player/getStats.ts: -------------------------------------------------------------------------------- 1 | import {doGet} from "../../apiHandler/apiHandler" 2 | 3 | export const getStats = (id: string): Promise => doGet(`/player/${id}/profile_stats`) 4 | -------------------------------------------------------------------------------- /webapp/src/Requests/Player/resolvePlayerNameOrId.ts: -------------------------------------------------------------------------------- 1 | // Checks if nameOrId exists by querying backend. Resolves name to Id. 2 | import {doGet} from "../../apiHandler/apiHandler" 3 | 4 | export const resolvePlayerNameOrId = (nameOrId: string): Promise => doGet(`/player/${nameOrId}`) 5 | -------------------------------------------------------------------------------- /webapp/src/Requests/Tag.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment" 2 | import {doGet, doRequest} from "../apiHandler/apiHandler" 3 | 4 | export const createTag = (name: string): Promise => { 5 | return doRequest(`/tag/${name}`, {method: "PUT"}) 6 | } 7 | 8 | export const renameTag = (currentName: string, newName: string): Promise => { 9 | return doRequest(`/tag/${currentName}?new_name=${newName}`, {method: "PATCH"}) 10 | } 11 | 12 | export const deleteTag = (name: string): Promise => { 13 | return doRequest(`/tag/${name}`, {method: "DELETE"}) 14 | } 15 | 16 | export const getAllTags = (): Promise => { 17 | return doGet(`/tag`) 18 | } 19 | 20 | export const addTagToGame = (name: string, replayId: string): Promise => { 21 | return doRequest(`/tag/${name}/replay/${replayId}`, {method: "PUT"}) 22 | } 23 | 24 | export const removeTagFromGame = (name: string, replayId: string): Promise => { 25 | return doRequest(`/tag/${name}/replay/${replayId}`, {method: "DELETE"}) 26 | } 27 | 28 | export const generateTagPrivateID = (tag: Tag): Promise => { 29 | const time = moment().valueOf() 30 | return doRequest(`/tag/${tag.name}/private_key/${time}`, {method: "PUT"}) 31 | } 32 | export const getPrivateTagKey = (tag: Tag): Promise => { 33 | return doGet(`/tag/${tag.name}/private_key`) 34 | } 35 | 36 | export const generateTagPrivateIdAndGetKey = (tag: Tag): Promise => { 37 | return generateTagPrivateID(tag).then(() => getPrivateTagKey(tag)) 38 | } 39 | -------------------------------------------------------------------------------- /webapp/src/Requests/Utils.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash" 2 | import qs from "qs" 3 | 4 | export interface QueryParamMetadata { 5 | name: string 6 | isDate?: boolean 7 | optional?: boolean 8 | } 9 | 10 | export const stringifyQueryParams = (queryParams: any, queryParamMetadatas: QueryParamMetadata[]): string => { 11 | const parsedQueryParams: object = _.fromPairs( 12 | queryParamMetadatas 13 | .filter( 14 | (queryParamMetadata) => 15 | !queryParamMetadata.optional || queryParams[queryParamMetadata.name] !== undefined 16 | ) 17 | .map((queryParamMetadata) => { 18 | let queryParamValue = queryParams[queryParamMetadata.name] 19 | if (queryParamMetadata.isDate) { 20 | queryParamValue = queryParamValue.unix() 21 | } 22 | return [_.snakeCase(queryParamMetadata.name), queryParamValue] 23 | }) 24 | ) 25 | 26 | return qs.stringify(parsedQueryParams, {arrayFormat: "repeat", addQueryPrefix: true}) 27 | } 28 | -------------------------------------------------------------------------------- /webapp/src/Utils/Chart.ts: -------------------------------------------------------------------------------- 1 | import {ChartData, ChartTooltipItem} from "chart.js" 2 | import {roundNumberToMaxDP} from "./String" 3 | 4 | export const roundLabelToMaxDPCallback = (tooltipItem: ChartTooltipItem, data: ChartData, decimalPoints?: number) => { 5 | let label = data.datasets![tooltipItem.datasetIndex!].label || "" 6 | if (label !== "") { 7 | label += ": " 8 | } 9 | label += roundNumberToMaxDP(Number(tooltipItem.yLabel!), decimalPoints) 10 | return label 11 | } 12 | -------------------------------------------------------------------------------- /webapp/src/Utils/CopyToClipboard/clipboard.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) Feross Aboukhadijeh 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | 24 | declare function clipboardCopy(text: string): Promise 25 | 26 | export = clipboardCopy 27 | -------------------------------------------------------------------------------- /webapp/src/Utils/String.ts: -------------------------------------------------------------------------------- 1 | import Filter from "bad-words" 2 | 3 | export const convertSnakeAndCamelCaseToReadable = (camelCaseString: string) => { 4 | const words = camelCaseString.match(/([A-Za-z%][a-z%]*)|(\([A-Za-z ]*\))/g) || [] 5 | return words.map((word: string) => word.charAt(0).toUpperCase() + word.substr(1)).join(" ") 6 | } 7 | 8 | export const roundNumberToMaxDP = (value: number, decimalPoints: number = 2) => 9 | (Math.round(value * 10 ** decimalPoints) / 10 ** decimalPoints).toString() 10 | 11 | export const sanitizeProfanity = (unsanitized: string): string => new Filter().clean(unsanitized) 12 | -------------------------------------------------------------------------------- /webapp/src/Utils/types/bad-words.d.ts: -------------------------------------------------------------------------------- 1 | declare module "bad-words" { 2 | class Filter { 3 | public clean(input: string): string 4 | } 5 | export default Filter 6 | } 7 | -------------------------------------------------------------------------------- /webapp/src/WrappedApp.test.tsx: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/extend-expect" 2 | import {render} from "@testing-library/react" 3 | import * as React from "react" 4 | import {WrappedApp} from "./WrappedApp" 5 | 6 | test("full render", async () => { 7 | const app = render() 8 | 9 | expect(app) 10 | }) 11 | -------------------------------------------------------------------------------- /webapp/src/WrappedApp.tsx: -------------------------------------------------------------------------------- 1 | import MomentUtils from "@date-io/moment" 2 | import CssBaseline from "@material-ui/core/CssBaseline" 3 | import {MuiPickersUtilsProvider} from "@material-ui/pickers" 4 | import * as React from "react" 5 | import {Provider} from "react-redux" 6 | import {App} from "./App" 7 | import {store} from "./Redux" 8 | import {Theme} from "./Theme" 9 | 10 | export class WrappedApp extends React.Component { 11 | public render() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /webapp/src/apiHandler/apiHandler.ts: -------------------------------------------------------------------------------- 1 | import {baseUrl, useLiveQueries} from "../Requests/Config" 2 | 3 | const getUrl = (destination: string) => 4 | useLiveQueries ? "https://calculated.gg" + baseUrl + destination : baseUrl + destination 5 | 6 | export const doGet = async (destination: string): Promise => { 7 | return fetch(getUrl(destination), { 8 | method: "GET", 9 | headers: { 10 | Accept: "application/json", 11 | "Content-Type": "application/json" 12 | } 13 | }).then(handleResponse) 14 | } 15 | 16 | export const doPost = async (destination: string, body: BodyInit): Promise => { 17 | return fetch(getUrl(destination), { 18 | method: "POST", 19 | body 20 | }).then(handleResponse) 21 | } 22 | 23 | export const doRequest = async (destination: string, requestInit: RequestInit): Promise => { 24 | return fetch(getUrl(destination), requestInit).then(handleResponse) 25 | } 26 | 27 | const handleResponse = async (response: Response): Promise => { 28 | if (!response.ok) { 29 | const code = response.status 30 | let message: string = response.statusText 31 | return response 32 | .json() 33 | .catch(() => { 34 | // eslint-disable-next-line 35 | throw {code, message} as AppError 36 | }) 37 | .then((responseJson: any) => { 38 | if (responseJson.message) { 39 | message = responseJson.message 40 | } 41 | }) 42 | .then(() => { 43 | // eslint-disable-next-line 44 | throw {code, message} as AppError 45 | }) 46 | } else { 47 | if (response.status !== 204) { 48 | return response.json() 49 | } 50 | return Promise.resolve() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /webapp/src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body, 6 | #root { 7 | margin: 0; 8 | min-height: 100vh; 9 | width: 100%; 10 | } 11 | 12 | .heatmap-canvas { 13 | position: relative !important; 14 | } 15 | 16 | a { 17 | text-decoration: none; 18 | color: inherit; 19 | } 20 | -------------------------------------------------------------------------------- /webapp/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ReactDOM from "react-dom" 3 | import "./index.css" 4 | import {unregister} from "./registerServiceWorker" 5 | import {WrappedApp} from "./WrappedApp" 6 | 7 | ReactDOM.render(, document.getElementById("root") as HTMLElement) 8 | 9 | // registerServiceWorker() 10 | unregister() 11 | -------------------------------------------------------------------------------- /webapp/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /webapp/src/test/CodeSplitComponent.test.tsx: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/extend-expect" 2 | import {render} from "@testing-library/react" 3 | import * as React from "react" 4 | 5 | import {codeSplit} from "../CodeSplitComponent" 6 | 7 | it("should render error information if async import fails", async () => { 8 | // given 9 | const Component = codeSplit(() => Promise.reject("any error on dynamic import"), "AboutPage") 10 | 11 | // when 12 | const {findByText} = render() 13 | 14 | // then 15 | expect( 16 | await findByText("Error loading AboutPage. Please check your network connection and reload this page.") 17 | ).toBeInTheDocument() 18 | }) 19 | -------------------------------------------------------------------------------- /webapp/src/test/mocks.ts: -------------------------------------------------------------------------------- 1 | export const TEST_PLAYER_ID = "TESTPLAYERID" 2 | 3 | export const mockImplementationGetPlayer = (id: string) => { 4 | switch (id) { 5 | case TEST_PLAYER_ID: 6 | return Promise.resolve({ 7 | avatarLink: 8 | "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/56/563a198ff25a301997b7c1806afdec3abf3e50e5_full.jpg", 9 | groups: [], 10 | id: TEST_PLAYER_ID, 11 | name: "Squishy", 12 | pastNames: [ 13 | "Squishy (754)", 14 | "C9 Squishy (164)", 15 | "SquishyMuffinz (154)", 16 | "Squishy Muffinz (58)", 17 | "Zodiac | SquishyMuffinz (12)" 18 | ], 19 | platform: "Steam", 20 | profileLink: "https://steamcommunity.com/id/SquishyMuffinz/" 21 | }) 22 | default: 23 | throw Error(`Unknown id in mock: ${id}`) 24 | } 25 | } 26 | export const mockImplementationGetRank = (id: string) => { 27 | switch (id) { 28 | case TEST_PLAYER_ID: 29 | return Promise.resolve({ 30 | doubles: {name: "Grand Champion III (div 3)", rank: 21, rating: 1827}, 31 | dropshot: {name: "Unranked (div 1)", rank: 0, rating: 1068}, 32 | duel: {name: "Supersonic Legend", rank: 22, rating: 1387}, 33 | hoops: {name: "Unranked (div 1)", rank: 0, rating: 1104}, 34 | rumble: {name: "Unranked (div 1)", rank: 0, rating: 1104}, 35 | snowday: {name: "Unranked (div 1)", rank: 0, rating: 1050}, 36 | standard: {name: "Grand Champion II (div 3)", rank: 20, rating: 1732}, 37 | tournament: {name: "Grand Champion III (div 1)", rank: 21, rating: 1807} 38 | }) 39 | default: 40 | throw Error(`Unknown id in mock: ${id}`) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /webapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "esnext", 5 | "target": "es5", 6 | "lib": [ 7 | "es6", 8 | "dom" 9 | ], 10 | "sourceMap": true, 11 | "allowJs": true, 12 | "allowSyntheticDefaultImports": true, 13 | "jsx": "preserve", 14 | "moduleResolution": "node", 15 | "rootDir": "src", 16 | "forceConsistentCasingInFileNames": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noImplicitAny": true, 20 | "strictNullChecks": true, 21 | "suppressImplicitAnyIndexErrors": true, 22 | "noUnusedLocals": true, 23 | "skipLibCheck": true, 24 | "esModuleInterop": true, 25 | "strict": true, 26 | "resolveJsonModule": true, 27 | "isolatedModules": true, 28 | "noEmit": true 29 | }, 30 | "exclude": [ 31 | "node_modules", 32 | "build", 33 | "scripts", 34 | "acceptance-tests", 35 | "webpack", 36 | "jest", 37 | "src/setupTests.ts" 38 | ], 39 | "include": [ 40 | "src" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /webapp/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } -------------------------------------------------------------------------------- /webapp/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /webapp/tslint.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "rules": { 4 | "no-console": true 5 | }, 6 | "extends": "./tslint.json" 7 | } 8 | -------------------------------------------------------------------------------- /win_run.bat: -------------------------------------------------------------------------------- 1 | echo OFF 2 | title win_run 3 | set FLASK_DEBUG=1 4 | start redis/redis-server.exe 5 | start cmd /k python RLBotServer.py 6 | start cmd /k celery -A backend.tasks.celery_worker.celery worker --pool=solo -l info 7 | start flower --port=5555 8 | cd webapp 9 | npm start 10 | EXIT 11 | --------------------------------------------------------------------------------