├── .gitignore ├── LICENSE ├── README.md ├── ballchasing ├── __init__.py ├── api.py ├── constants.py ├── stats_info.tsv └── util.py ├── release.bat ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.replay 2 | .idea 3 | dist/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rolv-Arild Braaten 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Ballchasing 2 | Python wrapper for the ballchasing.com API. 3 | 4 | # Installation 5 | ``` 6 | pip install python-ballchasing 7 | ``` 8 | 9 | # API 10 | The API is exposed via the `ballchasing.Api` class. 11 | 12 | Simple example: 13 | ```python 14 | import ballchasing 15 | api = ballchasing.Api("Your token here") 16 | 17 | # Get a specific replay 18 | replay = api.get_replay("2627e02a-aa46-4e13-b66b-b76a32069a07") 19 | ``` 20 | -------------------------------------------------------------------------------- /ballchasing/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # MIT License 3 | # 4 | # Copyright (c) 2020 Rolv-Arild Braaten 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, 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, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """A library that provides a Python interface to the Ballchasing API.""" 25 | from __future__ import absolute_import 26 | 27 | __author__ = 'Rolv-Arild Braaten' 28 | __email__ = 'rolv_arild@hotmail.com' 29 | __copyright__ = 'Copyright (c) 2020 Rolv-Arild Braaten' 30 | __license__ = 'Apache License 2.0' 31 | __version__ = '0.1.22' 32 | __url__ = 'https://github.com/Rolv-Arild/python-ballchasing' 33 | __download_url__ = 'https://pypi.python.org/pypi/python-ballchasing' 34 | __description__ = 'A Python wrapper around the Ballchasing API' 35 | 36 | from .api import Api # noqa 37 | from .constants import ( # noqa 38 | Playlist, 39 | Rank, 40 | Season, 41 | MatchResult, 42 | ReplaySortBy, 43 | GroupSortBy, 44 | SortDir, 45 | Visibility, 46 | PlayerIdentification, 47 | TeamIdentification, 48 | Map 49 | ) 50 | -------------------------------------------------------------------------------- /ballchasing/api.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from datetime import datetime 4 | from typing import Optional, Iterator, Union, List 5 | from urllib.parse import parse_qs, urlparse 6 | 7 | from requests import sessions, Response, ConnectionError 8 | 9 | from ballchasing.constants import GroupSortBy, SortDir, AnyPlaylist, AnyMap, AnySeason, AnyRank, AnyReplaySortBy, \ 10 | AnySortDir, AnyVisibility, AnyGroupSortBy, AnyPlayerIdentification, AnyTeamIdentification, AnyMatchResult 11 | from .util import rfc3339, replay_cols, team_cols, player_cols, parse_replay 12 | 13 | DEFAULT_URL = "https://ballchasing.com/api" 14 | 15 | 16 | class Api: 17 | """ 18 | Class for communication with ballchasing.com API (https://ballchasing.com/doc/api) 19 | """ 20 | 21 | def __init__(self, auth_key: str, sleep_time_on_rate_limit: Optional[float] = None, 22 | print_on_rate_limit: bool = False, base_url=None, do_initial_ping=True): 23 | """ 24 | 25 | :param auth_key: authentication key for API calls. 26 | :param sleep_time_on_rate_limit: seconds to wait after being rate limited. 27 | Default value is calculated depending on patron type. 28 | :param print_on_rate_limit: whether or not to print upon rate limits. 29 | """ 30 | self.auth_key = auth_key 31 | self._session = sessions.Session() 32 | self.steam_name = None 33 | self.steam_id = None 34 | self.patron_type = None 35 | self.rate_limit_count = 0 36 | self.base_url = DEFAULT_URL if base_url is None else base_url 37 | if do_initial_ping: 38 | self.ping() 39 | if sleep_time_on_rate_limit is None: 40 | self.sleep_time_on_rate_limit = { 41 | "regular": 3600 / 1000, 42 | "gold": 3600 / 2000, 43 | "diamond": 3600 / 5000, 44 | "champion": 1 / 8, 45 | "gc": 1 / 16 46 | }.get(self.patron_type or "regular") 47 | else: 48 | self.sleep_time_on_rate_limit = sleep_time_on_rate_limit 49 | self.print_on_rate_limit = print_on_rate_limit 50 | 51 | def _request(self, url_or_endpoint: str, method: callable, **params) -> Response: 52 | """ 53 | Helper method for all requests. 54 | 55 | :param url: url or endpoint for request. 56 | :param method: the method to use. 57 | :param params: parameters for GET request. 58 | :return: the request result. 59 | """ 60 | headers = {"Authorization": self.auth_key} 61 | url = f"{self.base_url}{url_or_endpoint}" if url_or_endpoint.startswith("/") else url_or_endpoint 62 | retries = 0 63 | while True: 64 | try: 65 | r = method(url, headers=headers, **params) 66 | retries = 0 67 | except ConnectionError as e: 68 | print("Connection error, trying again in 10 seconds...") 69 | time.sleep(10) 70 | retries += 1 71 | if retries >= 10: 72 | raise e 73 | continue 74 | if 200 <= r.status_code < 300: 75 | return r 76 | elif r.status_code == 429: 77 | self.rate_limit_count += 1 78 | if self.print_on_rate_limit: 79 | print(f"Rate limited at {url} ({self.rate_limit_count} total rate limits)") 80 | if self.sleep_time_on_rate_limit: 81 | time.sleep(self.sleep_time_on_rate_limit) 82 | else: 83 | raise r.raise_for_status() 84 | 85 | def ping(self): 86 | """ 87 | Use this API to: 88 | 89 | - check if your API key is correct 90 | - check if ballchasing API is reachable 91 | 92 | This method runs automatically at initialization and the steam name and id as well as patron type are stored. 93 | :return: ping response. 94 | """ 95 | result = self._request("/", self._session.get).json() 96 | self.steam_name = result["name"] 97 | self.steam_id = result["steam_id"] 98 | self.patron_type = result["type"] 99 | return result 100 | 101 | def get_replays(self, title: Optional[str] = None, 102 | player_name: Optional[Union[str, List[str]]] = None, 103 | player_id: Optional[Union[str, List[str]]] = None, 104 | playlist: Optional[Union[AnyPlaylist, List[AnyPlaylist]]] = None, 105 | season: Optional[Union[AnySeason, List[AnySeason]]] = None, 106 | match_result: Optional[Union[AnyMatchResult, List[AnyMatchResult]]] = None, 107 | min_rank: Optional[AnyRank] = None, 108 | max_rank: Optional[AnyRank] = None, 109 | pro: Optional[bool] = None, 110 | uploader: Optional[str] = None, 111 | group_id: Optional[Union[str, List[str]]] = None, 112 | map_id: Optional[Union[AnyMap, List[AnyMap]]] = None, 113 | created_before: Optional[Union[str, datetime]] = None, 114 | created_after: Optional[Union[str, datetime]] = None, 115 | replay_after: Optional[Union[str, datetime]] = None, 116 | replay_before: Optional[Union[str, datetime]] = None, 117 | count: int = 150, 118 | sort_by: Optional[AnyReplaySortBy] = None, 119 | sort_dir: AnySortDir = SortDir.DESCENDING, 120 | deep: bool = False 121 | ) -> Iterator[dict]: 122 | """ 123 | This endpoint lets you filter and retrieve replays. The implementation returns an iterator. 124 | 125 | :param title: filter replays by title. 126 | :param player_name: filter replays by a player’s name. 127 | :param player_id: filter replays by a player’s platform id in the $platform:$id, e.g. steam:76561198141161044, 128 | ps4:gamertag, … You can filter replays by multiple player ids, e.g ?player-id=steam:1&player-id=steam:2 129 | :param playlist: filter replays by one or more playlists. 130 | :param season: filter replays by season. Must be a number between 1 and 14 (for old seasons) 131 | or f1, f2, … for the new free to play seasons 132 | :param match_result: filter your replays by result. 133 | :param min_rank: filter your replays based on players minimum rank. 134 | :param max_rank: filter your replays based on players maximum rank. 135 | :param pro: only include replays containing at least one pro player. 136 | :param uploader: only include replays uploaded by the specified user. Accepts either the 137 | numerical 76*************44 steam id, or the special value 'me' 138 | :param group_id: only include replays belonging to the specified group. This only include replays immediately 139 | under the specified group, but not replays in child groups 140 | :param map_id: only include replays in the specified map. Check get_maps for the list of valid map codes 141 | :param created_before: only include replays created (uploaded) before some date. 142 | RFC3339 format, e.g. '2020-01-02T15:00:05+01:00' 143 | :param created_after: only include replays created (uploaded) after some date. 144 | RFC3339 format, e.g. '2020-01-02T15:00:05+01:00' 145 | :param replay_after: only include replays for games that happened after some date. 146 | RFC3339 format, e.g. '2020-01-02T15:00:05+01:00' 147 | :param replay_before: only include replays for games that happened before some date. 148 | RFC3339 format, e.g. '2020-01-02T15:00:05+01:00' 149 | :param count: returns at most count replays. Since the implementation uses an iterator it supports iterating 150 | past the limit of 200 set by the API 151 | :param sort_by: sort replays according the selected field 152 | :param sort_dir: sort direction 153 | :param deep: whether to get full stats for each replay (will be much slower). 154 | :return: an iterator over the replays returned by the API. 155 | """ 156 | url = f"{self.base_url}/replays" 157 | params = {"title": title, "player-name": player_name, "player-id": player_id, "playlist": playlist, 158 | "season": season, "match-result": match_result, "min-rank": min_rank, "max-rank": max_rank, 159 | "pro": pro, "uploader": uploader, "group": group_id, "map": map_id, 160 | "created-before": rfc3339(created_before), "created-after": rfc3339(created_after), 161 | "replay-date-after": rfc3339(replay_after), "replay-date-before": rfc3339(replay_before), 162 | "sort-by": sort_by, "sort-dir": sort_dir} 163 | left = count 164 | while left > 0: 165 | request_count = min(left, 200) 166 | params["count"] = request_count 167 | d = self._request(url, self._session.get, params=params).json() 168 | 169 | batch = d["list"][:request_count] 170 | if not deep: 171 | yield from batch 172 | else: 173 | yield from (self.get_replay(r["id"]) for r in batch) 174 | 175 | if "next" not in d: 176 | break 177 | 178 | next_url = d["next"] 179 | left -= len(batch) 180 | params["after"] = parse_qs(urlparse(next_url).query)["after"][0] 181 | 182 | def get_replay(self, replay_id: str) -> dict: 183 | """ 184 | Retrieve a given replay’s details and stats. 185 | 186 | :param replay_id: the replay id. 187 | :return: the result of the GET request. 188 | """ 189 | return self._request(f"/replays/{replay_id}", self._session.get).json() 190 | 191 | def patch_replay(self, replay_id: str, **params) -> None: 192 | """ 193 | This endpoint can patch one or more fields of the specified replay 194 | 195 | :param replay_id: the replay id. 196 | :param params: parameters for the PATCH request. 197 | """ 198 | self._request(f"/replays/{replay_id}", self._session.patch, json=params) 199 | 200 | def upload_replay(self, replay_file, visibility: Optional[AnyVisibility] = None, group: Optional[str] = None) -> dict: 201 | """ 202 | Use this API to upload a replay file to ballchasing.com. 203 | 204 | :param replay_file: replay file to upload. 205 | :param visibility: to set the visibility of the uploaded replay. 206 | :param group: to upload the replay to an existing group. 207 | :return: the result of the POST request. 208 | """ 209 | return self._request(f"/v2/upload", self._session.post, files={"file": replay_file}, 210 | params={"group": group, "visibility": visibility}).json() 211 | 212 | def delete_replay(self, replay_id: str) -> None: 213 | """ 214 | This endpoint deletes the specified replay. 215 | WARNING: This operation is permanent and undoable. 216 | 217 | :param replay_id: the replay id. 218 | """ 219 | self._request(f"/replays/{replay_id}", self._session.delete) 220 | 221 | def get_groups(self, name: Optional[str] = None, 222 | creator: Optional[str] = None, 223 | group: Optional[str] = None, 224 | created_before: Optional[Union[str, datetime]] = None, 225 | created_after: Optional[Union[str, datetime]] = None, 226 | count: int = 200, 227 | sort_by: AnyGroupSortBy = GroupSortBy.CREATED, 228 | sort_dir: AnySortDir = SortDir.DESCENDING 229 | ) -> Iterator[dict]: 230 | """ 231 | This endpoint lets you filter and retrieve replay groups. 232 | 233 | :param name: filter groups by name 234 | :param creator: only include groups created by the specified user. 235 | Accepts either the numerical 76*************44 steam id, or the special value me 236 | :param group: only include children of the specified group 237 | :param created_before: only include groups created (uploaded) before some date. 238 | RFC3339 format, e.g. 2020-01-02T15:00:05+01:00 239 | :param created_after: only include groups created (uploaded) after some date. 240 | RFC3339 format, e.g. 2020-01-02T15:00:05+01:00 241 | :param count: returns at most count groups. Since the implementation uses an iterator it supports iterating 242 | past the limit of 200 set by the API 243 | :param sort_by: Sort groups according the selected field. 244 | :param sort_dir: Sort direction. 245 | :return: an iterator over the groups returned by the API. 246 | """ 247 | url = f"{self.base_url}/groups/" 248 | params = {"name": name, "creator": creator, "group": group, "created-before": rfc3339(created_before), 249 | "created-after": rfc3339(created_after), "sort-by": sort_by, "sort-dir": sort_dir} 250 | 251 | left = count 252 | while left > 0: 253 | request_count = min(left, 200) 254 | params["count"] = request_count 255 | d = self._request(url, self._session.get, params=params).json() 256 | 257 | batch = d["list"][:request_count] 258 | yield from batch 259 | 260 | if "next" not in d: 261 | break 262 | 263 | next_url = d["next"] 264 | left -= len(batch) 265 | params["after"] = parse_qs(urlparse(next_url).query)["after"][0] 266 | 267 | def create_group(self, 268 | name: str, 269 | player_identification: AnyPlayerIdentification, 270 | team_identification: AnyTeamIdentification, 271 | parent: Optional[str] = None 272 | ) -> dict: 273 | """ 274 | Use this API to create a new replay group. 275 | 276 | :param name: the new group name. 277 | :param player_identification: how to identify the same player across multiple replays. 278 | Some tournaments (e.g. RLCS) make players use a pool of generic Steam accounts, 279 | meaning the same player could end up using 2 different accounts in 2 series. 280 | That's when the `by-name` comes in handy 281 | :param team_identification: How to identify the same team across multiple replays. 282 | Set to `by-distinct-players` if teams have a fixed roster of players for 283 | every single game. In some tournaments/leagues, teams allow player rotations, 284 | or a sub can replace another player, in which case use `by-player-clusters`. 285 | :param parent: if set,the new group will be created as a child of the specified group 286 | :return: the result of the POST request. 287 | """ 288 | json = {"name": name, "player_identification": player_identification, 289 | "team_identification": team_identification, "parent": parent} 290 | return self._request(f"/groups", self._session.post, json=json).json() 291 | 292 | def get_group(self, group_id: str) -> dict: 293 | """ 294 | This endpoint retrieves a specific replay group info and stats given its id. 295 | 296 | :param group_id: the group id. 297 | :return: the group info with stats. 298 | """ 299 | return self._request(f"/groups/{group_id}", self._session.get).json() 300 | 301 | def patch_group(self, group_id: str, **params) -> None: 302 | """ 303 | This endpoint can patch one or more fields of the specified group. 304 | 305 | :param group_id: the group id 306 | :param params: parameters for the PATCH request. 307 | """ 308 | self._request(f"/groups/{group_id}", self._session.patch, json=params) 309 | 310 | def delete_group(self, group_id: str) -> None: 311 | """ 312 | This endpoint deletes the specified group. 313 | WARNING: This operation is permanent and undoable. 314 | 315 | :param group_id: the group id. 316 | """ 317 | self._request(f"/groups/{group_id}", self._session.delete) 318 | 319 | def get_group_replays(self, group_id: str, deep: bool = False) -> Iterator[dict]: 320 | """ 321 | Finds all replays in a group, including child groups. 322 | 323 | :param group_id: the base group id. 324 | :param deep: whether or not to get full stats for each replay (will be much slower). 325 | :return: an iterator over all the replays in the group. 326 | """ 327 | child_groups = self.get_groups(group=group_id) 328 | for child in child_groups: 329 | for replay in self.get_group_replays(child["id"], deep): 330 | yield replay 331 | for replay in self.get_replays(group_id=group_id, deep=deep): 332 | yield replay 333 | 334 | def download_replay(self, replay_id: str, folder: str): 335 | """ 336 | Download a replay file. 337 | 338 | :param replay_id: the replay id. 339 | :param folder: the folder to download into. 340 | """ 341 | r = self._request(f"/replays/{replay_id}/file", self._session.get) 342 | with open(f"{folder}/{replay_id}.replay", "wb") as f: 343 | for ch in r: 344 | f.write(ch) 345 | 346 | def download_group(self, group_id: str, folder: str, recursive=True): 347 | """ 348 | Download an entire group. 349 | 350 | :param group_id: the base group id. 351 | :param folder: the folder in which to create the group folder. 352 | :param recursive: whether or not to create new folders for child groups. 353 | """ 354 | folder = os.path.join(folder, group_id) 355 | if recursive: 356 | os.makedirs(folder, exist_ok=True) 357 | for child_group in self.get_groups(group=group_id): 358 | self.download_group(child_group["id"], folder, True) 359 | for replay in self.get_replays(group_id=group_id): 360 | self.download_replay(replay["id"], folder) 361 | else: 362 | for replay in self.get_group_replays(group_id): 363 | self.download_replay(replay["id"], folder) 364 | 365 | def get_maps(self): 366 | """ 367 | Use this API to get the list of map codes to map names (map as in stadium). 368 | """ 369 | res = self._request("/maps", self._session.get).json() 370 | return res 371 | 372 | def generate_tsvs(self, replays: Iterator[Union[dict, str]], 373 | path_name: str, 374 | player_suffix: Optional[str] = "-players.tsv", 375 | team_suffix: Optional[str] = "-teams.tsv", 376 | replay_suffix: Optional[str] = "-replays.tsv", 377 | sep="\t", 378 | ): 379 | """ 380 | Generates tsv files for players, teams and replay info. 381 | 382 | :param replays: an iterator over either replays (with stats) or replay ids. 383 | :param path_name: the path to save the files at, including the name prefix. 384 | :param player_suffix: suffix for the player file. Set to None to disable player file writing. 385 | :param team_suffix: suffix for the team file. Set to None to disable team file writing. 386 | :param replay_suffix: suffix for the replay file. Set to None to disable replay file writing. 387 | :param sep: the separator to use. Default is tab character (tsv). 388 | """ 389 | player_file = None 390 | if player_suffix is not None: 391 | player_file = open(path_name + player_suffix, "w") 392 | player_file.write(sep.join(player_cols) + "\n") 393 | 394 | team_file = None 395 | if team_suffix is not None: 396 | team_file = open(path_name + team_suffix, "w") 397 | team_file.write(sep.join(team_cols) + "\n") 398 | 399 | replay_file = None 400 | if replay_suffix is not None: 401 | replay_file = open(path_name + replay_suffix, "w") 402 | replay_file.write(sep.join(replay_cols) + "\n") 403 | 404 | for replay in replays: 405 | if isinstance(replay, str): 406 | replay = self.get_replay(replay) 407 | for kind, values in parse_replay(replay): 408 | values = [str(v) for v in values] 409 | if kind == "replay" and replay_file is not None: 410 | replay_file.write(sep.join(values) + "\n") 411 | elif kind == "team" and team_file is not None: 412 | team_file.write(sep.join(values) + "\n") 413 | elif kind == "player" and player_file is not None: 414 | player_file.write(sep.join(values) + "\n") 415 | 416 | def __str__(self): 417 | return f"BallchasingApi[key={self.auth_key},name={self.steam_name}," \ 418 | f"steam_id={self.steam_id},type={self.patron_type}]" 419 | 420 | 421 | if __name__ == '__main__': 422 | # Basic initial tests 423 | import sys 424 | 425 | token = sys.argv[1] 426 | api = Api(token) 427 | print(api) 428 | # api.get_replays(season="123") 429 | # api.delete_replay("a22a8c81-fadd-4453-914e-ae54c2b8391f") 430 | upload_response = api.upload_replay(open("4E2B22344F748C6EB4922DB8CC8AC282.replay", "rb")) 431 | replays_response = api.get_replays() 432 | replay_response = api.get_replay(next(replays_response)["id"]) 433 | 434 | groups_response = api.get_groups() 435 | group_response = api.get_group(next(groups_response)["id"]) 436 | 437 | create_group_response = api.create_group(f"test-{time.time()}", "by-id", "by-distinct-players") 438 | api.patch_group(create_group_response["id"], team_identification="by-player-clusters") 439 | 440 | api.patch_replay(upload_response["id"], group=create_group_response["id"]) 441 | 442 | api.delete_group(create_group_response["id"]) 443 | api.delete_replay(upload_response["id"]) 444 | print("Nice") 445 | -------------------------------------------------------------------------------- /ballchasing/constants.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, get_args, AnyStr, Union 2 | 3 | AnyPlaylist = Union[ 4 | AnyStr, Literal["unranked-duels", "unranked-doubles", "unranked-standard", "unranked-chaos", "private", "season", 5 | "offline", "local-lobby", "ranked-duels", "ranked-doubles", "ranked-solo-standard", 6 | "ranked-standard", "snowday", "rocketlabs", "hoops", "rumble", "tournament", "dropshot", 7 | "ranked-hoops", "ranked-rumble", "ranked-dropshot", "ranked-snowday", "dropshot-rumble", 8 | "heatseeker", "gridiron"]] 9 | 10 | 11 | def _get_literals(type_): 12 | return get_args(get_args(type_)[1]) 13 | 14 | 15 | class Playlist: 16 | ALL = (UNRANKED_DUELS, UNRANKED_DOUBLES, UNRANKED_STANDARD, UNRANKED_CHAOS, PRIVATE, SEASON, OFFLINE, LOCAL_LOBBY, 17 | RANKED_DUELS, RANKED_DOUBLES, RANKED_SOLO_STANDARD, RANKED_STANDARD, SNOWDAY, ROCKETLABS, HOOPS, RUMBLE, 18 | TOURNAMENT, DROPSHOT, RANKED_HOOPS, RANKED_RUMBLE, RANKED_DROPSHOT, RANKED_SNOWDAY, DROPSHOT_RUMBLE, 19 | HEATSEEKER, GRIDIRON) = _get_literals(AnyPlaylist) 20 | LEGACY = (SNOWDAY, HOOPS, RUMBLE, DROPSHOT, RANKED_SOLO_STANDARD, ROCKETLABS) 21 | UNRANKED = (UNRANKED_DUELS, UNRANKED_DOUBLES, UNRANKED_STANDARD, UNRANKED_CHAOS) 22 | RANKED = (RANKED_DUELS, RANKED_DOUBLES, RANKED_STANDARD) 23 | LIMITED = (GRIDIRON, HEATSEEKER, DROPSHOT_RUMBLE) 24 | EXTRA_MODES = (RANKED_HOOPS, RANKED_RUMBLE, RANKED_DROPSHOT, RANKED_SNOWDAY) 25 | MISC = (PRIVATE, SEASON, OFFLINE, LOCAL_LOBBY, TOURNAMENT) 26 | 27 | 28 | AnyRank = Union[ 29 | AnyStr, Literal["unranked", "bronze-1", "bronze-2", "bronze-3", "silver-1", "silver-2", "silver-3", "gold-1", 30 | "gold-2", "gold-3", "platinum-1", "platinum-2", "platinum-3", "diamond-1", "diamond-2", 31 | "diamond-3", "champion-1", "champion-2", "champion-3", "grand-champion-1", "grand-champion-2", 32 | "grand-champion-3", "supersonic-legend"]] 33 | 34 | 35 | class Rank: 36 | ALL = (UNRANKED, BRONZE_1, BRONZE_2, BRONZE_3, SILVER_1, SILVER_2, SILVER_3, GOLD_1, GOLD_2, GOLD_3, PLATINUM_1, 37 | PLATINUM_2, PLATINUM_3, DIAMOND_1, DIAMOND_2, DIAMOND_3, CHAMPION_1, CHAMPION_2, CHAMPION_3, 38 | GRAND_CHAMPION_1, GRAND_CHAMPION_2, GRAND_CHAMPION_3, SUPERSONIC_LEGEND) = _get_literals(AnyRank) 39 | BRONZE = (BRONZE_1, BRONZE_2, BRONZE_3) 40 | SILVER = (SILVER_1, SILVER_2, SILVER_3) 41 | GOLD = (GOLD_1, GOLD_2, GOLD_3) 42 | PLATINUM = (PLATINUM_1, PLATINUM_2, PLATINUM_3) 43 | DIAMOND = (DIAMOND_1, DIAMOND_2, DIAMOND_3) 44 | CHAMPION = (CHAMPION_1, CHAMPION_2, CHAMPION_3) 45 | GRAND_CHAMPION = (GRAND_CHAMPION_1, GRAND_CHAMPION_2, GRAND_CHAMPION_3) 46 | 47 | 48 | AnySeason = Union[AnyStr, Literal[ 49 | "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", 50 | "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "f13" 51 | ]] 52 | 53 | 54 | class Season: 55 | ALL = (SEASON_1, SEASON_2, SEASON_3, SEASON_4, SEASON_5, SEASON_6, SEASON_7, SEASON_8, SEASON_9, SEASON_10, 56 | SEASON_11, SEASON_12, SEASON_13, SEASON_14, SEASON_1_FTP, SEASON_2_FTP, SEASON_3_FTP, SEASON_4_FTP, 57 | SEASON_5_FTP, SEASON_6_FTP, SEASON_7_FTP, SEASON_8_FTP, SEASON_9_FTP, SEASON_10_FTP, SEASON_11_FTP, 58 | SEASON_12_FTP, SEASON_13_FTP) = _get_literals(AnySeason) 59 | PAY_TO_PLAY = (SEASON_1, SEASON_2, SEASON_3, SEASON_4, SEASON_5, SEASON_6, SEASON_7, SEASON_8, SEASON_9, SEASON_10, 60 | SEASON_11, SEASON_12, SEASON_13, SEASON_14) 61 | FREE_TO_PLAY = (SEASON_1_FTP, SEASON_2_FTP, SEASON_3_FTP, SEASON_4_FTP, SEASON_5_FTP, SEASON_6_FTP, SEASON_7_FTP, 62 | SEASON_8_FTP, SEASON_9_FTP, SEASON_10_FTP, SEASON_11_FTP, SEASON_12_FTP, SEASON_13_FTP) 63 | 64 | 65 | AnyMatchResult = Union[AnyStr, Literal["win", "loss"]] 66 | 67 | 68 | class MatchResult: 69 | WIN, LOSS = _get_literals(AnyMatchResult) 70 | 71 | 72 | AnyReplaySortBy = Union[AnyStr, Literal["replay-date", "upload-date"]] 73 | 74 | 75 | class ReplaySortBy: 76 | REPLAY_DATE, UPLOAD_DATE = _get_literals(AnyReplaySortBy) 77 | 78 | 79 | AnyGroupSortBy = Union[AnyStr, Literal["created", "name"]] 80 | 81 | 82 | class GroupSortBy: 83 | CREATED, NAME = _get_literals(AnyGroupSortBy) 84 | 85 | 86 | AnySortDir = Union[AnyStr, Literal["asc", "desc"]] 87 | 88 | 89 | class SortDir: 90 | ASCENDING, DESCENDING = ASC, DESC = _get_literals(AnySortDir) 91 | 92 | 93 | AnyVisibility = Union[AnyStr, Literal["public", "unlisted", "private"]] 94 | 95 | 96 | class Visibility: 97 | PUBLIC, UNLISTED, PRIVATE = _get_literals(AnyVisibility) 98 | 99 | 100 | AnyPlayerIdentification = Union[AnyStr, Literal["by-id", "by-name"]] 101 | 102 | 103 | class PlayerIdentification: 104 | BY_ID, BY_NAME = _get_literals(AnyPlayerIdentification) 105 | 106 | 107 | AnyTeamIdentification = Union[AnyStr, Literal["by-distinct-players", "by-player-clusters"]] 108 | 109 | 110 | class TeamIdentification: 111 | BY_DISTINCT_PLAYERS, BY_PLAYER_CLUSTERS = _get_literals(AnyTeamIdentification) 112 | 113 | 114 | AnyMap = Union[AnyStr, Literal[ 115 | "arc_p", "arc_darc_p", "arc_standard_p", "bb_p", "beach_night_p", "beach_p", "beachvolley", "chn_stadium_day_p", 116 | "chn_stadium_p", "cs_day_p", "cs_hw_p", "cs_p", "eurostadium_night_p", "eurostadium_p", 117 | "eurostadium_rainy_p", "eurostadium_snownight_p", "farm_night_p", "farm_p", "farm_upsidedown_p", 118 | "haunted_trainstation_p", "hoopsstadium_p", "labs_circlepillars_p", "labs_cosmic_p", 119 | "labs_cosmic_v4_p", "labs_doublegoal_p", "labs_doublegoal_v2_p", "labs_octagon_02_p", 120 | "labs_octagon_p", "labs_underpass_p", "labs_underpass_v0_p", "labs_utopia_p", "music_p", 121 | "neotokyo_p", "neotokyo_standard_p", "park_night_p", "park_p", "park_rainy_p", "shattershot_p", 122 | "stadium_day_p", "stadium_foggy_p", "stadium_p", "stadium_race_day_p", "stadium_winter_p", 123 | "throwbackstadium_p", "trainstation_dawn_p", "trainstation_night_p", "trainstation_p", "underwater_p", 124 | "utopiastadium_dusk_p", "utopiastadium_p", "utopiastadium_snow_p", "wasteland_night_p", 125 | "wasteland_night_s_p", "wasteland_p", "wasteland_s_p"]] 126 | 127 | 128 | class Map: 129 | ALL = ( 130 | ARC_P, ARC_DARC_P, ARC_STANDARD_P, BB_P, BEACH_NIGHT_P, BEACH_P, BEACHVOLLEY, CHN_STADIUM_DAY_P, CHN_STADIUM_P, 131 | CS_DAY_P, 132 | CS_HW_P, CS_P, EUROSTADIUM_NIGHT_P, EUROSTADIUM_P, EUROSTADIUM_RAINY_P, EUROSTADIUM_SNOWNIGHT_P, 133 | FARM_NIGHT_P, FARM_P, FARM_UPSIDEDOWN_P, HAUNTED_TRAINSTATION_P, HOOPSSTADIUM_P, LABS_CIRCLEPILLARS_P, 134 | LABS_COSMIC_P, LABS_COSMIC_V4_P, LABS_DOUBLEGOAL_P, LABS_DOUBLEGOAL_V2_P, LABS_OCTAGON_02_P, LABS_OCTAGON_P, 135 | LABS_UNDERPASS_P, LABS_UNDERPASS_V0_P, LABS_UTOPIA_P, MUSIC_P, NEOTOKYO_P, NEOTOKYO_STANDARD_P, PARK_NIGHT_P, 136 | PARK_P, PARK_RAINY_P, SHATTERSHOT_P, STADIUM_DAY_P, STADIUM_FOGGY_P, STADIUM_P, STADIUM_RACE_DAY_P, 137 | STADIUM_WINTER_P, THROWBACKSTADIUM_P, TRAINSTATION_DAWN_P, TRAINSTATION_NIGHT_P, TRAINSTATION_P, 138 | UNDERWATER_P, UTOPIASTADIUM_DUSK_P, UTOPIASTADIUM_P, UTOPIASTADIUM_SNOW_P, WASTELAND_NIGHT_P, 139 | WASTELAND_NIGHT_S_P, WASTELAND_P, WASTELAND_S_P) = _get_literals(AnyMap) 140 | NAMES = dict(zip(ALL, ( 141 | "Starbase ARC", "Starbase ARC (Aftermath)", "Starbase ARC (Standard)", "Champions Field (NFL)", 142 | "Salty Shores (Night)", 143 | "Salty Shores", "Salty Shores (Volley)", "Forbidden Temple (Day)", "Forbidden Temple", 144 | "Champions Field (Day)", "Rivals Arena", "Champions Field", "Mannfield (Night)", "Mannfield", 145 | "Mannfield (Stormy)", "Mannfield (Snowy)", "Farmstead (Night)", "Farmstead", 146 | "Farmstead (The Upside Down)", "Urban Central (Haunted)", "Dunk House", "Pillars", "Cosmic", 147 | "Cosmic", "Double Goal", "Double Goal", "Octagon", "Octagon", "Underpass", "Underpass", 148 | "Utopia Retro", "Neon Fields", "Neo Tokyo", "Neo Tokyo (Standard)", 149 | "Beckwith Park (Midnight)", "Beckwith Park", "Beckwith Park (Stormy)", "Core 707", 150 | "DFH Stadium (Day)", "DFH Stadium (Stormy)", "DFH Stadium", "DFH Stadium (Circuit)", 151 | "DFH Stadium (Snowy)", "Throwback Stadium", "Urban Central (Dawn)", "Urban Central (Night)", 152 | "Urban Central", "Aquadome", "Utopia Coliseum (Dusk)", "Utopia Coliseum", 153 | "Utopia Coliseum (Snowy)", "Wasteland (Night)", "Wasteland (Standard, Night)", "Wasteland", 154 | "Wasteland (Standard)"))) 155 | STANDARD_MAPS = ( 156 | ARC_DARC_P, ARC_STANDARD_P, BEACH_NIGHT_P, CHN_STADIUM_P, CS_DAY_P, CS_P, EUROSTADIUM_NIGHT_P, EUROSTADIUM_P, 157 | EUROSTADIUM_RAINY_P, EUROSTADIUM_SNOWNIGHT_P, FARM_P, MUSIC_P, NEOTOKYO_STANDARD_P, PARK_NIGHT_P, 158 | PARK_P, PARK_RAINY_P, STADIUM_DAY_P, STADIUM_FOGGY_P, STADIUM_P, TRAINSTATION_DAWN_P, 159 | TRAINSTATION_NIGHT_P, TRAINSTATION_P, UNDERWATER_P, UTOPIASTADIUM_DUSK_P, UTOPIASTADIUM_P, 160 | WASTELAND_NIGHT_S_P, WASTELAND_S_P) 161 | NON_STANDARD_MAPS = (ARC_P, BB_P, BEACH_P, BEACHVOLLEY, CHN_STADIUM_DAY_P, CS_HW_P, FARM_NIGHT_P, 162 | FARM_UPSIDEDOWN_P, HAUNTED_TRAINSTATION_P, HOOPSSTADIUM_P, LABS_CIRCLEPILLARS_P, LABS_COSMIC_P, 163 | LABS_COSMIC_V4_P, LABS_DOUBLEGOAL_P, LABS_DOUBLEGOAL_V2_P, LABS_OCTAGON_02_P, LABS_OCTAGON_P, 164 | LABS_UNDERPASS_P, LABS_UNDERPASS_V0_P, LABS_UTOPIA_P, NEOTOKYO_P, SHATTERSHOT_P, 165 | STADIUM_RACE_DAY_P, STADIUM_WINTER_P, THROWBACKSTADIUM_P, UTOPIASTADIUM_SNOW_P, 166 | WASTELAND_NIGHT_P, WASTELAND_P) 167 | -------------------------------------------------------------------------------- /ballchasing/stats_info.tsv: -------------------------------------------------------------------------------- 1 | name path is_replay is_team is_player type is_player_sum dtype 2 | replay_id id TRUE TRUE TRUE info FALSE str 3 | rocket_league_id rocket_league_id TRUE FALSE FALSE info FALSE str 4 | title title TRUE FALSE FALSE info FALSE str 5 | created created TRUE FALSE FALSE info FALSE datetime 6 | date date TRUE FALSE FALSE info FALSE datetime 7 | map_code map_code TRUE FALSE FALSE info FALSE str 8 | match_type match_type TRUE FALSE FALSE info FALSE str 9 | team_size team_size TRUE FALSE FALSE info FALSE int 10 | uploader uploader.steam_id TRUE FALSE FALSE info FALSE str 11 | status status TRUE FALSE FALSE info FALSE str 12 | duration duration TRUE TRUE FALSE info FALSE float 13 | overtime_duration overtime_seconds TRUE FALSE FALSE info FALSE float 14 | playlist_id playlist_id TRUE FALSE FALSE info FALSE str 15 | season season TRUE FALSE FALSE info FALSE str 16 | season_type season_type TRUE FALSE FALSE info FALSE str 17 | visibility visibility TRUE FALSE FALSE info FALSE str 18 | min_rank min_rank.name TRUE FALSE FALSE info FALSE str 19 | max_rank max_rank.name TRUE FALSE FALSE info FALSE str 20 | color {}.color FALSE TRUE TRUE info FALSE str 21 | team_name {}.name FALSE TRUE FALSE info FALSE str 22 | possession_time {}.stats.ball.possession_time FALSE TRUE FALSE time FALSE float 23 | time_in_side {}.stats.ball.time_in_side FALSE TRUE FALSE time FALSE float 24 | shots {}.stats.core.shots FALSE TRUE FALSE normalizable TRUE int 25 | shots_against {}.stats.core.shots_against FALSE TRUE FALSE normalizable TRUE int 26 | goals {}.stats.core.goals FALSE TRUE FALSE normalizable TRUE int 27 | goals_against {}.stats.core.goals_against FALSE TRUE FALSE normalizable TRUE int 28 | saves {}.stats.core.saves FALSE TRUE FALSE normalizable TRUE int 29 | assists {}.stats.core.assists FALSE TRUE FALSE normalizable TRUE int 30 | score {}.stats.core.score FALSE TRUE FALSE normalizable TRUE int 31 | shooting_percentage {}.stats.core.shooting_percentage FALSE TRUE FALSE average TRUE int 32 | bpm {}.stats.boost.bpm FALSE TRUE FALSE average TRUE float 33 | bcpm {}.stats.boost.bcpm FALSE TRUE FALSE average TRUE float 34 | avg_boost_amount {}.stats.boost.avg_amount FALSE TRUE FALSE average TRUE float 35 | amount_collected {}.stats.boost.amount_collected FALSE TRUE FALSE normalizable TRUE float 36 | amount_stolen {}.stats.boost.amount_stolen FALSE TRUE FALSE normalizable TRUE float 37 | amount_collected_big {}.stats.boost.amount_collected_big FALSE TRUE FALSE normalizable TRUE float 38 | amount_stolen_big {}.stats.boost.amount_stolen_big FALSE TRUE FALSE normalizable TRUE float 39 | amount_collected_small {}.stats.boost.amount_collected_small FALSE TRUE FALSE normalizable TRUE float 40 | amount_stolen_small {}.stats.boost.amount_stolen_small FALSE TRUE FALSE normalizable TRUE float 41 | count_collected_big {}.stats.boost.count_collected_big FALSE TRUE FALSE normalizable TRUE int 42 | count_stolen_big {}.stats.boost.count_stolen_big FALSE TRUE FALSE normalizable TRUE int 43 | count_collected_small {}.stats.boost.count_collected_small FALSE TRUE FALSE normalizable TRUE int 44 | count_stolen_small {}.stats.boost.count_stolen_small FALSE TRUE FALSE normalizable TRUE int 45 | amount_overfill {}.stats.boost.amount_overfill FALSE TRUE FALSE normalizable TRUE float 46 | amount_overfill_stolen {}.stats.boost.amount_overfill_stolen FALSE TRUE FALSE normalizable TRUE float 47 | amount_used_while_supersonic {}.stats.boost.amount_used_while_supersonic FALSE TRUE FALSE normalizable TRUE float 48 | time_zero_boost {}.stats.boost.time_zero_boost FALSE TRUE FALSE time TRUE float 49 | time_full_boost {}.stats.boost.time_full_boost FALSE TRUE FALSE time TRUE float 50 | time_boost_0_25 {}.stats.boost.time_boost_0_25 FALSE TRUE FALSE time TRUE float 51 | time_boost_25_50 {}.stats.boost.time_boost_25_50 FALSE TRUE FALSE time TRUE float 52 | time_boost_50_75 {}.stats.boost.time_boost_50_75 FALSE TRUE FALSE time TRUE float 53 | time_boost_75_100 {}.stats.boost.time_boost_75_100 FALSE TRUE FALSE time TRUE float 54 | time_supersonic_speed {}.stats.movement.time_supersonic_speed FALSE TRUE FALSE time TRUE float 55 | time_boost_speed {}.stats.movement.time_boost_speed FALSE TRUE FALSE time TRUE float 56 | time_slow_speed {}.stats.movement.time_slow_speed FALSE TRUE FALSE time TRUE float 57 | time_ground {}.stats.movement.time_ground FALSE TRUE FALSE time TRUE float 58 | time_low_air {}.stats.movement.time_low_air FALSE TRUE FALSE time TRUE float 59 | time_high_air {}.stats.movement.time_high_air FALSE TRUE FALSE time TRUE float 60 | time_powerslide {}.stats.movement.time_powerslide FALSE TRUE FALSE time TRUE float 61 | count_powerslide {}.stats.movement.count_powerslide FALSE TRUE FALSE normalizable TRUE int 62 | time_defensive_third {}.stats.positioning.time_defensive_third FALSE TRUE FALSE time TRUE float 63 | time_neutral_third {}.stats.positioning.time_neutral_third FALSE TRUE FALSE time TRUE float 64 | time_offensive_third {}.stats.positioning.time_offensive_third FALSE TRUE FALSE time TRUE float 65 | time_defensive_half {}.stats.positioning.time_defensive_half FALSE TRUE FALSE time TRUE float 66 | time_offensive_half {}.stats.positioning.time_offensive_half FALSE TRUE FALSE time TRUE float 67 | time_behind_ball {}.stats.positioning.time_behind_ball FALSE TRUE FALSE time TRUE float 68 | time_infront_ball {}.stats.positioning.time_infront_ball FALSE TRUE FALSE time TRUE float 69 | demos_inflicted {}.stats.demo.inflicted FALSE TRUE FALSE normalizable TRUE int 70 | demos_taken {}.stats.demo.taken FALSE TRUE FALSE normalizable TRUE int 71 | start_time {}.players.{}.start_time FALSE FALSE TRUE info FALSE float 72 | end_time {}.players.{}.end_time FALSE FALSE TRUE info FALSE float 73 | player_name {}.players.{}.name FALSE FALSE TRUE info FALSE str 74 | platform {}.players.{}.id.platform FALSE FALSE TRUE info FALSE str 75 | player_id {}.players.{}.id.id FALSE FALSE TRUE info FALSE str 76 | rank {}.players.{}.rank.tier FALSE FALSE TRUE info FALSE int 77 | division {}.players.{}.rank.division FALSE FALSE TRUE info FALSE int 78 | car_id {}.players.{}.car_id FALSE FALSE TRUE info FALSE int 79 | car_name {}.players.{}.car_name FALSE FALSE TRUE info FALSE str 80 | camera_fov {}.players.{}.camera.fov FALSE FALSE TRUE info FALSE float 81 | camera_height {}.players.{}.camera.height FALSE FALSE TRUE info FALSE float 82 | camera_pitch {}.players.{}.camera.pitch FALSE FALSE TRUE info FALSE float 83 | camera_distance {}.players.{}.camera.distance FALSE FALSE TRUE info FALSE float 84 | camera_stiffness {}.players.{}.camera.stiffness FALSE FALSE TRUE info FALSE float 85 | camera_swivel_speed {}.players.{}.camera.swivel_speed FALSE FALSE TRUE info FALSE float 86 | camera_transition_speed {}.players.{}.camera.transition_speed FALSE FALSE TRUE info FALSE float 87 | steering_sensitivity {}.players.{}.steering_sensitivity FALSE FALSE TRUE info FALSE float 88 | shots {}.players.{}.stats.core.shots FALSE FALSE TRUE normalizable FALSE int 89 | goals {}.players.{}.stats.core.goals FALSE FALSE TRUE normalizable FALSE int 90 | saves {}.players.{}.stats.core.saves FALSE FALSE TRUE normalizable FALSE int 91 | assists {}.players.{}.stats.core.assists FALSE FALSE TRUE normalizable FALSE int 92 | score {}.players.{}.stats.core.score FALSE FALSE TRUE normalizable FALSE int 93 | mvp {}.players.{}.stats.core.mvp FALSE FALSE TRUE info FALSE bool 94 | shooting_percentage {}.players.{}.stats.core.shooting_percentage FALSE FALSE TRUE average FALSE float 95 | bpm {}.players.{}.stats.boost.bpm FALSE FALSE TRUE average FALSE float 96 | bcpm {}.players.{}.stats.boost.bcpm FALSE FALSE TRUE average FALSE float 97 | avg_amount {}.players.{}.stats.boost.avg_amount FALSE FALSE TRUE average FALSE float 98 | amount_collected {}.players.{}.stats.boost.amount_collected FALSE FALSE TRUE normalizable FALSE float 99 | amount_stolen {}.players.{}.stats.boost.amount_stolen FALSE FALSE TRUE normalizable FALSE float 100 | amount_collected_big {}.players.{}.stats.boost.amount_collected_big FALSE FALSE TRUE normalizable FALSE float 101 | amount_stolen_big {}.players.{}.stats.boost.amount_stolen_big FALSE FALSE TRUE normalizable FALSE float 102 | amount_collected_small {}.players.{}.stats.boost.amount_collected_small FALSE FALSE TRUE normalizable FALSE float 103 | amount_stolen_small {}.players.{}.stats.boost.amount_stolen_small FALSE FALSE TRUE normalizable FALSE float 104 | count_collected_big {}.players.{}.stats.boost.count_collected_big FALSE FALSE TRUE normalizable FALSE int 105 | count_stolen_big {}.players.{}.stats.boost.count_stolen_big FALSE FALSE TRUE normalizable FALSE int 106 | count_collected_small {}.players.{}.stats.boost.count_collected_small FALSE FALSE TRUE normalizable FALSE int 107 | count_stolen_small {}.players.{}.stats.boost.count_stolen_small FALSE FALSE TRUE normalizable FALSE int 108 | amount_overfill {}.players.{}.stats.boost.amount_overfill FALSE FALSE TRUE normalizable FALSE float 109 | amount_overfill_stolen {}.players.{}.stats.boost.amount_overfill_stolen FALSE FALSE TRUE normalizable FALSE float 110 | amount_used_while_supersonic {}.players.{}.stats.boost.amount_used_while_supersonic FALSE FALSE TRUE normalizable FALSE float 111 | percent_zero_boost {}.players.{}.stats.boost.percent_zero_boost FALSE FALSE TRUE average FALSE float 112 | percent_full_boost {}.players.{}.stats.boost.percent_full_boost FALSE FALSE TRUE average FALSE float 113 | percent_boost_0_25 {}.players.{}.stats.boost.percent_boost_0_25 FALSE FALSE TRUE average FALSE float 114 | percent_boost_25_50 {}.players.{}.stats.boost.percent_boost_25_50 FALSE FALSE TRUE average FALSE float 115 | percent_boost_50_75 {}.players.{}.stats.boost.percent_boost_50_75 FALSE FALSE TRUE average FALSE float 116 | percent_boost_75_100 {}.players.{}.stats.boost.percent_boost_75_100 FALSE FALSE TRUE average FALSE float 117 | avg_speed {}.players.{}.stats.movement.avg_speed FALSE FALSE TRUE average FALSE float 118 | count_powerslide {}.players.{}.stats.movement.count_powerslide FALSE FALSE TRUE normalizable FALSE int 119 | avg_powerslide_duration {}.players.{}.stats.movement.avg_powerslide_duration FALSE FALSE TRUE average FALSE float 120 | percent_slow_speed {}.players.{}.stats.movement.percent_slow_speed FALSE FALSE TRUE average FALSE float 121 | percent_boost_speed {}.players.{}.stats.movement.percent_boost_speed FALSE FALSE TRUE average FALSE float 122 | percent_supersonic_speed {}.players.{}.stats.movement.percent_supersonic_speed FALSE FALSE TRUE average FALSE float 123 | percent_ground {}.players.{}.stats.movement.percent_ground FALSE FALSE TRUE average FALSE float 124 | percent_low_air {}.players.{}.stats.movement.percent_low_air FALSE FALSE TRUE average FALSE float 125 | percent_high_air {}.players.{}.stats.movement.percent_high_air FALSE FALSE TRUE average FALSE float 126 | avg_distance_to_ball {}.players.{}.stats.positioning.avg_distance_to_ball FALSE FALSE TRUE average FALSE float 127 | avg_distance_to_ball_possession {}.players.{}.stats.positioning.avg_distance_to_ball_possession FALSE FALSE TRUE average FALSE float 128 | avg_distance_to_ball_no_possession {}.players.{}.stats.positioning.avg_distance_to_ball_no_possession FALSE FALSE TRUE average FALSE float 129 | avg_distance_to_mates {}.players.{}.stats.positioning.avg_distance_to_mates FALSE FALSE TRUE average FALSE float 130 | goals_against_while_last_defender {}.players.{}.stats.positioning.goals_against_while_last_defender FALSE FALSE TRUE normalizable FALSE int 131 | percent_defensive_third {}.players.{}.stats.positioning.percent_defensive_third FALSE FALSE TRUE average FALSE float 132 | percent_offensive_third {}.players.{}.stats.positioning.percent_offensive_third FALSE FALSE TRUE average FALSE float 133 | percent_neutral_third {}.players.{}.stats.positioning.percent_neutral_third FALSE FALSE TRUE average FALSE float 134 | percent_defensive_half {}.players.{}.stats.positioning.percent_defensive_half FALSE FALSE TRUE average FALSE float 135 | percent_offensive_half {}.players.{}.stats.positioning.percent_offensive_half FALSE FALSE TRUE average FALSE float 136 | percent_behind_ball {}.players.{}.stats.positioning.percent_behind_ball FALSE FALSE TRUE average FALSE float 137 | percent_infront_ball {}.players.{}.stats.positioning.percent_infront_ball FALSE FALSE TRUE average FALSE float 138 | percent_most_back {}.players.{}.stats.positioning.percent_most_back FALSE FALSE TRUE average FALSE float 139 | percent_most_forward {}.players.{}.stats.positioning.percent_most_forward FALSE FALSE TRUE average FALSE float 140 | percent_closest_to_ball {}.players.{}.stats.positioning.percent_closest_to_ball FALSE FALSE TRUE average FALSE float 141 | percent_farthest_from_ball {}.players.{}.stats.positioning.percent_farthest_from_ball FALSE FALSE TRUE average FALSE float 142 | demos_inflicted {}.players.{}.stats.demo.inflicted FALSE FALSE TRUE normalizable FALSE int 143 | demos_taken {}.players.{}.stats.demo.taken FALSE FALSE TRUE normalizable FALSE int -------------------------------------------------------------------------------- /ballchasing/util.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from pathlib import Path 3 | 4 | stats_info = open(Path(__file__).parent / "stats_info.tsv") 5 | stats_info = [line.strip().split("\t") for line in stats_info] 6 | 7 | replay_cols = [row[0] for row in stats_info if row[2].lower() == "true"] 8 | team_cols = [row[0] for row in stats_info if row[3].lower() == "true"] 9 | player_cols = [row[0] for row in stats_info if row[4].lower() == "true"] 10 | 11 | 12 | def get_value(replay, path, dtype, *path_args): 13 | tree = replay 14 | for branch in path.format(*path_args).split("."): 15 | if isinstance(tree, dict): 16 | tree = tree.get(branch, None) 17 | elif isinstance(tree, list): 18 | tree = tree[int(branch)] 19 | 20 | if tree is None: 21 | return "MISSING" 22 | if isinstance(tree, (dict, list)): 23 | return "MISSING" 24 | if dtype == "str": 25 | return str(tree) 26 | elif dtype == "int": 27 | return int(tree) 28 | elif dtype == "float": 29 | return float(tree) 30 | elif dtype == "bool": 31 | return bool(tree) 32 | 33 | return tree 34 | 35 | 36 | def parse_replay(replay: dict): 37 | replay_stats = [] 38 | team_stats = [[], []] 39 | player_stats = [] 40 | 41 | for name, path, is_replay, is_team, is_player, type_, is_player_sum, dtype in stats_info: 42 | if is_replay.lower() == "true": 43 | replay_stats.append(get_value(replay, path, dtype)) 44 | 45 | if is_team.lower() == "true": 46 | for n, team in enumerate(("blue", "orange")): 47 | ts = team_stats[n] 48 | ts.append(get_value(replay, path, dtype, team)) 49 | 50 | if is_player.lower() == "true": 51 | n = 0 52 | for team in "blue", "orange": 53 | for p in range(len(replay[team]["players"])): 54 | if n >= len(player_stats): 55 | player_stats.append([]) 56 | ps = player_stats[n] 57 | ps.append(get_value(replay, path, dtype, team, p)) 58 | n += 1 59 | 60 | yield "replay", replay_stats 61 | yield from (("team", ts) for ts in team_stats) 62 | yield from (("player", ps) for ps in player_stats) 63 | 64 | 65 | def rfc3339(dt): 66 | if dt is None: 67 | return dt 68 | elif isinstance(dt, str): 69 | return dt 70 | elif isinstance(dt, datetime): 71 | return dt.isoformat("T") + "Z" 72 | else: 73 | raise ValueError("Date must be either string or datetime") 74 | -------------------------------------------------------------------------------- /release.bat: -------------------------------------------------------------------------------- 1 | rm -r dist 2 | rm -r rlgym.egg-info 3 | python setup.py sdist && twine upload dist/* -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | . -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | from ballchasing import __version__, __author__, __email__, __description__, __url__, __download_url__ 4 | 5 | with open("README.md", "r") as fh: 6 | long_description = fh.read() 7 | 8 | setuptools.setup( 9 | name='python-ballchasing', 10 | version=__version__, 11 | author=__author__, 12 | author_email=__email__, 13 | description=__description__, 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url=__url__, 17 | download_url=__download_url__, 18 | install_requires=["requests"], 19 | packages=setuptools.find_packages(), 20 | python_requires='>=3.8', 21 | package_data={'ballchasing': ['*.tsv']}, 22 | classifiers=[ 23 | "Programming Language :: Python :: 3.8", 24 | "License :: OSI Approved :: MIT License", 25 | "Operating System :: OS Independent", 26 | ], 27 | ) 28 | --------------------------------------------------------------------------------