├── requirements.txt ├── MANIFEST.in ├── rtd-requirements.txt ├── examples ├── simple_example.py ├── old_asyncio_example.py ├── player_batch_simple_example.py └── player_batch_full_example.py ├── .gitattributes ├── docs ├── index.rst ├── Makefile ├── make.bat ├── gettingstarted.rst ├── api.rst ├── how.rst └── conf.py ├── README.md ├── LICENSE ├── .gitignore ├── r6sapi ├── exceptions.py ├── definitions │ ├── __init__.py │ ├── models.py │ ├── stores.py │ ├── seasons.py │ └── loadouts.py ├── __init__.py ├── platforms.py ├── gamequeues.py ├── gamemodes.py ├── weapons.py ├── ranks.py ├── auth.py ├── operators.py └── players.py └── setup.py /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp>=3.6.0,<3.8.0 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include requirements.txt -------------------------------------------------------------------------------- /rtd-requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp>=3.6.0,<3.8.0 2 | sphinx>=2.0.0 3 | sphinx_rtd_theme>=0.4.0 4 | -------------------------------------------------------------------------------- /examples/simple_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import r6sapi as api 3 | 4 | async def run(): 5 | auth = api.Auth("email", "password") 6 | 7 | player = await auth.get_player("billy_yoyo", api.Platforms.UPLAY) 8 | operator = await player.get_operator("sledge") 9 | 10 | print(operator.kills) 11 | 12 | await auth.close() 13 | 14 | asyncio.get_event_loop().run_until_complete(run()) -------------------------------------------------------------------------------- /examples/old_asyncio_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import r6sapi as api 3 | 4 | @asyncio.coroutine 5 | def run(): 6 | auth = api.Auth("email", "password") 7 | 8 | player = yield from auth.get_player("billy_yoyo", api.Platforms.UPLAY) 9 | operator = yield from player.get_operator("sledge") 10 | print(operator.kills) 11 | 12 | yield from auth.close() 13 | 14 | asyncio.get_event_loop().run_until_complete(run()) -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. r6sapi.py documentation master file, created by 2 | sphinx-quickstart on Sat Dec 31 12:48:36 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to r6sapi.py's documentation! 7 | ===================================== 8 | 9 | Contents 10 | -------- 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | gettingstarted 16 | api 17 | how 18 | 19 | 20 | Indices and tables 21 | ------------------ 22 | 23 | * :ref:`genindex` 24 | * :ref:`search` 25 | -------------------------------------------------------------------------------- /examples/player_batch_simple_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import r6sapi as api 3 | 4 | async def run(): 5 | auth = api.Auth("email", "password") 6 | 7 | player_batch = await auth.get_player_batch(["billy_yoyo", "another_user"], api.Platforms.UPLAY) 8 | ranks = await player_batch.get_rank(api.RankedRegions.EU) 9 | 10 | for player in player_batch: 11 | rank = ranks[player.id] 12 | 13 | print("player %s has %s mmr" % (player.name, rank.mmr)) 14 | 15 | await auth.close() 16 | 17 | asyncio.get_event_loop().run_until_complete(run()) -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = r6sapipy 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=r6sapipy 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # r6sapi 2 | 3 | r6sapi is an easy-to-use asynchronous API for rainbow six siege, written in python. To use it you'll need to use your ubisoft email and password 4 | 5 | ### Installation 6 | 7 | To install this module, simply run 8 | 9 | pip install r6sapi 10 | 11 | ### Documentation 12 | 13 | http://rainbowsixsiege-python-api.readthedocs.io/en/latest/ 14 | 15 | ### Quick Example 16 | 17 | ```py 18 | import asyncio 19 | import r6sapi as api 20 | 21 | async def run(): 22 | auth = api.Auth("email", "password") 23 | 24 | player = await auth.get_player("billy_yoyo", api.Platforms.UPLAY) 25 | operator = await player.get_operator("sledge") 26 | print(operator.kills) 27 | 28 | await auth.close() 29 | 30 | asyncio.get_event_loop().run_until_complete(run()) 31 | ``` 32 | 33 | ### TODO 34 | 35 | - nothing for now, open an issue if you'd like any new feature added. 36 | 37 | ### License 38 | 39 | 40 | MIT 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/gettingstarted.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | ================ 3 | 4 | Introduction 5 | ------------ 6 | r6sapi.py is a module for easily getting information from the unofficial rainbow six siege api. It allows you to get things such as a players rank and specific stats for operators, gamemodes and queues 7 | The api requires authentication to process any api requests, so r6sapi requires your ubisoft login email and password. 8 | 9 | Quick Example 10 | ------------- 11 | 12 | .. code-block:: python 13 | 14 | import asyncio 15 | import r6sapi as api 16 | 17 | @asyncio.coroutine 18 | def run(): 19 | auth = api.Auth("email", "password") 20 | 21 | player = yield from auth.get_player("billy_yoyo", api.Platforms.UPLAY) 22 | operator = yield from player.get_operator("sledge") 23 | 24 | print(operator.kills) 25 | 26 | asyncio.get_event_loop().run_until_complete(run()) 27 | 28 | 29 | License 30 | ------- 31 | MIT 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2019 billyoyo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | .gitattributes 42 | .pypirc 43 | 44 | # Directories potentially created on remote AFP share 45 | .AppleDB 46 | .AppleDesktop 47 | Network Trash Folder 48 | Temporary Items 49 | .apdisk 50 | __pycache__ 51 | r6sapi.egg-info 52 | build 53 | dist 54 | .idea 55 | .git 56 | testing 57 | 58 | docs/_build/ 59 | 60 | # for virtual environments 61 | .env 62 | venv -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: r6sapi 2 | 3 | API Reference 4 | ============= 5 | 6 | Auth 7 | ---- 8 | 9 | .. autoclass:: Auth 10 | :members: 11 | 12 | 13 | Player 14 | ------ 15 | 16 | .. autoclass:: Player 17 | :members: 18 | 19 | 20 | PlayerBatch 21 | ----------- 22 | 23 | .. autoclass:: PlayerBatch 24 | :members: 25 | 26 | 27 | 28 | Rank 29 | ---- 30 | 31 | .. autoclass:: Rank 32 | :members: 33 | 34 | 35 | 36 | Operator 37 | -------- 38 | 39 | .. autoclass:: Operator 40 | :members: 41 | 42 | 43 | 44 | Weapon 45 | ------ 46 | 47 | .. autoclass:: Weapon 48 | :members: 49 | 50 | 51 | 52 | Gamemode 53 | -------- 54 | 55 | .. autoclass:: Gamemode 56 | :members: 57 | 58 | 59 | 60 | GameQueue 61 | --------- 62 | 63 | .. autoclass:: GameQueue 64 | :members: 65 | 66 | 67 | 68 | Platforms 69 | --------- 70 | 71 | .. autoclass:: Platforms 72 | :members: 73 | 74 | 75 | 76 | RankedRegions 77 | ------------- 78 | 79 | .. autoclass:: RankedRegions 80 | :members: 81 | 82 | 83 | 84 | WeaponTypes 85 | ----------- 86 | 87 | .. autoclass:: WeaponTypes 88 | :members: 89 | -------------------------------------------------------------------------------- /r6sapi/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2016-2019 billyoyo 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | """ 10 | 11 | class InvalidRequest(Exception): 12 | def __init__(self, *args, code=0, **kwargs): 13 | super().__init__(*args, **kwargs) 14 | self.code = code 15 | 16 | 17 | class FailedToConnect(Exception): pass -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from setuptools import setup, find_packages 4 | import re, os 5 | 6 | requirements = [] 7 | with open('requirements.txt') as f: 8 | requirements = f.read().splitlines() 9 | 10 | version = '1.4.1' 11 | 12 | readme = '' 13 | with open('README.md') as f: 14 | readme = f.read() 15 | 16 | setup(name='r6sapi', 17 | author='billyoyo', 18 | author_email='billyoyo@hotmail.co.uk', 19 | url='https://github.com/billy-yoyo/RainbowSixSiege-Python-API', 20 | version=version, 21 | packages=find_packages(), 22 | license='MIT', 23 | description='Interface for Ubisoft API', 24 | long_description=readme, 25 | long_description_content_type='text/markdown', 26 | include_package_data=True, 27 | install_requires=requirements, 28 | extras_require={}, 29 | classifiers=[ 30 | 'Development Status :: 4 - Beta', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Intended Audience :: Developers', 33 | 'Natural Language :: English', 34 | 'Operating System :: OS Independent', 35 | 'Programming Language :: Python :: 3.4', 36 | 'Programming Language :: Python :: 3.5', 37 | 'Topic :: Internet', 38 | 'Topic :: Software Development :: Libraries', 39 | 'Topic :: Software Development :: Libraries :: Python Modules', 40 | 'Topic :: Utilities', 41 | ] 42 | ) -------------------------------------------------------------------------------- /r6sapi/definitions/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2016-2020 jackywathy 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | """ 10 | from r6sapi.definitions.stores import Loadouts, Operators, Seasons 11 | from r6sapi.definitions.loadouts import loadouts_const 12 | from r6sapi.definitions.operators import operators_const 13 | from r6sapi.definitions.seasons import seasons_const 14 | 15 | loadouts = Loadouts(loadouts_const) 16 | operators = Operators(operators_const, loadouts) 17 | seasons = Seasons(seasons_const) 18 | -------------------------------------------------------------------------------- /r6sapi/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2016-2019 billyoyo 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | """ 10 | 11 | __title__ = "r6sapi" 12 | __author__ = "Billyoyo" 13 | __license__ = "MIT" 14 | __copyright__ = "Copyright (c) 2016-2019 Billyoyo" 15 | __version__ = "1.4.1" 16 | 17 | from .auth import * 18 | from .ranks import * 19 | from .players import Player, PlayerBatch 20 | from .gamemodes import * 21 | from .gamequeues import * 22 | from .weapons import * 23 | from .platforms import * 24 | from .operators import * 25 | from .exceptions import * 26 | 27 | -------------------------------------------------------------------------------- /r6sapi/platforms.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2016-2019 billyoyo 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | """ 10 | 11 | class Platforms: 12 | """Platforms supported 13 | 14 | Attributes 15 | ---------- 16 | UPLAY : str 17 | name of the uplay platform 18 | XBOX : str 19 | name of the xbox platform 20 | PLAYSTATION : str 21 | name of the playstation platform""" 22 | 23 | UPLAY = "uplay" 24 | XBOX = "xbl" 25 | PLAYSTATION = "psn" 26 | 27 | 28 | valid_platforms = [x.lower() for x in dir(Platforms) if "_" not in x] 29 | 30 | 31 | PlatformURLNames = { 32 | "uplay": "OSBOR_PC_LNCH_A", 33 | "psn": "OSBOR_PS4_LNCH_A", 34 | "xbl": "OSBOR_XBOXONE_LNCH_A" 35 | } 36 | -------------------------------------------------------------------------------- /r6sapi/gamequeues.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2016-2019 billyoyo 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | """ 10 | 11 | class GameQueue: 12 | """Contains information about a specific game queue 13 | 14 | Attributes 15 | ---------- 16 | name : str 17 | the name for this gamemode (always either "ranked" or "casual" 18 | won : int 19 | the number of wins the player has on this gamemode 20 | lost : int 21 | the number of losses the player has on this gamemode 22 | time_played : int 23 | the amount of time in seconds the player has spent playing on this gamemode 24 | played : int 25 | the number of games the player has played on this gamemode 26 | kills : int 27 | the number of kills the player has on this gamemode 28 | deaths : int 29 | the number of deaths the player has on this gamemode""" 30 | def __init__(self, name, stats=None): 31 | self.name = name 32 | 33 | statname = name + "pvp_" 34 | 35 | stats = stats or {} 36 | self.won = stats.get(statname + "matchwon", 0) 37 | self.lost = stats.get(statname + "matchlost", 0) 38 | self.time_played = stats.get(statname + "timeplayed", 0) 39 | self.played = stats.get(statname + "matchplayed", 0) 40 | self.kills = stats.get(statname + "kills", 0) 41 | self.deaths = stats.get(statname + "death", 0) 42 | 43 | @property 44 | def wins(self): 45 | return self.won 46 | 47 | @property 48 | def losses(self): 49 | return self.lost -------------------------------------------------------------------------------- /docs/how.rst: -------------------------------------------------------------------------------- 1 | How It Works 2 | ============ 3 | 4 | Introduction 5 | ------------ 6 | 7 | Most of the API endpoints can be fairly easily retrieved by going on to the network tab and monitoring the requests sent. 8 | Your browser, as usual, will send a load of unnecessary headers with the request, and a quick bit of testing will show that the only two required are 9 | the "Authorization" header and the "Ubi-AppId" header. (Also the request must have content-type set to application/json) 10 | 11 | 12 | Experimenting 13 | ------------- 14 | 15 | When you're logged in to your account on the website, your "Authorization" header looks like :code:`Ubi_v1 t=[token]` where :code:`[token]` is a load of random characters. 16 | Your Ubi-AppId is a string of characters split by "-", so if we attempt to simply copy/paste these two and use them in our code, it will work but this type of token is called a "ticket" 17 | and is only temporary. Eventually you'll get a response telling you your token is invalid, meaning you need to resend the information you used to recieve your ticket in the first place. 18 | 19 | 20 | Logging In 21 | ---------- 22 | 23 | So clearly some sort of auth login logic is required, where you recieve a new ticket every time your current one runs out. 24 | So if you monitor the requests sent when you log in, you'll see the very first request sent has the authorization header set to :code:`Basic [token]`. 25 | This time :code:`[token]` appears to be constant, and the response you get from this endpoint gives you a valid ticket, along with some other things. 26 | Great, now there's two things left to do: figure out how to generate this token from username/id and figure out how to get you appid 27 | 28 | 29 | Generating The Token 30 | -------------------- 31 | 32 | To do this I read through the javascript on the login page until I found the bit that converts your username and password in to a base64 number. 33 | This is actually, very simply, :code:`base64.encode(email + ":" + password)`. Nice and simple, this solves our first problem. 34 | 35 | 36 | Getting the AppId 37 | ----------------- 38 | 39 | Turns out the AppId doesn't seem to matter at all, after reading through the code I couldn't figure out where the AppId gets decided. 40 | I believe it's generated server-side by ubisoft based on your IP, but either way I did manage to find a default AppId in the code, so unless one is specified, just using that one seems to work. 41 | 42 | 43 | Conclusion 44 | ---------- 45 | 46 | That's basically the end of it, I convert the username and password in to a basic token, then every time a request gets an unauthorized I try and fetch a new one. 47 | Then using the default appid, I can access any of the endpoints easily. -------------------------------------------------------------------------------- /examples/player_batch_full_example.py: -------------------------------------------------------------------------------- 1 | import r6sapi 2 | import asyncio 3 | import json 4 | 5 | async def run_rank(player_batch): 6 | ranks = await player_batch.get_rank(r6sapi.RankedRegions.EU) 7 | 8 | print("current season wins:") 9 | for player in player_batch: 10 | rank = ranks[player.id] 11 | 12 | print(" %s: %s" % (player.name, rank.wins)) 13 | 14 | 15 | async def run_ops(player_batch): 16 | ops = await player_batch.get_operator("ash") 17 | 18 | print("ash wins:") 19 | for player in player_batch: 20 | op = ops[player.id] 21 | print(" %s: %s" % (player.name, op.wins)) 22 | 23 | await player_batch.load_all_operators() 24 | 25 | print("sledge kills:") 26 | for player in player_batch: 27 | op = await player.get_operator("sledge") 28 | print(" %s: %s" % (player.name, op.kills)) 29 | 30 | 31 | async def run_weapons(player_batch): 32 | player_weapons = await player_batch.load_weapons() 33 | 34 | print("submachine gun shots:") 35 | for player in player_batch: 36 | weapons = player_weapons[player.id] 37 | weapon = weapons[r6sapi.WeaponTypes.SUBMACHINE_GUN] 38 | 39 | print(" %s: %s" % (player.name, weapon.shots)) 40 | 41 | 42 | async def run_gamemodes(player_batch): 43 | player_gamemodes = await player_batch.load_gamemodes() 44 | 45 | print("defuse bomb wins:") 46 | for player in player_batch: 47 | gamemodes = player_gamemodes[player.id] 48 | gamemode = gamemodes["plantbomb"] 49 | 50 | print(" %s: %s" % (player.name, gamemode.wins)) 51 | 52 | 53 | async def run_general(player_batch): 54 | await player_batch.load_general() 55 | 56 | print("player deaths:") 57 | for player in player_batch: 58 | print(" %s: %s" % (player.name, player.deaths)) 59 | 60 | 61 | async def run_queues(player_batch): 62 | await player_batch.load_queues() 63 | 64 | print("casual wins:") 65 | for player in player_batch: 66 | print(" %s: %s" % (player.name, player.casual.wins)) 67 | 68 | 69 | async def run_terrohunt(player_batch): 70 | await player_batch.load_terrohunt() 71 | 72 | print("terrorist hunt kills:") 73 | for player in player_batch: 74 | print(" %s: %s" % (player.name, player.terrorist_hunt.kills)) 75 | 76 | 77 | async def run(): 78 | auth = r6sapi.Auth("email", "password") 79 | 80 | player_batch = await auth.get_player_batch(names=["player_1", "player_2", "player_3"], platform=r6sapi.Platforms.UPLAY) 81 | 82 | await run_rank(player_batch) 83 | await run_ops(player_batch) 84 | await run_weapons(player_batch) 85 | await run_gamemodes(player_batch) 86 | await run_general(player_batch) 87 | await run_queues(player_batch) 88 | await run_terrohunt(player_batch) 89 | 90 | await auth.close() 91 | 92 | asyncio.get_event_loop().run_until_complete(run()) 93 | 94 | -------------------------------------------------------------------------------- /r6sapi/gamemodes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2016-2019 billyoyo 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | """ 10 | 11 | GamemodeNames = { 12 | "securearea": "Secure Area", 13 | "rescuehostage": "Hostage Rescue", 14 | "plantbomb": "Bomb" 15 | } 16 | 17 | class Gamemode: 18 | """Contains information about a gamemode 19 | 20 | Attributes 21 | ---------- 22 | type : str 23 | the gamemode id 24 | name : str 25 | the human-readable name for this gamemode 26 | won : int 27 | the number of wins the player has on this gamemode 28 | lost : int 29 | the number of losses the player has on this gamemode 30 | played : int 31 | the number of games this player has played on this gamemode 32 | best_score : int 33 | the best score this player has achieved on this gamemode""" 34 | def __init__(self, gamemodeType, stats=None): 35 | self.type = gamemodeType 36 | self.name = GamemodeNames[self.type] 37 | 38 | statname = gamemodeType + "pvp_" 39 | 40 | stats = stats or {} 41 | self.best_score = stats.get(statname + "bestscore", 0) 42 | self.lost = stats.get(statname + "matchlost", 0) 43 | self.won = stats.get(statname + "matchwon", 0) 44 | self.played = stats.get(statname + "matchplayed", 0) 45 | 46 | if gamemodeType == "securearea": 47 | self.areas_secured = stats.get("generalpvp_servershacked", 0) 48 | self.areas_defended = stats.get("generalpvp_serverdefender", 0) 49 | self.areas_contested = stats.get("generalpvp_serveraggression", 0) 50 | elif gamemodeType == "rescuehostage": 51 | self.hostages_rescued = stats.get("generalpvp_hostagerescue", 0) 52 | self.hostages_defended = stats.get("generalpvp_hostagedefense", 0) 53 | 54 | @property 55 | def wins(self): 56 | return self.won 57 | 58 | @property 59 | def losses(self): 60 | return self.lost 61 | 62 | -------------------------------------------------------------------------------- /r6sapi/weapons.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2016-2019 billyoyo 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | """ 10 | 11 | class WeaponTypes: 12 | """Weapon Types 13 | 14 | Attributes 15 | ---------- 16 | ASSAULT_RIFLE : int 17 | the assault rifle weapon id 18 | SUBMACHINE_GUN : int 19 | the submachine gun weapon id 20 | MARKSMAN_RIFLE : int 21 | the marksman rifle weapon id 22 | SHOTGUN : int 23 | the shotgun weapon id 24 | HANDGUN : int 25 | the handgun weapon id 26 | LIGHT_MACHINE_GUN : int 27 | the light machine gun weapon id 28 | MACHINE_PISTOL : int 29 | the machine pistol weapon id""" 30 | ASSAULT_RIFLE = 0 31 | SUBMACHINE_GUN = 1 32 | MARKSMAN_RIFLE = 2 33 | SHOTGUN = 3 34 | HANDGUN = 4 35 | LIGHT_MACHINE_GUN = 5 36 | MACHINE_PISTOL = 6 37 | 38 | 39 | WeaponNames = [ 40 | "Assault Rifle", 41 | "Submachine Gun", 42 | "Marksman Rifle", 43 | "Shotgun", 44 | "Handgun", 45 | "Light Machine Gun", 46 | "Machine Pistol" 47 | ] 48 | 49 | class Weapon: 50 | """Contains information about a weapon 51 | 52 | Attributes 53 | ---------- 54 | type : int 55 | the weapon type 56 | name : str 57 | the human-friendly name for this weapon type 58 | kills : int 59 | the number of kills the player has for this weapon 60 | headshots : int 61 | the number of headshots the player has for this weapon 62 | hits : int 63 | the number of bullet this player has hit with this weapon 64 | shots : int 65 | the number of bullets this player has shot with this weapon 66 | 67 | """ 68 | def __init__(self, weaponType, stats=None): 69 | self.type = weaponType 70 | self.name = WeaponNames[self.type] 71 | 72 | stat_name = lambda name: "weapontypepvp_%s:%s:infinite" % (name, self.type) 73 | 74 | stats = stats or {} 75 | self.kills = stats.get(stat_name("kills"), 0) 76 | self.headshots = stats.get(stat_name("headshot"), 0) 77 | self.hits = stats.get(stat_name("bullethit"), 0) 78 | self.shots = stats.get(stat_name("bulletfired"), 0) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # r6sapi.py documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Dec 31 12:48:36 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | sys.path.insert(0, os.path.abspath('..')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.napoleon' 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix(es) of source filenames. 43 | # You can specify multiple suffix as a list of string: 44 | # 45 | # source_suffix = ['.rst', '.md'] 46 | source_suffix = '.rst' 47 | 48 | # The master toctree document. 49 | master_doc = 'index' 50 | 51 | # General information about the project. 52 | project = 'r6sapi.py' 53 | copyright = '2016, Billyoyo' 54 | author = 'Billyoyo' 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = '1.4.1' 62 | # The full version, including alpha/beta/rc tags. 63 | release = version 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | # 68 | # This is also used if you do content translation via gettext catalogs. 69 | # Usually you set "language" from the command line for these cases. 70 | language = None 71 | 72 | # List of patterns, relative to source directory, that match files and 73 | # directories to ignore when looking for source files. 74 | # This patterns also effect to html_static_path and html_extra_path 75 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 76 | 77 | # The name of the Pygments (syntax highlighting) style to use. 78 | pygments_style = 'sphinx' 79 | 80 | # If true, `todo` and `todoList` produce output, else they produce nothing. 81 | todo_include_todos = False 82 | 83 | 84 | # -- Options for HTML output ---------------------------------------------- 85 | 86 | # The theme to use for HTML and HTML Help pages. See the documentation for 87 | # a list of builtin themes. 88 | # 89 | html_theme = 'sphinx_rtd_theme' 90 | 91 | rst_prolog = """ 92 | .. |coro| replace:: This function is a |corourl|_. 93 | .. |corourl| replace:: *coroutine* 94 | .. _corourl: https://docs.python.org/3/library/asyncio-task.html#coroutine 95 | """ 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | # 101 | # html_theme_options = {} 102 | 103 | # Add any paths that contain custom static files (such as style sheets) here, 104 | # relative to this directory. They are copied after the builtin static files, 105 | # so a file named "default.css" will overwrite the builtin "default.css". 106 | html_static_path = ['_static'] 107 | 108 | 109 | # -- Options for HTMLHelp output ------------------------------------------ 110 | 111 | # Output file base name for HTML help builder. 112 | htmlhelp_basename = 'r6sapipydoc' 113 | 114 | 115 | # -- Options for LaTeX output --------------------------------------------- 116 | 117 | latex_elements = { 118 | # The paper size ('letterpaper' or 'a4paper'). 119 | # 120 | # 'papersize': 'letterpaper', 121 | 122 | # The font size ('10pt', '11pt' or '12pt'). 123 | # 124 | # 'pointsize': '10pt', 125 | 126 | # Additional stuff for the LaTeX preamble. 127 | # 128 | # 'preamble': '', 129 | 130 | # Latex figure (float) alignment 131 | # 132 | # 'figure_align': 'htbp', 133 | } 134 | 135 | # Grouping the document tree into LaTeX files. List of tuples 136 | # (source start file, target name, title, 137 | # author, documentclass [howto, manual, or own class]). 138 | latex_documents = [ 139 | (master_doc, 'r6sapipy.tex', 'r6sapi.py Documentation', 140 | 'Billyoyo', 'manual'), 141 | ] 142 | 143 | 144 | # -- Options for manual page output --------------------------------------- 145 | 146 | # One entry per manual page. List of tuples 147 | # (source start file, name, description, authors, manual section). 148 | man_pages = [ 149 | (master_doc, 'r6sapipy', 'r6sapi.py Documentation', 150 | [author], 1) 151 | ] 152 | 153 | 154 | # -- Options for Texinfo output ------------------------------------------- 155 | 156 | # Grouping the document tree into Texinfo files. List of tuples 157 | # (source start file, target name, title, author, 158 | # dir menu entry, description, category) 159 | texinfo_documents = [ 160 | (master_doc, 'r6sapipy', 'r6sapi.py Documentation', 161 | author, 'r6sapipy', 'One line description of project.', 162 | 'Miscellaneous'), 163 | ] 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /r6sapi/definitions/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2016-2020 jackywathy 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | Classes that convert the raw dict data into a more structured representation 11 | """ 12 | from typing import Iterable, Union 13 | 14 | import enum 15 | 16 | import r6sapi.definitions 17 | 18 | 19 | class OperatorSide(enum.Enum): 20 | attacker = "attacker" 21 | defender = "defender" 22 | 23 | 24 | class WeaponType(enum.Enum): 25 | primary = "primary" 26 | secondary = "secondary" 27 | gadget = "gadget" 28 | unique_ability = "unique_ability" 29 | 30 | 31 | class Loadout: 32 | """ 33 | Contains a loadout for 1 weapon. Each operator has several loadouts and loadouts can be shared between operators 34 | 35 | Attributes 36 | ---------- 37 | id : str 38 | The id of this weapon. Each loadout with the same gun share the same 'id', e.g. the mk-14-ebr on both dokk and 39 | aruni have the id `6xDz1HSwIn3ZcV9nKIeKUN` as of the time this documentation was written 40 | name : str 41 | the language independent name of this weapon. Generally lowercase english version of the localisedName 42 | weapon_type : WeaponType 43 | the type of weapon. Can be PRIMARY of SECONDARY 44 | weapon_image_url : str 45 | the URL of the image of this weapon 46 | 47 | """ 48 | def __repr__(self): 49 | return "Loadout(id='{}', name='{}', weapon_type='{}', weapon_image_url={})".format(self.id, self.name, self.weapon_type, self.weapon_image_url) 50 | 51 | def __eq__(self, other): 52 | return other.id == self.id and other.name == self.name and other.weapon_type == self.weapon_type and other.weapon_image_url == self.weapon_image_url 53 | 54 | def __init__(self, id, name, weapon_type, weapon_image_url): 55 | self.id = id 56 | self.name = name 57 | if isinstance(weapon_type, WeaponType): 58 | self.weapon_type = weapon_type 59 | else: 60 | self.weapon_type = WeaponType[weapon_type] 61 | self.weapon_image_url = weapon_image_url 62 | 63 | def __str__(self): 64 | return self.__repr__() 65 | 66 | 67 | class OperatorInfo: 68 | """ 69 | Contains only information describing the operator as it appears in the game file 70 | This includes weapon loadouts, name, and icon 71 | 72 | The operator's faction name and image are available in the json, however, these are only available 73 | in a localized fashion and change depending on what language the extracted JSON file is in, so additional 74 | data processing will be required to remove language dependent features 75 | 76 | Attributes 77 | ---------- 78 | id : str 79 | the id of this operator. `should` be unique across all operators 80 | name : str 81 | the language independent name of this operator. Generally lowercase english 82 | icon_url : str 83 | the url at which the operator's badge or icon can be found 84 | loadouts : list[:class:`Loadout`] 85 | a list of loadouts that this operator has access to 86 | side : OperatorSide 87 | which side the operator is one (ATK or DEF) 88 | roles : list[str] 89 | the operator's roles as returned by the API. Can be incomplete or empty, but generally consists of some of the 90 | following: 91 | 'secure', 'intel-gathers', 'anchor', 'covering-fire', 'shield', 92 | index : str 93 | the `index` of this operator as defined by the v1 ubisoft api. this value is needed to query for operator 94 | stats 95 | unique_abilities : Iterable[:class:`UniqueOperatorStat`] 96 | the unique abilities that only this operator has. 97 | """ 98 | def __init__(self, id, name, icon_url, loadouts, side, roles, index, unique_abilities): 99 | """ 100 | Creates a new OperatorInfo object. 101 | Parameters 102 | ---------- 103 | gadget_template: Union[str, Iterable[str]] 104 | the string or strings used to fetch 105 | """ 106 | self.id = id 107 | self.name = name 108 | self.icon_url = icon_url 109 | self.loadouts = loadouts 110 | self.side = side 111 | self.roles = roles 112 | self.index = index 113 | self.unique_abilities = tuple(unique_abilities) 114 | 115 | def __eq__(self, other): 116 | return self.id == other.id and self.name == other.name and self.icon_url == other.icon_url and self.loadouts == r6sapi.definitions.loadouts and self.index == other.index 117 | 118 | def __repr__(self): 119 | return "OperatorInfo(id='{}', name='{}', icon_url='{}'," \ 120 | "loadouts={}, side={}, roles={}, index={})".format(self.id, self.name, self.icon_url, self.loadouts, 121 | self.side, self.roles, self.index) 122 | 123 | def __str__(self): 124 | return self.__repr__() 125 | 126 | 127 | class RankInfo: 128 | def __init__(self, name, min_mmr, max_mmr): 129 | self.name = name 130 | self.min_mmr = min_mmr 131 | self.max_mmr = max_mmr 132 | 133 | 134 | UNRANKED = RankInfo("unranked", -1, -1) 135 | RankInfo.UNRANKED = UNRANKED 136 | 137 | 138 | class Season: 139 | """ 140 | Represents a single season 141 | Attributes 142 | ---------- 143 | id: the id of the season. Guaranteed to be unique 144 | season_code: the code name of the season. In the format YaSb, where a and b are the year and season number. 145 | e.g. Y5S1 for year 5 season 1 146 | season_ranks: the ranks that that occured in this season. Note that the rank system has been 147 | reworked multiple time so some seasons have a different set of ranks from others 148 | operation_name: the full (English) name of the season 149 | 150 | """ 151 | def __init__(self, id, season_code, start_date, season_ranks, operation_name): 152 | self.id = id 153 | self.season_code = season_code 154 | self.start_date = start_date 155 | self.season_ranks = season_ranks 156 | self.operation_name = operation_name 157 | 158 | 159 | class UniqueOperatorStat: 160 | """ 161 | Unique Statistic for an operator (e.g. how many kills with cap's fire bolt) 162 | We query the API using a certain magic string, which varies depending on 163 | Attributes 164 | ---------- 165 | id_template: str 166 | template which can be used to construct the full stat name to use 167 | In the form of `operator{}_smoke_poisongaskill` 168 | name: str 169 | the name of the stat e.g. `Gadgets Jammed` 170 | """ 171 | def __init__(self, id_template, name): 172 | self.id_template = id_template 173 | self.name = name 174 | 175 | @property 176 | def pvp_stat_name(self): 177 | return self.id_template.format("pvp") 178 | 179 | @property 180 | def pve_stat_name(self): 181 | return self.id_template.format("pve") 182 | 183 | def __repr__(self): 184 | return "UniqueOperatorStat(name={}, id_template={})".format(self.name, self.id_template) 185 | 186 | def __str__(self): 187 | return repr(self) 188 | -------------------------------------------------------------------------------- /r6sapi/definitions/stores.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2016-2020 jackywathy 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | These collection classes store a collection of the definition objects that are needed. They convert from the "raw" ' 11 | definitions of models found in {ranks, seasons, maps}.py and make it available as a collection of `model` objects in 12 | the class's constructor. 13 | 14 | If we use an alternative data store in the future it may be worth refactoring out this logic. 15 | """ 16 | from typing import Optional 17 | 18 | import collections 19 | import datetime 20 | 21 | import logging 22 | 23 | from r6sapi.definitions.models import Loadout, OperatorInfo, OperatorSide, Season, RankInfo, UniqueOperatorStat 24 | 25 | 26 | class Loadouts: 27 | """ 28 | Stores all loadouts, providing operations to fetch loadouts depending name and id. 29 | """ 30 | def __init__(self, all_loadouts): 31 | """ 32 | 33 | Reads a list of loadouts and stores it in this object 34 | 35 | Parameters 36 | ---------- 37 | all_loadouts: list[dict] 38 | a list of loadout dictionary objects 39 | """ 40 | self._name_to_loadout = {} 41 | self._id_to_loadout = {} 42 | for loadout_dict in all_loadouts: 43 | loadout = Loadout(**loadout_dict) 44 | self._id_to_loadout[loadout.id] = loadout 45 | self._name_to_loadout[loadout.name.lower()] = loadout 46 | 47 | def from_name(self, name): 48 | """ 49 | Gets a loadout by the name of the weapon 50 | Parameters 51 | ---------- 52 | name 53 | 54 | Returns: 55 | ------- 56 | Optional[:class:`Loadout`]: the found loadout 57 | """ 58 | return self._name_to_loadout.get(name.lower()) 59 | 60 | def from_id(self, id_): 61 | """ 62 | Gets a loadout by its id. 63 | Parameters 64 | ---------- 65 | id_ 66 | 67 | Returns 68 | ------- 69 | Optional[:class:`Loadout`] 70 | 71 | """ 72 | return self._id_to_loadout.get(id_) 73 | 74 | 75 | class Operators: 76 | """ 77 | Stores all operators, providing operations to fetch operators depending on name and id. 78 | """ 79 | def __init__(self, all_operators, loadouts_store): 80 | """ 81 | 82 | Reads a list of operators and stores it in this object. We also 83 | 84 | Parameters 85 | ---------- 86 | all_operators: list[dict] 87 | a list of loadout dictionary objects 88 | loadouts_store: :class:`Loadouts` 89 | a Loadouts object containing all the operators' loadouts 90 | """ 91 | self._name_to_operator = {} 92 | self._id_to_operator = {} 93 | for operator_dict in all_operators: 94 | # separate out the parts of the dictionary that can be just passed through to the constructor 95 | finished_fields = { 96 | key: value for key, value in operator_dict.items() 97 | if key in ("id", "name", "icon_url", "index", "roles") 98 | } 99 | side = OperatorSide[operator_dict["side"]] 100 | 101 | # convert the id -> actual loadout objects 102 | loadouts = [] 103 | for loadout_id in operator_dict["loadouts"]: 104 | found = loadouts_store.from_id(loadout_id) 105 | if found is not None: 106 | loadouts.append(found) 107 | else: 108 | logging.warning("Skipped a loadout from operator %s with id %s", operator_dict["name"], operator_dict["id"]) 109 | 110 | # load in the unique abilities 111 | op_stats = [] 112 | for ability in operator_dict["unique_stats"]: 113 | stat = UniqueOperatorStat(ability["id"], ability["name"]) 114 | op_stats.append(stat) 115 | 116 | op = OperatorInfo(**finished_fields, side=side, loadouts=loadouts, unique_abilities=op_stats) 117 | self._id_to_operator[op.id] = op 118 | self._name_to_operator[op.name.lower()] = op 119 | 120 | def from_name(self, name): 121 | """ 122 | Gets a operator by the name of the weapon 123 | Parameters 124 | ---------- 125 | name 126 | 127 | Returns 128 | ------- 129 | Optional[:class:`OperatorInfo`]: the operator with this name 130 | """ 131 | return self._name_to_operator.get(name.lower()) 132 | 133 | def from_id(self, id_): 134 | """ 135 | Gets a operator by its id. 136 | Parameters 137 | ---------- 138 | id_ 139 | 140 | Returns 141 | ------- 142 | Optional[:class:`OperatorInfo`] 143 | """ 144 | return self._name_to_operator.get(id_) 145 | 146 | def get_all(self): 147 | """ 148 | Gets all the operators, as a list 149 | Returns 150 | list[:class:`OperatorInfo`] 151 | ------- 152 | 153 | """ 154 | return self._name_to_operator.values() 155 | 156 | 157 | class RankInfoCollection(collections.UserList): 158 | """ 159 | Basically a list; but supports a couple convenience methods 160 | """ 161 | def get_rank(self, rank_id): 162 | if rank_id == 0: 163 | # taken from bracket_from_rank 164 | return RankInfo.UNRANKED 165 | 166 | 167 | class Seasons: 168 | """ 169 | 170 | """ 171 | def __init__(self, all_seasons): 172 | self._seasons = [] 173 | for season_dict in all_seasons: 174 | # seperate out the parts of the dictionary that can be just passed through to the constructor 175 | finished_fields = {key: value for key, value in season_dict.items() if 176 | key in ("id", "season_code", "operation_name")} 177 | 178 | season_ranks = RankInfoCollection() 179 | for rank_dict in season_dict["season_ranks"]: 180 | season_ranks.append(RankInfo(**rank_dict)) 181 | 182 | start_date = datetime.datetime.strptime(season_dict["startDate"], "%Y-%m-%dT%H:%M:%S.%fZ") 183 | 184 | season = Season(**finished_fields, start_date=start_date, season_ranks=season_ranks) 185 | 186 | self._seasons.append(season) 187 | 188 | def from_code(self, code): 189 | """ 190 | Gets a season by its code name 191 | Parameters 192 | ---------- 193 | code 194 | 195 | Returns 196 | ------- 197 | Optional[:class:`Season`]: the operator with this name 198 | """ 199 | return next((season for season in self._seasons if season.season_code == code.lower()), None) 200 | 201 | def from_id(self, id_): 202 | """ 203 | Gets a operator by its id. 204 | Parameters 205 | ---------- 206 | id_ 207 | 208 | Returns 209 | ------- 210 | Optional[:class:`Season`] 211 | """ 212 | return next((season for season in self._seasons if season.id == id_), None) 213 | 214 | def __len__(self): 215 | return len(self._seasons) 216 | 217 | @property 218 | def last_season(self): 219 | return self._seasons[-1] 220 | 221 | def __getitem__(self, item): 222 | return self._seasons[item] 223 | 224 | 225 | -------------------------------------------------------------------------------- /r6sapi/ranks.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2016-2019 billyoyo 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | """ 10 | from r6sapi.definitions.models import RankInfo 11 | from r6sapi.definitions.stores import Seasons 12 | 13 | class RankedRegions: 14 | """Ranked regions supported 15 | 16 | Attributes 17 | ---------- 18 | EU : str 19 | name of the european data centre 20 | NA : str 21 | name of the north american data centre 22 | ASIA : str 23 | name of the asian data centre""" 24 | EU = "emea" 25 | NA = "ncsa" 26 | ASIA = "apac" 27 | 28 | 29 | valid_regions = [x.lower() for x in dir(RankedRegions) if "_" not in x] 30 | 31 | 32 | class Rank: 33 | """Contains information about your rank 34 | 35 | Attributes 36 | ---------- 37 | RANKS : list[str] 38 | Names of the ranks 39 | RANK_CHARMS : list[str] 40 | URLs for the rank charms 41 | UNRANKED : int 42 | the unranked bracket id 43 | COPPER : int 44 | the copper bracket id 45 | BRONZE : int 46 | the bronze bracket id 47 | SILVER : int 48 | the silver bracket id 49 | GOLD : int 50 | the gold bracket id 51 | PLATINUM : int 52 | the platinum bracket id 53 | DIAMOND : int 54 | the diamond bracket id 55 | max_mmr : int 56 | the maximum MMR the player has achieved 57 | mmr : int 58 | the MMR the player currently has 59 | wins : int 60 | the number of wins this player has this season 61 | losses : int 62 | the number of losses this player has this season 63 | abandons : int 64 | the number of abandons this player has this season 65 | 66 | rank_id : int 67 | the id of the players current rank 68 | rank : str 69 | the name of the players current rank 70 | max_rank : int 71 | the id of the players max rank 72 | next_rank_mmr : int 73 | the mmr required for the player to achieve their next rank 74 | season : int 75 | the season this rank is for 76 | region : str 77 | the region this rank is for 78 | skill_mean : float 79 | the mean for this persons skill 80 | skill_stdev : float 81 | the standard deviation for this persons skill 82 | """ 83 | 84 | RANKS = ["Unranked", 85 | "Copper 5", "Copper 4", "Copper 3", "Copper 2", "Copper 1", 86 | "Bronze 5", "Bronze 4", "Bronze 3", "Bronze 2", "Bronze 1", 87 | "Silver 5", "Silver 4", "Silver 3", "Silver 2", "Silver 1", 88 | "Gold 3", "Gold 2", "Gold 1", 89 | "Platinum 3", "Platinum 2", "Platinum 1", 90 | "Diamond", 91 | "Champion"] 92 | 93 | # DEPRACATED 94 | RANK_CHARMS = [ 95 | "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/season02%20-%20copper%20charm.44c1ede2.png", 96 | "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/season02%20-%20bronze%20charm.5edcf1c6.png", 97 | "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/season02%20-%20silver%20charm.adde1d01.png", 98 | "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/season02%20-%20gold%20charm.1667669d.png", 99 | "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/season02%20-%20platinum%20charm.d7f950d5.png", 100 | "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/season02%20-%20diamond%20charm.e66cad88.png" 101 | ] 102 | 103 | COMPLETE_RANK_ICONS = [ 104 | # unranked 105 | [ 106 | "https://i.imgur.com/sB11BIz.png", # unranked 107 | ], 108 | # copper 109 | [ 110 | "https://i.imgur.com/0J0jSWB.jpg", # copper 1 111 | "https://i.imgur.com/eI11lah.jpg", # copper 2 112 | "https://i.imgur.com/6CxJoMn.jpg", # copper 3 113 | "https://i.imgur.com/ehILQ3i.jpg", # copper 4 114 | "https://i.imgur.com/B8NCTyX.png", # copper 5 115 | ], 116 | # bronze 117 | [ 118 | "https://i.imgur.com/hmPhPBj.jpg", # bronze 1 119 | "https://i.imgur.com/9AORiNm.jpg", # bronze 2 120 | "https://i.imgur.com/QD5LYD7.jpg", # bronze 3 121 | "https://i.imgur.com/42AC7RD.jpg", # bronze 4 122 | "https://i.imgur.com/TIWCRyO.png" # bronze 5 123 | ], 124 | # silver 125 | [ 126 | "https://i.imgur.com/KmFpkNc.jpg", # silver 1 127 | "https://i.imgur.com/EswGcx1.jpg", # silver 2 128 | "https://i.imgur.com/m8GToyF.jpg", # silver 3 129 | "https://i.imgur.com/D36ZfuR.jpg", # silver 4 130 | "https://i.imgur.com/PY2p17k.png", # silver 5 131 | ], 132 | # gold 133 | [ 134 | "https://i.imgur.com/ffDmiPk.jpg", # gold 1 135 | "https://i.imgur.com/ELbGMc7.jpg", # gold 2 136 | "https://i.imgur.com/B0s1o1h.jpg", # gold 3, 137 | "https://i.imgur.com/6Qg6aaH.jpg", # gold 4 138 | ], 139 | # platinum 140 | [ 141 | "https://i.imgur.com/qDYwmah.png", # plat 1 142 | "https://i.imgur.com/CYMO3Er.png", # plat 2 143 | "https://i.imgur.com/tmcWQ6I.png", # plat 3 144 | ], 145 | # diamond 146 | [ 147 | "https://i.imgur.com/37tSxXm.png", # diamond 148 | ], 149 | # champion 150 | [ 151 | "https://i.imgur.com/VlnwLGk.png", # champion 152 | ] 153 | ] 154 | 155 | RANK_ICONS = [ 156 | COMPLETE_RANK_ICONS[0][0], # unranked 157 | 158 | COMPLETE_RANK_ICONS[1][4], # copper 5 159 | COMPLETE_RANK_ICONS[1][3], # copper 4 160 | COMPLETE_RANK_ICONS[1][2], # copper 3 161 | COMPLETE_RANK_ICONS[1][1], # copper 2 162 | COMPLETE_RANK_ICONS[1][0], # copper 1 163 | 164 | COMPLETE_RANK_ICONS[2][4], # bronze 5 165 | COMPLETE_RANK_ICONS[2][3], # bronze 4 166 | COMPLETE_RANK_ICONS[2][2], # bronze 3 167 | COMPLETE_RANK_ICONS[2][1], # bronze 2 168 | COMPLETE_RANK_ICONS[2][0], # bronze 1 169 | 170 | COMPLETE_RANK_ICONS[3][4], # silver 5 171 | COMPLETE_RANK_ICONS[3][3], # silver 4 172 | COMPLETE_RANK_ICONS[3][2], # silver 3 173 | COMPLETE_RANK_ICONS[3][1], # silver 2 174 | COMPLETE_RANK_ICONS[3][0], # silver 1 175 | 176 | COMPLETE_RANK_ICONS[4][2], # gold 3 177 | COMPLETE_RANK_ICONS[4][1], # gold 2 178 | COMPLETE_RANK_ICONS[4][0], # gold 1 179 | 180 | COMPLETE_RANK_ICONS[5][2], # platinum 3 181 | COMPLETE_RANK_ICONS[5][1], # platinum 2 182 | COMPLETE_RANK_ICONS[5][0], # platinum 1 183 | 184 | COMPLETE_RANK_ICONS[6][0], # diamond 185 | 186 | COMPLETE_RANK_ICONS[7][0], # champion 187 | ] 188 | 189 | 190 | 191 | @staticmethod 192 | def bracket_from_rank(rank_id): 193 | if rank_id == 0: return -1 # unranked 194 | elif rank_id <= 5: return 0 # copper 195 | elif rank_id <= 10: return 1 # bronze 196 | elif rank_id <= 15: return 2 # silver 197 | elif rank_id <= 18: return 3 # gold 198 | elif rank_id <= 21: return 4 # platinum 199 | elif rank_id <= 22: return 5 # diamond 200 | else: return 6 # champion 201 | 202 | @staticmethod 203 | def bracket_name(bracket): 204 | if bracket == -1: return "Unranked" 205 | elif bracket == 0: return "Copper" 206 | elif bracket == 1: return "Bronze" 207 | elif bracket == 2: return "Silver" 208 | elif bracket == 3: return "Gold" 209 | elif bracket == 4: return "Platinum" 210 | elif bracket == 5: return "Diamond" 211 | else: return "Champion" 212 | 213 | UNRANKED = -1 214 | COPPER = 0 215 | BRONZE = 1 216 | SILVER = 2 217 | GOLD = 3 218 | PLATINUM = 4 219 | DIAMOND = 5 220 | CHAMPION = 6 221 | 222 | def __init__(self, data, rank_definitions): 223 | """ 224 | 225 | Parameters 226 | ---------- 227 | data 228 | rank_definitions: :class:`RankInfoCollection` 229 | """ 230 | self._rank_definitions = rank_definitions 231 | self._new_ranks_threshold = 14 232 | 233 | self.max_mmr = data.get("max_mmr") 234 | self.mmr = data.get("mmr") 235 | self.wins = data.get("wins") 236 | self.losses = data.get("losses") 237 | 238 | self.rank_id = data.get("rank", 0) 239 | self.rank = rank_definitions.get_rank(self.rank_id) 240 | 241 | self.max_rank = data.get("max_rank") 242 | self.next_rank_mmr = data.get("next_rank_mmr") 243 | self.season = data.get("season") 244 | self.region = data.get("region") 245 | self.abandons = data.get("abandons") 246 | self.skill_mean = data.get("skill_mean") 247 | self.skill_stdev = data.get("skill_stdev") 248 | 249 | @property 250 | def _season_definitions(self): 251 | if self.season >= len(self._rank_definitions): 252 | return self._rank_definitions.last_season 253 | return self._rank_definitions[self.season] 254 | 255 | def get_icon_url(self): 256 | """Get URL for this rank's icon 257 | 258 | Returns 259 | ------- 260 | :class:`str` 261 | the URL for the rank icon""" 262 | 263 | if self.season > self._new_ranks_threshold: 264 | return Rank.RANK_ICONS[self.rank_id] 265 | 266 | bracket = self.get_bracket() 267 | rank_index = self._get_bracket_rank_index() 268 | 269 | print("getting icon url for bracket=%s, rank_index=%s (rank = %s)" % (bracket, rank_index, self.get_rank_name())) 270 | 271 | return Rank.COMPLETE_RANK_ICONS[bracket + 1][rank_index] 272 | 273 | # DEPRACATED 274 | def get_charm_url(self): 275 | """Get charm URL for the bracket this rank is in 276 | 277 | Returns 278 | ------- 279 | :class:`str` 280 | the URL for the charm 281 | 282 | """ 283 | if self.rank_id <= 4: return self.RANK_CHARMS[0] 284 | if self.rank_id <= 8: return self.RANK_CHARMS[1] 285 | if self.rank_id <= 12: return self.RANK_CHARMS[2] 286 | if self.rank_id <= 16: return self.RANK_CHARMS[3] 287 | if self.rank_id <= 19: return self.RANK_CHARMS[4] 288 | return self.RANK_CHARMS[5] 289 | 290 | def get_bracket(self, rank_id=None): 291 | """Get rank bracket 292 | 293 | Returns 294 | ------- 295 | :class:`int` 296 | the id for the rank bracket this rank is in 297 | 298 | """ 299 | rank_id = rank_id if rank_id is not None else self.rank_id 300 | 301 | if self.season > self._new_ranks_threshold: 302 | return Rank.bracket_from_rank(rank_id) 303 | 304 | for i, division in enumerate(self._season_definitions["divisions"]): 305 | if rank_id in division["ranks"]: 306 | return i 307 | return -1 308 | 309 | def get_bracket_name(self, rank_id=None): 310 | """Get rank bracket name 311 | 312 | Returns 313 | ------- 314 | :class:`str` 315 | the name for the rank bracket this rank is in 316 | """ 317 | bracket = self.get_bracket(rank_id=rank_id) 318 | 319 | if self.season > self._new_ranks_threshold: 320 | return Rank.bracket_name(bracket) 321 | 322 | if bracket < 0: 323 | return "Unranked" 324 | return self._season_definitions["divisions"][bracket]["id"].title() 325 | 326 | def _get_bracket_rank_index(self, rank_id=None): 327 | """Gets the rank index within the bracket (e.g. 0 for gold 1), returns -1 if it failed to find rank index 328 | 329 | Returns 330 | ------- 331 | :class:`int` 332 | the rank index within the bracket 333 | """ 334 | rank_id = rank_id if rank_id is not None else self.rank_id 335 | 336 | bracket = self.get_bracket(rank_id=rank_id) 337 | 338 | ranks = self._season_definitions["divisions"][bracket]["ranks"] 339 | 340 | rank_index = -1 341 | for i, rid in enumerate(ranks): 342 | if rid == rank_id: 343 | rank_index = len(ranks) - (i + 1) 344 | break 345 | 346 | return rank_index 347 | 348 | def get_rank_name(self, rank_id=None): 349 | """Get rank name 350 | 351 | Returns 352 | ------- 353 | :class:`str` 354 | the name for this rank 355 | """ 356 | rank_id = rank_id if rank_id is not None else self.rank_id 357 | 358 | if self.season > self._new_ranks_threshold: 359 | return Rank.RANKS[rank_id] 360 | 361 | bracket_name = self.get_bracket_name(rank_id) 362 | rank_index = self._get_bracket_rank_index(rank_id) 363 | 364 | if bracket_name.lower() in ["unranked", "diamond", "champion"]: 365 | return bracket_name 366 | 367 | return "%s %s" % (bracket_name, rank_index + 1) 368 | 369 | def get_max_rank_name(self): 370 | """Get rank name of max rank 371 | 372 | Returns 373 | ------- 374 | :class:`str` 375 | the name for this rank 376 | """ 377 | return self.get_rank_name(self.max_rank) -------------------------------------------------------------------------------- /r6sapi/auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2016-2019 billyoyo 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | """ 10 | 11 | import aiohttp 12 | import asyncio 13 | import time 14 | import json 15 | import base64 16 | from urllib import parse 17 | 18 | from .players import Player, PlayerBatch 19 | from .exceptions import * 20 | 21 | 22 | class Auth: 23 | """Holds your authentication information. Used to retrieve Player objects 24 | Once you're done with the auth object, auth.close() should be called. 25 | 26 | Parameters 27 | ---------- 28 | email : Optional[str] 29 | Your Ubisoft email 30 | password : Optional[str] 31 | Your Ubisoft password 32 | token : Optional[str] 33 | Your Ubisoft auth token, either supply this OR email/password 34 | appid : Optional[str] 35 | Your Ubisoft appid, not required 36 | cachetime : Optional[float] 37 | How long players are cached for (in seconds) 38 | max_connect_retries : Optional[int] 39 | How many times the auth client will automatically try to reconnect, high numbers can get you temporarily banned 40 | refresh_session_period : Optional[int] 41 | How frequently the http session should be refreshed, in seconds. Negative number for never. Defaults to 3 minutes. 42 | 43 | Attributes 44 | ---------- 45 | session 46 | aiohttp client session 47 | token : str 48 | your token 49 | appid : str 50 | your appid 51 | sessionid : str 52 | the current connections session id (will change upon attaining new key) 53 | key : str 54 | your current auth key (will change every time you connect) 55 | spaceids : dict 56 | contains the spaceid for each platform 57 | profileid : str 58 | your profileid (corresponds to your appid) 59 | userid : str 60 | your userid (corresponds to your appid) 61 | cachetime : float 62 | the time players are cached for 63 | cache : dict 64 | the current player cache 65 | 66 | """ 67 | 68 | @staticmethod 69 | def get_basic_token(email, password): 70 | return base64.b64encode((email + ":" + password).encode("utf-8")).decode("utf-8") 71 | 72 | def __init__(self, email=None, password=None, token=None, appid=None, 73 | cachetime=120, max_connect_retries=1, session=None, 74 | refresh_session_period=180): 75 | if session is not None: 76 | self.session = session 77 | else: 78 | self.session = aiohttp.ClientSession() 79 | 80 | self.max_connect_retries = max_connect_retries 81 | self.refresh_session_period = refresh_session_period 82 | 83 | if email is not None and password is not None: 84 | self.token = Auth.get_basic_token(email, password) 85 | elif token is not None: 86 | self.token = token 87 | else: 88 | raise TypeError("Argument error, requires either email/password or token to be set, neither given") 89 | 90 | if appid is not None: 91 | self.appid = appid 92 | else: 93 | self.appid = "39baebad-39e5-4552-8c25-2c9b919064e2" 94 | 95 | self.sessionid = "" 96 | self.key = "" 97 | self.uncertain_spaceid = "" 98 | self.spaceids = { 99 | "uplay": "5172a557-50b5-4665-b7db-e3f2e8c5041d", 100 | "psn": "05bfb3f7-6c21-4c42-be1f-97a33fb5cf66", 101 | "xbl": "98a601e5-ca91-4440-b1c5-753f601a2c90" 102 | } 103 | self.profileid = "" 104 | self.userid = "" 105 | self.genome = "" 106 | 107 | self.cachetime = cachetime 108 | self.cache={} 109 | 110 | self._login_cooldown = 0 111 | self._session_start = time.time() 112 | 113 | @asyncio.coroutine 114 | def close(self): 115 | """|coro| 116 | 117 | Closes the session associated with the auth object""" 118 | yield from self.session.close() 119 | 120 | @asyncio.coroutine 121 | def refresh_session(self): 122 | """|coro| 123 | 124 | Closes the current session and opens a new one""" 125 | if self.session: 126 | try: 127 | yield from self.session.close() 128 | except: 129 | # we don't care if closing the session does nothing 130 | pass 131 | 132 | self.session = aiohttp.ClientSession() 133 | self._session_start = time.time() 134 | 135 | @asyncio.coroutine 136 | def _ensure_session_valid(self): 137 | if not self.session: 138 | yield from self.refresh_session() 139 | elif self.refresh_session_period >= 0 and time.time() - self._session_start >= self.refresh_session_period: 140 | yield from self.refresh_session() 141 | 142 | @asyncio.coroutine 143 | def get_session(self): 144 | """|coro| 145 | 146 | Retrieves the current session, ensuring it's valid first""" 147 | yield from self._ensure_session_valid() 148 | return self.session 149 | 150 | @asyncio.coroutine 151 | def connect(self): 152 | """|coro| 153 | 154 | Connect to ubisoft, automatically called when needed""" 155 | if time.time() < self._login_cooldown: 156 | raise FailedToConnect("login on cooldown") 157 | 158 | session = yield from self.get_session() 159 | resp = yield from session.post("https://public-ubiservices.ubi.com/v3/profiles/sessions", headers = { 160 | "Content-Type": "application/json", 161 | "Ubi-AppId": self.appid, 162 | "Authorization": "Basic " + self.token 163 | }, data=json.dumps({"rememberMe": True})) 164 | 165 | data = yield from resp.json() 166 | 167 | message = "Unknown Error" 168 | if "message" in data and "httpCode" in data: 169 | message = "HTTP %s: %s" % (data["httpCode"], data["message"]) 170 | elif "message" in data: 171 | message = data["message"] 172 | elif "httpCode" in data: 173 | message = str(data["httpCode"]) 174 | 175 | if "ticket" in data: 176 | self.key = data.get("ticket") 177 | self.sessionid = data.get("sessionId") 178 | self.uncertain_spaceid = data.get("spaceId") 179 | else: 180 | raise FailedToConnect(message) 181 | 182 | @asyncio.coroutine 183 | def get(self, *args, retries=0, referer=None, json=True, **kwargs): 184 | if not self.key: 185 | last_error = None 186 | for i in range(self.max_connect_retries): 187 | try: 188 | yield from self.connect() 189 | break 190 | except FailedToConnect as e: 191 | last_error = e 192 | else: 193 | # assume this error is going uncaught, so we close the session 194 | yield from self.close() 195 | 196 | if last_error: 197 | raise last_error 198 | else: 199 | raise FailedToConnect("Unknown Error") 200 | 201 | if "headers" not in kwargs: kwargs["headers"] = {} 202 | kwargs["headers"]["Authorization"] = "Ubi_v1 t=" + self.key 203 | kwargs["headers"]["Ubi-AppId"] = self.appid 204 | kwargs["headers"]["Ubi-SessionId"] = self.sessionid 205 | kwargs["headers"]["Connection"] = "keep-alive" 206 | if referer is not None: 207 | if isinstance(referer, Player): 208 | referer = "https://game-rainbow6.ubi.com/en-gb/uplay/player-statistics/%s/multiplayer" % referer.id 209 | kwargs["headers"]["Referer"] = str(referer) 210 | 211 | session = yield from self.get_session() 212 | resp = yield from session.get(*args, **kwargs) 213 | 214 | if json: 215 | try: 216 | data = yield from resp.json() 217 | except: 218 | text = yield from resp.text() 219 | 220 | message = text.split("h1>") 221 | if len(message) > 1: 222 | message = message[1][:-2] 223 | code = 0 224 | if "502" in message: code = 502 225 | else: 226 | message = text 227 | 228 | raise InvalidRequest("Received a text response, expected JSON response. Message: %s" % message, code=code) 229 | 230 | if "httpCode" in data: 231 | if data["httpCode"] == 401: 232 | if retries >= self.max_connect_retries: 233 | # wait 30 seconds before sending another request 234 | self._login_cooldown = time.time() + 30 235 | 236 | # key no longer works, so remove key and let the following .get() call refresh it 237 | self.key = None 238 | result = yield from self.get(*args, retries=retries+1, **kwargs) 239 | return result 240 | else: 241 | msg = data.get("message", "") 242 | if data["httpCode"] == 404: msg = "Missing resource %s" % data.get("resource", args[0]) 243 | raise InvalidRequest("HTTP %s: %s" % (data["httpCode"], msg), code=data["httpCode"]) 244 | 245 | return data 246 | else: 247 | text = yield from resp.text() 248 | return text 249 | 250 | @asyncio.coroutine 251 | def get_players(self, name=None, platform=None, uid=None): 252 | """|coro| 253 | 254 | get a list of players matching the term on that platform, 255 | exactly one of uid and name must be given, platform must be given, 256 | this list almost always has only 1 element, so it's easier to use get_player 257 | 258 | Parameters 259 | ---------- 260 | name : str 261 | the name of the player you're searching for 262 | platform : str 263 | the name of the platform you're searching on (See :class:`Platforms`) 264 | uid : str 265 | the uid of the player you're searching for 266 | 267 | Returns 268 | ------- 269 | list[:class:`Player`] 270 | list of found players""" 271 | 272 | if name is None and uid is None: 273 | raise TypeError("name and uid are both None, exactly one must be given") 274 | 275 | if name is not None and uid is not None: 276 | raise TypeError("cannot search by uid and name at the same time, please give one or the other") 277 | 278 | if platform is None: 279 | raise TypeError("platform cannot be None") 280 | 281 | if "platform" not in self.cache: self.cache[platform] = {} 282 | 283 | if name: 284 | cache_key = "NAME:%s" % name 285 | else: 286 | cache_key = "UID:%s" % uid 287 | 288 | if cache_key in self.cache[platform]: 289 | if self.cachetime > 0 and self.cache[platform][cache_key][0] < time.time(): 290 | del self.cache[platform][cache_key] 291 | else: 292 | return self.cache[platform][cache_key][1] 293 | 294 | if name: 295 | data = yield from self.get("https://public-ubiservices.ubi.com/v3/profiles?nameOnPlatform=%s&platformType=%s" % (parse.quote(name), parse.quote(platform))) 296 | else: 297 | data = yield from self.get("https://public-ubiservices.ubi.com/v3/users/%s/profiles?platformType=%s" % (uid, parse.quote(platform))) 298 | 299 | if "profiles" in data: 300 | results = [Player(self, x) for x in data["profiles"] if x.get("platformType", "") == platform] 301 | if len(results) == 0: raise InvalidRequest("No results") 302 | if self.cachetime != 0: 303 | self.cache[platform][cache_key] = [time.time() + self.cachetime, results] 304 | return results 305 | else: 306 | raise InvalidRequest("Missing key profiles in returned JSON object %s" % str(data)) 307 | 308 | @asyncio.coroutine 309 | def get_player(self, name=None, platform=None, uid=None): 310 | """|coro| 311 | 312 | Calls get_players and returns the first element, 313 | exactly one of uid and name must be given, platform must be given 314 | 315 | Parameters 316 | ---------- 317 | name : str 318 | the name of the player you're searching for 319 | platform : str 320 | the name of the platform you're searching on (See :class:`Platforms`) 321 | uid : str 322 | the uid of the player you're searching for 323 | 324 | Returns 325 | ------- 326 | :class:`Player` 327 | player found""" 328 | 329 | results = yield from self.get_players(name=name, platform=platform, uid=uid) 330 | return results[0] 331 | 332 | @asyncio.coroutine 333 | def get_player_batch(self, names=None, platform=None, uids=None): 334 | """|coro| 335 | 336 | Calls get_player for each name and uid you give, and creates a player batch out of 337 | all the resulting player objects found. See :class:`PlayerBatch` for how to use this. 338 | 339 | Parameters 340 | ---------- 341 | names : list[str] 342 | a list of player names to add to the batch, can be none 343 | uids : list[str] 344 | a list of player uids to add to the batch, can be none 345 | platform : str 346 | the name of the platform you're searching for players on (See :class:`Platforms`) 347 | 348 | Returns 349 | ------- 350 | :class:`PlayerBatch` 351 | the player batch 352 | """ 353 | if names is None and uids is None: 354 | raise TypeError("names and uids are both None, at least one must be given") 355 | 356 | players = {} 357 | 358 | if names is not None: 359 | for name in names: 360 | player = yield from self.get_player(name=name, platform=platform) 361 | players[player.id] = player 362 | 363 | if uids is not None: 364 | for uid in uids: 365 | player = yield from self.get_player(uid=uid, platform=platform) 366 | players[player.id] = player 367 | 368 | return PlayerBatch(players) 369 | -------------------------------------------------------------------------------- /r6sapi/operators.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2016-2019 billyoyo 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | """ 10 | 11 | OperatorUrlStatisticNames = ["operatorpvp_kills","operatorpvp_death","operatorpvp_roundwon", 12 | "operatorpvp_roundlost","operatorpvp_meleekills","operatorpvp_totalxp", 13 | "operatorpvp_headshot","operatorpvp_timeplayed","operatorpvp_dbno"] 14 | 15 | # DEPRECATED - this dict is no longer updated with new OPs (sorry) 16 | OperatorProfiles = { 17 | "DOC": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-doc.0b0321eb.png", 18 | "TWITCH": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-twitch.70219f02.png", 19 | "ASH": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-ash.9d28aebe.png", 20 | "THERMITE": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-thermite.e973bb04.png", 21 | "BLITZ": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-blitz.734e347c.png", 22 | "BUCK": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-buck.78712d24.png", 23 | "HIBANA": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-hibana.2010ec35.png", 24 | "KAPKAN": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-kapkan.db3ab661.png", 25 | "PULSE": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-pulse.30ab3682.png", 26 | "CASTLE": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-castle.b95704d7.png", 27 | "ROOK": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-rook.b3d0bfa3.png", 28 | "BANDIT": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-bandit.6d7d15bc.png", 29 | "SMOKE": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-smoke.1bf90066.png", 30 | "FROST": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-frost.f4325d10.png", 31 | "VALKYRIE": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-valkyrie.c1f143fb.png", 32 | "TACHANKA": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-tachanka.41caebce.png", 33 | "GLAZ": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-glaz.8cd96a16.png", 34 | "FUZE": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-fuze.dc9f2a14.png", 35 | "SLEDGE": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-sledge.832f6c6b.png", 36 | "MONTAGNE": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-montagne.1d04d00a.png", 37 | "MUTE": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-mute.ae51429f.png", 38 | "ECHO": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-echo.662156dc.png", 39 | "THATCHER": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-thatcher.73132fcd.png", 40 | "CAPITAO": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-capitao.1d0ea713.png", 41 | "IQ": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-iq.d97d8ee2.png", 42 | "BLACKBEARD": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-blackbeard.2292a791.png", 43 | "JAGER": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-jaeger.d8a6c470.png", 44 | "CAVEIRA": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/large-caveira.e4d82365.png", 45 | "DEFAULT": "https://ubistatic-a.akamaihd.net/0058/prod/assets/styles/images/mask-large-bandit.fc038cf1.png" 46 | } 47 | 48 | 49 | # DEPRECATED - use Auth.get_operator_badge() instead 50 | OperatorIcons = { 51 | "DEFAULT": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Glaz_Badge_229122.png", 52 | "HIBANA": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/R6-operators-badge-hibana_275569.png", 53 | "SMOKE": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Smoke_Badge_196198.png", 54 | "KAPKAN": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Kapkan_Badge_229123.png", 55 | "TACHANKA": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Tachanka_Badge_229124.png", 56 | "THERMITE": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Thermite_Badge_196408.png", 57 | "THATCHER": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Thatcher_Badge_196196.png", 58 | "GLAZ": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Glaz_Badge_229122.png", 59 | "BANDIT": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Bandit_Badge_222163.png", 60 | "ROOK": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Rook_Badge_211296.png", 61 | "IQ": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/IQ_Badge_222165.png", 62 | "PULSE": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Pulse_Badge_202497.png", 63 | "MUTE": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Mute_Badge_196195.png", 64 | "VALKYRIE": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/R6-operators-badge-valkyrie_250313.png", 65 | "FROST": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/R6-operators-badge-frost_237595.png", 66 | "DOC": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Doc_Badge_211294.png", 67 | "SLEDGE": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Sledge_Badge_196197.png", 68 | "JAGER": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Jager_Badge_222166.png", 69 | "BLACKBEARD": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/R6-operators-badge-blackbeard_250312.png", 70 | "FUZE": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Fuze_Badge_229121.png", 71 | "ECHO": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/R6-operators-badge-echo_275572.png", 72 | "CAVEIRA": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/R6-operators-badge-caveira_263102.png", 73 | "BLITZ": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Blitz_Badge_222164.png", 74 | "MONTAGNE": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Montagne_Badge_211295.png", 75 | "ASH": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Ash_Badge_196406.png", 76 | "TWITCH": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Twitch_Badge_211297.png", 77 | "CASTLE": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/Castle_Badge_196407.png", 78 | "BUCK": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/R6-operators-badge-buck_237592.png", 79 | "CAPITAO": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/R6-operators-badge-capitao_263100.png", 80 | "JACKAL": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/R6-velvet-shell-badge-jackal_282825.png", 81 | "MIRA": "https://ubistatic19-a.akamaihd.net/resource/en-GB/game/rainbow6/siege/R6-velvet-shell-badge-mira_282826.png", 82 | "ELA": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/badge-ela.63ec2d26.png", 83 | "LESION": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/badge-lesion.07c3d352.png", 84 | "YING": "https://ubistatic-a.akamaihd.net/0058/prod/assets/images/badge-ying.b88be612.png", 85 | "DOKKAEBI": "https://ubistatic19-a.akamaihd.net/resource/en-us/game/rainbow6/siege/r6-white-noise-badge-dokkaebi_306314.png", 86 | "VIGIL": "https://ubistatic19-a.akamaihd.net/resource/en-us/game/rainbow6/siege/r6-white-noise-badge-vigil_306315.png", 87 | "ZOFIA": "https://ubistatic19-a.akamaihd.net/resource/en-gb/game/rainbow6/siege/zofia_badge_306416.png" 88 | } 89 | 90 | 91 | # DEPRECATED - use OperatorInfo.unique_abilities[0] 92 | OperatorStatistics = { 93 | "DOC": "teammaterevive", 94 | "TWITCH": "gadgetdestroybyshockdrone", 95 | "ASH": "bonfirewallbreached", 96 | "THERMITE": "reinforcementbreached", 97 | "BLITZ": "flashedenemy", 98 | "BUCK": "kill", 99 | "HIBANA": "detonate_projectile", 100 | "KAPKAN": "boobytrapkill", 101 | "PULSE": "heartbeatspot", 102 | "CASTLE": "kevlarbarricadedeployed", 103 | "ROOK": "armortakenteammate", 104 | "BANDIT": "batterykill", 105 | "SMOKE": "poisongaskill", 106 | "FROST": "dbno", 107 | "VALKYRIE": "camdeployed", 108 | "TACHANKA": "turretkill", 109 | "GLAZ": "sniperkill", 110 | "FUZE": "clusterchargekill", 111 | "SLEDGE": "hammerhole", 112 | "MONTAGNE": "shieldblockdamage", 113 | "MUTE": "gadgetjammed", 114 | "ECHO": "enemy_sonicburst_affected", 115 | "THATCHER": "gadgetdestroywithemp", 116 | "CAPITAO": "lethaldartkills", 117 | "IQ": "gadgetspotbyef", 118 | "BLACKBEARD": "gunshieldblockdamage", 119 | "JAGER": "gadgetdestroybycatcher", 120 | "CAVEIRA": "interrogations", 121 | "JACKAL": "cazador_assist_kill", 122 | "MIRA": "black_mirror_gadget_deployed", 123 | "LESION": "caltrop_enemy_affected", 124 | "ELA": "concussionmine_detonate", 125 | "YING": "dazzler_gadget_detonate", 126 | "DOKKAEBI": "phoneshacked", 127 | "VIGIL": "diminishedrealitymode", 128 | "ZOFIA": "concussiongrenade_detonate" 129 | } 130 | 131 | 132 | # DEPRECATED - use OperatorInfo.statistic_name 133 | OperatorStatisticNames = { 134 | "DOC": "Teammates Revived", 135 | "TWITCH": "Gadgets Destroyed With Shock Drone", 136 | "ASH": "Walls Breached", 137 | "THERMITE": "Reinforcements Breached", 138 | "BLITZ": "Enemies Flashed", 139 | "BUCK": "Shotgun Kills", 140 | "HIBANA": "Projectiles Detonated", 141 | "KAPKAN": "Boobytrap Kills", 142 | "PULSE": "Heartbeat Spots", 143 | "CASTLE": "Barricades Deployed", 144 | "ROOK": "Armor Taken", 145 | "BANDIT": "Battery Kills", 146 | "SMOKE": "Poison Gas Kills", 147 | "FROST": "DBNOs From Traps", 148 | "VALKYRIE": "Cameras Deployed", 149 | "TACHANKA": "Turret Kills", 150 | "GLAZ": "Sniper Kills", 151 | "FUZE": "Cluster Charge Kills", 152 | "SLEDGE": "Hammer Holes", 153 | "MONTAGNE": "Damage Blocked", 154 | "MUTE": "Gadgets Jammed", 155 | "ECHO": "Enemies Sonic Bursted", 156 | "THATCHER": "Gadgets Destroyed", 157 | "CAPITAO": "Lethal Dart Kills", 158 | "IQ": "Gadgets Spotted", 159 | "BLACKBEARD": "Damage Blocked", 160 | "JAGER": "Projectiles Destroyed", 161 | "CAVEIRA": "Interrogations", 162 | "JACKAL": "Footprint Scan Assists", 163 | "MIRA": "Black Mirrors Deployed", 164 | "LESION": "Enemies poisoned by Gu mines", 165 | "YING": "Candela devices detonated", 166 | "ELA": "Grzmot Mines Detonated", 167 | "DOKKAEBI": "Phones Hacked", 168 | "VIGIL": "Drones Deceived", 169 | "ZOFIA": "Concussion Grenades Detonated", 170 | "FINKA": "Nano-boosts used", 171 | "LION": "Enemies revealed", 172 | "ALIBI": "Enemies pinged by decoys", 173 | "MAESTRO": "Enemies spotted with turret camera", 174 | "MAVERICK": "D.I.Y. Blowtorch", 175 | "CLASH": "CCE Shield", 176 | "NOMAD": "No statistic available", 177 | "KAID": "No statistic available", 178 | "MOZZIE": "Drones Hacked", 179 | "GRIDLOCK": "Trax Deployed", 180 | "WARDEN": "Flashes Resisted", 181 | "NOKK": "Observation tools deceived", 182 | "AMARU": "Distance Reeled", 183 | "GOYO": "Volcans Detonated", 184 | "KALI": "Gadgets destroyed with explosive lance", 185 | "WAMAI": "Gadgets destroyed by magnet", 186 | "IANA": "Kills after using replicator", 187 | "ORYX": "kills after dash", 188 | "ACE": "S.E.L.M.A. Detonations", 189 | "MELUSI": "Attackers slowed by Banshee", 190 | "ZERO": "Gadgets Destroyed by ARGUS Camera", 191 | "ARUNI": "none" 192 | } 193 | 194 | 195 | class Operator: 196 | """Contains information about an operator 197 | 198 | Attributes 199 | ---------- 200 | name : str 201 | the name of the operator 202 | wins : int 203 | the number of wins the player has on this operator 204 | losses : int 205 | the number of losses the player has on this operator 206 | kills : int 207 | the number of kills the player has on this operator 208 | deaths : int 209 | the number of deaths the player has on this operator 210 | headshots : int 211 | the number of headshots the player has on this operator 212 | melees : int 213 | the number of melee kills the player has on this operator 214 | dbnos : int 215 | the number of DBNO (down-but-not-out)'s the player has on this operator 216 | xp : int 217 | the total amount of xp the player has on this operator 218 | time_played : int 219 | the amount of time the player has played this operator for in seconds 220 | statistic : int 221 | the value for this operators unique statistic (depreciated in favour of unique_stats) 222 | statistic_name : str 223 | the human-friendly name for this operators statistic (depreciated in favour of unique_stats) 224 | unique_stats : dict[:class:`UniqueOperatorStat`, int] 225 | mapping of an operator's unique stat to number of times that stat has been achieved (e.g. kills with a gadget) 226 | """ 227 | def __init__(self, name, stats=None, unique_stats=None): 228 | self.name = name.lower() 229 | 230 | stats = stats or {} 231 | self.wins = stats.get("roundwon", 0) 232 | self.losses = stats.get("roundlost", 0) 233 | self.kills = stats.get("kills", 0) 234 | self.deaths = stats.get("death", 0) 235 | self.headshots = stats.get("headshot", 0) 236 | self.melees = stats.get("meleekills", 0) 237 | self.dbnos = stats.get("dbno", 0) 238 | self.xp = stats.get("totalxp", 0) 239 | self.time_played = stats.get("timeplayed", 0) 240 | 241 | if unique_stats is not None: 242 | self.unique_stats = unique_stats 243 | else: 244 | self.unique_stats = {} 245 | 246 | @property 247 | def statistic(self): 248 | # get the first unique statistic `stat`, e.g. the number of kills using a gadget 249 | stat = _first_key(self.unique_stats) 250 | if stat is None: 251 | return 0 252 | else: 253 | return self.unique_stats[stat] 254 | 255 | @property 256 | def statistic_name(self): 257 | # get the first unique statistic `stat`, e.g. the number of kills using a gadget 258 | stat = _first_key(self.unique_stats) 259 | if stat is not None: 260 | return stat.name 261 | else: 262 | return None 263 | 264 | 265 | def _first_key(d): 266 | """Gets the first inserted key of a dictionary. Returns None if empty""" 267 | return next(iter(d), None) 268 | -------------------------------------------------------------------------------- /r6sapi/definitions/seasons.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2016-2020 jackywathy 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | """ 10 | seasons_const = [ 11 | { 12 | "id": "6Og3BZ9ZhGnWRW2fQooGVD", 13 | "season_code": "Y1S1", 14 | "startDate": "2016-02-02T07:00:00.000Z", 15 | "season_ranks": [], 16 | "operation_name": "Operation Black Ice" 17 | }, 18 | { 19 | "id": "7ApYusrB1XQ7JUqi0gKask", 20 | "season_code": "Y1S2", 21 | "startDate": "2016-05-10T07:00:00.000Z", 22 | "season_ranks": [], 23 | "operation_name": "Operation Dust Line" 24 | }, 25 | { 26 | "id": "5gXNYW4aILJ2BCwGQVU5R", 27 | "season_code": "Y1S3", 28 | "startDate": "2016-08-02T07:00:00.000Z", 29 | "season_ranks": [], 30 | "operation_name": "Operation Skull Rain" 31 | }, 32 | { 33 | "id": "3yQu0JLznw23E1RUKnnlSF", 34 | "season_code": "Y1S4", 35 | "startDate": "2016-11-17T07:00:00.000Z", 36 | "season_ranks": [], 37 | "operation_name": "Operation Red Crow" 38 | }, 39 | { 40 | "id": "2vALfmPkwAJjVEqTzGm4VQ", 41 | "season_code": "Y2S1", 42 | "startDate": "2017-02-07T07:00:00.000Z", 43 | "season_ranks": [], 44 | "operation_name": "Operation Velvet Shell" 45 | }, 46 | { 47 | "id": "3fSQAoJYWfHHibuiYYfUs3", 48 | "season_code": "Y2S2", 49 | "startDate": "2017-06-07T07:00:00.000Z", 50 | "season_ranks": [], 51 | "operation_name": "Operation Operation Health" 52 | }, 53 | { 54 | "id": "5Pd0LEVFTsKfZVfAFf0yAm", 55 | "season_code": "Y2S3", 56 | "startDate": "2017-08-29T07:00:00.000Z", 57 | "season_ranks": [], 58 | "operation_name": "Operation Blood Orchid" 59 | }, 60 | { 61 | "id": "ySWfvtzFakBXwBwJL2YCP", 62 | "season_code": "Y2S4", 63 | "startDate": "2017-12-05T07:00:00.000Z", 64 | "season_ranks": [], 65 | "operation_name": "Operation White Noise" 66 | }, 67 | { 68 | "id": "3stC4aybcJTXFJ3ARWeJCe", 69 | "season_code": "Y3S1", 70 | "startDate": "2018-03-06T07:00:00.000Z", 71 | "season_ranks": [], 72 | "operation_name": "Operation Chimera" 73 | }, 74 | { 75 | "id": "RS4Myxuf0m0Oot9OszJNZ", 76 | "season_code": "Y3S2", 77 | "startDate": "2018-06-07T07:00:00.000Z", 78 | "season_ranks": [], 79 | "operation_name": "Operation Para Bellum" 80 | }, 81 | { 82 | "id": "fONvvcWq3e5jT8TY3PkfL", 83 | "season_code": "Y3S3", 84 | "startDate": "2018-09-04T07:00:00.000Z", 85 | "season_ranks": [], 86 | "operation_name": "Operation Grim Sky" 87 | }, 88 | { 89 | "id": "1CzDGQqeeZJZbOVrkw0NGG", 90 | "season_code": "Y3S4", 91 | "startDate": "2018-12-04T07:00:00.000Z", 92 | "season_ranks": [], 93 | "operation_name": "Operation Wind Bastion" 94 | }, 95 | { 96 | "id": "5PB4pyo7Fba2uv6L9iKFls", 97 | "season_code": "Y4S1", 98 | "startDate": "2019-03-06T07:00:00.000Z", 99 | "season_ranks": [], 100 | "operation_name": "Operation Burnt Horizon" 101 | }, 102 | { 103 | "id": "5NMaoOUQwYVDL4uoWwNMrt", 104 | "season_code": "Y4S2", 105 | "startDate": "2019-06-11T07:00:00.000Z", 106 | "season_ranks": [], 107 | "operation_name": "Operation Phantom Sight" 108 | }, 109 | { 110 | "id": "4A1c3NvSFrnT3cbBFnYFdn", 111 | "season_code": "Y4S3", 112 | "startDate": "2019-09-11T07:00:00.000Z", 113 | "season_ranks": [ 114 | { 115 | "name": "copper-5", 116 | "min_mmr": 1, 117 | "max_mmr": 1199 118 | }, 119 | { 120 | "name": "copper-4", 121 | "min_mmr": 1200, 122 | "max_mmr": 1299 123 | }, 124 | { 125 | "name": "copper-3", 126 | "min_mmr": 1300, 127 | "max_mmr": 1399 128 | }, 129 | { 130 | "name": "copper-2", 131 | "min_mmr": 1400, 132 | "max_mmr": 1499 133 | }, 134 | { 135 | "name": "copper-1", 136 | "min_mmr": 1500, 137 | "max_mmr": 1599 138 | }, 139 | { 140 | "name": "bronze-5", 141 | "min_mmr": 1600, 142 | "max_mmr": 1699 143 | }, 144 | { 145 | "name": "bronze-4", 146 | "min_mmr": 1700, 147 | "max_mmr": 1799 148 | }, 149 | { 150 | "name": "bronze-3", 151 | "min_mmr": 1800, 152 | "max_mmr": 1899 153 | }, 154 | { 155 | "name": "bronze-2", 156 | "min_mmr": 1900, 157 | "max_mmr": 1999 158 | }, 159 | { 160 | "name": "bronze-1", 161 | "min_mmr": 2000, 162 | "max_mmr": 2099 163 | }, 164 | { 165 | "name": "silver-5", 166 | "min_mmr": 2100, 167 | "max_mmr": 2199 168 | }, 169 | { 170 | "name": "silver-4", 171 | "min_mmr": 2200, 172 | "max_mmr": 2299 173 | }, 174 | { 175 | "name": "silver-3", 176 | "min_mmr": 2300, 177 | "max_mmr": 2399 178 | }, 179 | { 180 | "name": "silver-2", 181 | "min_mmr": 2400, 182 | "max_mmr": 2499 183 | }, 184 | { 185 | "name": "silver-1", 186 | "min_mmr": 2500, 187 | "max_mmr": 2599 188 | }, 189 | { 190 | "name": "gold-3", 191 | "min_mmr": 2600, 192 | "max_mmr": 2799 193 | }, 194 | { 195 | "name": "gold-2", 196 | "min_mmr": 2800, 197 | "max_mmr": 2999 198 | }, 199 | { 200 | "name": "gold-1", 201 | "min_mmr": 3000, 202 | "max_mmr": 3199 203 | }, 204 | { 205 | "name": "platinum-3", 206 | "min_mmr": 3200, 207 | "max_mmr": 3599 208 | }, 209 | { 210 | "name": "platinum-2", 211 | "min_mmr": 3600, 212 | "max_mmr": 3999 213 | }, 214 | { 215 | "name": "platinum-1", 216 | "min_mmr": 4000, 217 | "max_mmr": 4399 218 | }, 219 | { 220 | "name": "diamond", 221 | "min_mmr": 4400, 222 | "max_mmr": 4999 223 | }, 224 | { 225 | "name": "champions", 226 | "min_mmr": 5000, 227 | "max_mmr": 15000 228 | } 229 | ], 230 | "operation_name": "Operation Ember Rise" 231 | }, 232 | { 233 | "id": "5m1VNVEpIXWLf1NeF5ISNm", 234 | "season_code": "Y4S4", 235 | "startDate": "2019-12-03T07:00:00.000Z", 236 | "season_ranks": [ 237 | { 238 | "name": "copper-5", 239 | "min_mmr": 1, 240 | "max_mmr": 1199 241 | }, 242 | { 243 | "name": "copper-4", 244 | "min_mmr": 1200, 245 | "max_mmr": 1299 246 | }, 247 | { 248 | "name": "copper-3", 249 | "min_mmr": 1300, 250 | "max_mmr": 1399 251 | }, 252 | { 253 | "name": "copper-2", 254 | "min_mmr": 1400, 255 | "max_mmr": 1499 256 | }, 257 | { 258 | "name": "copper-1", 259 | "min_mmr": 1500, 260 | "max_mmr": 1599 261 | }, 262 | { 263 | "name": "bronze-5", 264 | "min_mmr": 1600, 265 | "max_mmr": 1699 266 | }, 267 | { 268 | "name": "bronze-4", 269 | "min_mmr": 1700, 270 | "max_mmr": 1799 271 | }, 272 | { 273 | "name": "bronze-3", 274 | "min_mmr": 1800, 275 | "max_mmr": 1899 276 | }, 277 | { 278 | "name": "bronze-2", 279 | "min_mmr": 1900, 280 | "max_mmr": 1999 281 | }, 282 | { 283 | "name": "bronze-1", 284 | "min_mmr": 2000, 285 | "max_mmr": 2099 286 | }, 287 | { 288 | "name": "silver-5", 289 | "min_mmr": 2100, 290 | "max_mmr": 2199 291 | }, 292 | { 293 | "name": "silver-4", 294 | "min_mmr": 2200, 295 | "max_mmr": 2299 296 | }, 297 | { 298 | "name": "silver-3", 299 | "min_mmr": 2300, 300 | "max_mmr": 2399 301 | }, 302 | { 303 | "name": "silver-2", 304 | "min_mmr": 2400, 305 | "max_mmr": 2499 306 | }, 307 | { 308 | "name": "silver-1", 309 | "min_mmr": 2500, 310 | "max_mmr": 2599 311 | }, 312 | { 313 | "name": "gold-3", 314 | "min_mmr": 2600, 315 | "max_mmr": 2799 316 | }, 317 | { 318 | "name": "gold-2", 319 | "min_mmr": 2800, 320 | "max_mmr": 2999 321 | }, 322 | { 323 | "name": "gold-1", 324 | "min_mmr": 3000, 325 | "max_mmr": 3199 326 | }, 327 | { 328 | "name": "platinum-3", 329 | "min_mmr": 3200, 330 | "max_mmr": 3599 331 | }, 332 | { 333 | "name": "platinum-2", 334 | "min_mmr": 3600, 335 | "max_mmr": 3999 336 | }, 337 | { 338 | "name": "platinum-1", 339 | "min_mmr": 4000, 340 | "max_mmr": 4399 341 | }, 342 | { 343 | "name": "diamond", 344 | "min_mmr": 4400, 345 | "max_mmr": 4999 346 | }, 347 | { 348 | "name": "champions", 349 | "min_mmr": 5000, 350 | "max_mmr": 15000 351 | } 352 | ], 353 | "operation_name": "Operation Shifting Tides" 354 | }, 355 | { 356 | "id": "J2MmVWkGRc9rMX1iZotjW", 357 | "season_code": "Y5S1", 358 | "startDate": "2020-03-10T07:00:00.000Z", 359 | "season_ranks": [ 360 | { 361 | "name": "copper-5", 362 | "min_mmr": 1, 363 | "max_mmr": 1199 364 | }, 365 | { 366 | "name": "copper-4", 367 | "min_mmr": 1200, 368 | "max_mmr": 1299 369 | }, 370 | { 371 | "name": "copper-3", 372 | "min_mmr": 1300, 373 | "max_mmr": 1399 374 | }, 375 | { 376 | "name": "copper-2", 377 | "min_mmr": 1400, 378 | "max_mmr": 1499 379 | }, 380 | { 381 | "name": "copper-1", 382 | "min_mmr": 1500, 383 | "max_mmr": 1599 384 | }, 385 | { 386 | "name": "bronze-5", 387 | "min_mmr": 1600, 388 | "max_mmr": 1699 389 | }, 390 | { 391 | "name": "bronze-4", 392 | "min_mmr": 1700, 393 | "max_mmr": 1799 394 | }, 395 | { 396 | "name": "bronze-3", 397 | "min_mmr": 1800, 398 | "max_mmr": 1899 399 | }, 400 | { 401 | "name": "bronze-2", 402 | "min_mmr": 1900, 403 | "max_mmr": 1999 404 | }, 405 | { 406 | "name": "bronze-1", 407 | "min_mmr": 2000, 408 | "max_mmr": 2099 409 | }, 410 | { 411 | "name": "silver-5", 412 | "min_mmr": 2100, 413 | "max_mmr": 2199 414 | }, 415 | { 416 | "name": "silver-4", 417 | "min_mmr": 2200, 418 | "max_mmr": 2299 419 | }, 420 | { 421 | "name": "silver-3", 422 | "min_mmr": 2300, 423 | "max_mmr": 2399 424 | }, 425 | { 426 | "name": "silver-2", 427 | "min_mmr": 2400, 428 | "max_mmr": 2499 429 | }, 430 | { 431 | "name": "silver-1", 432 | "min_mmr": 2500, 433 | "max_mmr": 2599 434 | }, 435 | { 436 | "name": "gold-3", 437 | "min_mmr": 2600, 438 | "max_mmr": 2799 439 | }, 440 | { 441 | "name": "gold-2", 442 | "min_mmr": 2800, 443 | "max_mmr": 2999 444 | }, 445 | { 446 | "name": "gold-1", 447 | "min_mmr": 3000, 448 | "max_mmr": 3199 449 | }, 450 | { 451 | "name": "platinum-3", 452 | "min_mmr": 3200, 453 | "max_mmr": 3599 454 | }, 455 | { 456 | "name": "platinum-2", 457 | "min_mmr": 3600, 458 | "max_mmr": 3999 459 | }, 460 | { 461 | "name": "platinum-1", 462 | "min_mmr": 4000, 463 | "max_mmr": 4399 464 | }, 465 | { 466 | "name": "diamond", 467 | "min_mmr": 4400, 468 | "max_mmr": 4999 469 | }, 470 | { 471 | "name": "champions", 472 | "min_mmr": 5000, 473 | "max_mmr": 15000 474 | } 475 | ], 476 | "operation_name": "Operation Void Edge" 477 | }, 478 | { 479 | "id": "1ajYMeLxlqBgCcQCVO6h7Z", 480 | "season_code": "Y5S2", 481 | "startDate": "2020-06-08T07:00:00.000Z", 482 | "season_ranks": [ 483 | { 484 | "name": "copper-5", 485 | "min_mmr": 1, 486 | "max_mmr": 1199 487 | }, 488 | { 489 | "name": "copper-4", 490 | "min_mmr": 1200, 491 | "max_mmr": 1299 492 | }, 493 | { 494 | "name": "copper-3", 495 | "min_mmr": 1300, 496 | "max_mmr": 1399 497 | }, 498 | { 499 | "name": "copper-2", 500 | "min_mmr": 1400, 501 | "max_mmr": 1499 502 | }, 503 | { 504 | "name": "copper-1", 505 | "min_mmr": 1500, 506 | "max_mmr": 1599 507 | }, 508 | { 509 | "name": "bronze-5", 510 | "min_mmr": 1600, 511 | "max_mmr": 1699 512 | }, 513 | { 514 | "name": "bronze-4", 515 | "min_mmr": 1700, 516 | "max_mmr": 1799 517 | }, 518 | { 519 | "name": "bronze-3", 520 | "min_mmr": 1800, 521 | "max_mmr": 1899 522 | }, 523 | { 524 | "name": "bronze-2", 525 | "min_mmr": 1900, 526 | "max_mmr": 1999 527 | }, 528 | { 529 | "name": "bronze-1", 530 | "min_mmr": 2000, 531 | "max_mmr": 2099 532 | }, 533 | { 534 | "name": "silver-5", 535 | "min_mmr": 2100, 536 | "max_mmr": 2199 537 | }, 538 | { 539 | "name": "silver-4", 540 | "min_mmr": 2200, 541 | "max_mmr": 2299 542 | }, 543 | { 544 | "name": "silver-3", 545 | "min_mmr": 2300, 546 | "max_mmr": 2399 547 | }, 548 | { 549 | "name": "silver-2", 550 | "min_mmr": 2400, 551 | "max_mmr": 2499 552 | }, 553 | { 554 | "name": "silver-1", 555 | "min_mmr": 2500, 556 | "max_mmr": 2599 557 | }, 558 | { 559 | "name": "gold-3", 560 | "min_mmr": 2600, 561 | "max_mmr": 2799 562 | }, 563 | { 564 | "name": "gold-2", 565 | "min_mmr": 2800, 566 | "max_mmr": 2999 567 | }, 568 | { 569 | "name": "gold-1", 570 | "min_mmr": 3000, 571 | "max_mmr": 3199 572 | }, 573 | { 574 | "name": "platinum-3", 575 | "min_mmr": 3200, 576 | "max_mmr": 3599 577 | }, 578 | { 579 | "name": "platinum-2", 580 | "min_mmr": 3600, 581 | "max_mmr": 3999 582 | }, 583 | { 584 | "name": "platinum-1", 585 | "min_mmr": 4000, 586 | "max_mmr": 4399 587 | }, 588 | { 589 | "name": "diamond", 590 | "min_mmr": 4400, 591 | "max_mmr": 4999 592 | }, 593 | { 594 | "name": "champions", 595 | "min_mmr": 5000, 596 | "max_mmr": 15000 597 | } 598 | ], 599 | "operation_name": "Operation Steel Wave" 600 | }, 601 | { 602 | "id": "408eJ1avRd3AYdmO333fkb", 603 | "season_code": "Y5S3", 604 | "startDate": "2020-09-10T07:00:00.000Z", 605 | "season_ranks": [ 606 | { 607 | "name": "copper-5", 608 | "min_mmr": 1, 609 | "max_mmr": 1199 610 | }, 611 | { 612 | "name": "copper-4", 613 | "min_mmr": 1200, 614 | "max_mmr": 1299 615 | }, 616 | { 617 | "name": "copper-3", 618 | "min_mmr": 1300, 619 | "max_mmr": 1399 620 | }, 621 | { 622 | "name": "copper-2", 623 | "min_mmr": 1400, 624 | "max_mmr": 1499 625 | }, 626 | { 627 | "name": "copper-1", 628 | "min_mmr": 1500, 629 | "max_mmr": 1599 630 | }, 631 | { 632 | "name": "bronze-5", 633 | "min_mmr": 1600, 634 | "max_mmr": 1699 635 | }, 636 | { 637 | "name": "bronze-4", 638 | "min_mmr": 1700, 639 | "max_mmr": 1799 640 | }, 641 | { 642 | "name": "bronze-3", 643 | "min_mmr": 1800, 644 | "max_mmr": 1899 645 | }, 646 | { 647 | "name": "bronze-2", 648 | "min_mmr": 1900, 649 | "max_mmr": 1999 650 | }, 651 | { 652 | "name": "bronze-1", 653 | "min_mmr": 2000, 654 | "max_mmr": 2099 655 | }, 656 | { 657 | "name": "silver-5", 658 | "min_mmr": 2100, 659 | "max_mmr": 2199 660 | }, 661 | { 662 | "name": "silver-4", 663 | "min_mmr": 2200, 664 | "max_mmr": 2299 665 | }, 666 | { 667 | "name": "silver-3", 668 | "min_mmr": 2300, 669 | "max_mmr": 2399 670 | }, 671 | { 672 | "name": "silver-2", 673 | "min_mmr": 2400, 674 | "max_mmr": 2499 675 | }, 676 | { 677 | "name": "silver-1", 678 | "min_mmr": 2500, 679 | "max_mmr": 2599 680 | }, 681 | { 682 | "name": "gold-3", 683 | "min_mmr": 2600, 684 | "max_mmr": 2799 685 | }, 686 | { 687 | "name": "gold-2", 688 | "min_mmr": 2800, 689 | "max_mmr": 2999 690 | }, 691 | { 692 | "name": "gold-1", 693 | "min_mmr": 3000, 694 | "max_mmr": 3199 695 | }, 696 | { 697 | "name": "platinum-3", 698 | "min_mmr": 3200, 699 | "max_mmr": 3599 700 | }, 701 | { 702 | "name": "platinum-2", 703 | "min_mmr": 3600, 704 | "max_mmr": 3999 705 | }, 706 | { 707 | "name": "platinum-1", 708 | "min_mmr": 4000, 709 | "max_mmr": 4399 710 | }, 711 | { 712 | "name": "diamond", 713 | "min_mmr": 4400, 714 | "max_mmr": 4999 715 | }, 716 | { 717 | "name": "champions", 718 | "min_mmr": 5000, 719 | "max_mmr": 15000 720 | } 721 | ], 722 | "operation_name": "Operation Shadow Legacy" 723 | }, 724 | { 725 | "id": "281ZRQbACsvzB6TE1cWAbm", 726 | "season_code": "Y5S4", 727 | "startDate": "2020-12-01T07:00:00.000Z", 728 | "season_ranks": [ 729 | { 730 | "name": "copper-5", 731 | "min_mmr": 1, 732 | "max_mmr": 1199 733 | }, 734 | { 735 | "name": "copper-4", 736 | "min_mmr": 1200, 737 | "max_mmr": 1299 738 | }, 739 | { 740 | "name": "copper-3", 741 | "min_mmr": 1300, 742 | "max_mmr": 1399 743 | }, 744 | { 745 | "name": "copper-2", 746 | "min_mmr": 1400, 747 | "max_mmr": 1499 748 | }, 749 | { 750 | "name": "copper-1", 751 | "min_mmr": 1500, 752 | "max_mmr": 1599 753 | }, 754 | { 755 | "name": "bronze-5", 756 | "min_mmr": 1600, 757 | "max_mmr": 1699 758 | }, 759 | { 760 | "name": "bronze-4", 761 | "min_mmr": 1700, 762 | "max_mmr": 1799 763 | }, 764 | { 765 | "name": "bronze-3", 766 | "min_mmr": 1800, 767 | "max_mmr": 1899 768 | }, 769 | { 770 | "name": "bronze-2", 771 | "min_mmr": 1900, 772 | "max_mmr": 1999 773 | }, 774 | { 775 | "name": "bronze-1", 776 | "min_mmr": 2000, 777 | "max_mmr": 2099 778 | }, 779 | { 780 | "name": "silver-5", 781 | "min_mmr": 2100, 782 | "max_mmr": 2199 783 | }, 784 | { 785 | "name": "silver-4", 786 | "min_mmr": 2200, 787 | "max_mmr": 2299 788 | }, 789 | { 790 | "name": "silver-3", 791 | "min_mmr": 2300, 792 | "max_mmr": 2399 793 | }, 794 | { 795 | "name": "silver-2", 796 | "min_mmr": 2400, 797 | "max_mmr": 2499 798 | }, 799 | { 800 | "name": "silver-1", 801 | "min_mmr": 2500, 802 | "max_mmr": 2599 803 | }, 804 | { 805 | "name": "gold-3", 806 | "min_mmr": 2600, 807 | "max_mmr": 2799 808 | }, 809 | { 810 | "name": "gold-2", 811 | "min_mmr": 2800, 812 | "max_mmr": 2999 813 | }, 814 | { 815 | "name": "gold-1", 816 | "min_mmr": 3000, 817 | "max_mmr": 3199 818 | }, 819 | { 820 | "name": "platinum-3", 821 | "min_mmr": 3200, 822 | "max_mmr": 3599 823 | }, 824 | { 825 | "name": "platinum-2", 826 | "min_mmr": 3600, 827 | "max_mmr": 3999 828 | }, 829 | { 830 | "name": "platinum-1", 831 | "min_mmr": 4000, 832 | "max_mmr": 4399 833 | }, 834 | { 835 | "name": "diamond", 836 | "min_mmr": 4400, 837 | "max_mmr": 4999 838 | }, 839 | { 840 | "name": "champions", 841 | "min_mmr": 5000, 842 | "max_mmr": 15000 843 | } 844 | ], 845 | "operation_name": "Operation Neon Dawn" 846 | } 847 | ] 848 | -------------------------------------------------------------------------------- /r6sapi/players.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2016-2019 billyoyo 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | """ 10 | from collections import OrderedDict 11 | 12 | import asyncio 13 | import logging 14 | 15 | from .definitions.models import OperatorInfo 16 | from .definitions import operators, seasons 17 | 18 | from .exceptions import InvalidRequest 19 | from .platforms import PlatformURLNames 20 | from .weapons import * 21 | from .gamemodes import * 22 | from .gamequeues import * 23 | from .operators import * 24 | from .ranks import * 25 | 26 | 27 | class PlayerUrlTemplates: 28 | """ Private class, base API URLs """ 29 | 30 | FETCH_STATISTIC = "https://public-ubiservices.ubi.com/v1/spaces/%s/sandboxes/%s/playerstats2/statistics?populations=%s&statistics=%s" 31 | LOAD_LEVEL = "https://public-ubiservices.ubi.com/v1/spaces/%s/sandboxes/%s/r6playerprofile/playerprofile/progressions?profile_ids=%s" 32 | LOAD_RANK = "https://public-ubiservices.ubi.com/v1/spaces/%s/sandboxes/%s/r6karma/players?board_id=pvp_ranked&profile_ids=%s®ion_id=%s&season_id=%s" 33 | LOAD_OPERATOR = "https://public-ubiservices.ubi.com/v1/spaces/%s/sandboxes/%s/playerstats2/statistics?populations=%s&statistics=%s" 34 | LOAD_WEAPON = "https://public-ubiservices.ubi.com/v1/spaces/%s/sandboxes/%s/playerstats2/statistics?populations=%s&statistics=weapontypepvp_kills,weapontypepvp_headshot,weapontypepvp_bulletfired,weapontypepvp_bullethit" 35 | 36 | 37 | class PlayerUrlBuilder: 38 | """ Private class, creates URLs for different types of requests """ 39 | 40 | def __init__(self, spaceid, platform_url, player_ids): 41 | self.spaceid = spaceid 42 | self.platform_url = platform_url 43 | self.player_ids = player_ids 44 | 45 | if isinstance(player_ids, list) or isinstance(player_ids, tuple): 46 | player_ids = ",".join(player_ids) 47 | 48 | def fetch_statistic_url(self, statistics): 49 | return PlayerUrlTemplates.FETCH_STATISTIC % (self.spaceid, self.platform_url, self.player_ids, ",".join(statistics)) 50 | 51 | def load_level_url(self): 52 | return PlayerUrlTemplates.LOAD_LEVEL % (self.spaceid, self.platform_url, self.player_ids) 53 | 54 | def load_rank_url(self, region, season): 55 | return PlayerUrlTemplates.LOAD_RANK % (self.spaceid, self.platform_url, self.player_ids, region, season) 56 | 57 | def load_operator_url(self, statistics): 58 | return PlayerUrlTemplates.LOAD_OPERATOR % (self.spaceid, self.platform_url, self.player_ids, statistics) 59 | 60 | def load_weapon_url(self): 61 | return PlayerUrlTemplates.LOAD_WEAPON % (self.spaceid, self.platform_url, self.player_ids) 62 | 63 | 64 | class PlayerBatch: 65 | """ Accumulates requests for multiple players' stats in to a single request, saving time. 66 | 67 | Acts as a proxy for any asynchronous method in :class:`Player`. The response of the method will be a dictionary of 68 | the responses from each player, with the player ids as keys. 69 | 70 | This class is also an iterable, and iterates over the :class:`Player` objects contained in the batch. 71 | Individual players in the batch can be accessed via their ID using an item accessor (player_batch[player.id]) 72 | 73 | Parameters 74 | ---------- 75 | players : list[:class:`Player`] 76 | the list of players in the batch """ 77 | 78 | def __init__(self, players): 79 | self.players = players 80 | self.player_ids = [player_id for player_id in players] 81 | self._player_objs = [players[player_id] for player_id in players] 82 | 83 | if len(players) == 0: 84 | raise ValueError("batch must contain at least one player") 85 | 86 | def __iter__(self): 87 | return iter(self._player_objs) 88 | 89 | def __getitem__(self, name): 90 | return self.players[name] 91 | 92 | def __getattr__(self, name): 93 | root_player = self.players[self.player_ids[0]] 94 | root_method = getattr(root_player, name) 95 | 96 | @asyncio.coroutine 97 | def _proxy(*args, **kwargs): 98 | results = {} 99 | 100 | # temporarily override url builder so we get data for all players 101 | root_player.url_builder.player_ids = ",".join(self.player_ids) 102 | 103 | root_result = yield from root_method(*args, **kwargs) 104 | results[root_player.id] = root_result 105 | 106 | data = root_player._last_data 107 | kwargs["data"] = data 108 | 109 | for player_id in self.players: 110 | if player_id != root_player.id: 111 | results[player_id] = yield from getattr(self.players[player_id], name)(*args, **kwargs) 112 | 113 | # reset root player url builder to default state 114 | root_player.url_builder.player_ids = root_player.id 115 | 116 | return results 117 | 118 | return _proxy 119 | 120 | 121 | 122 | class Player: 123 | """Contains information about a specific player 124 | 125 | Attributes 126 | ---------- 127 | auth : :class:`Auth` 128 | the auth object used to find this player 129 | id : str 130 | the players profile id 131 | userid : str 132 | the players user id 133 | platform : str 134 | the platform this player is on 135 | platform_url : str 136 | the URL name for this platform (used internally) 137 | id_on_platform : str 138 | the players ID on the platform 139 | name : str 140 | the players name on the platform 141 | url : str 142 | a link to the players profile 143 | icon_url : str 144 | a link to the players avatar 145 | xp : int 146 | the amount of xp the player has, must call check_level or load_level first 147 | level : int 148 | the level of the player, must call check_level or load_level first 149 | ranks : dict 150 | dict containing already found ranks ("region_name:season": :class:`Rank`) 151 | operators : dict 152 | dict containing already found operators (operator_name: :class:`Operator`) 153 | gamemodes : dict 154 | dict containing already found gamemodes (gamemode_id: :class:`Gamemode`) 155 | weapons : dict 156 | dict containing already found weapons (weapon_id: :class:`Weapon`) 157 | casual : :class:`GameQueue` 158 | stats for the casual queue, must call load_queues or check_queues first 159 | ranked : :class:`GameQueue` 160 | stats for the ranked queue, must call load_queues or check_queues first 161 | deaths : int 162 | the number of deaths the player has (must call load_general or check_general first) 163 | kills : int 164 | the number of kills the player has (must call load_general or check_general first) 165 | kill_assists : int 166 | the number of kill assists the player has (must call load_general or check_general first) 167 | penetration_kills : int 168 | the number of penetration kills the player has (must call load_general or check_general first) 169 | melee_kills : int 170 | the number of melee kills the player has (must call load_general or check_general first) 171 | revives : int 172 | the number of revives the player has (must call load_general or check_general first) 173 | matches_won : int 174 | the number of matches the player has won (must call load_general or check_general first) 175 | matches_lost : int 176 | the number of matches the player has lost (must call load_general or check_general first) 177 | matches_played : int 178 | the number of matches the player has played (must call load_general or check_general first) 179 | time_played : int 180 | the amount of time in seconds the player has played for (must call load_general or check_general first) 181 | bullets_fired : int 182 | the amount of bullets the player has fired (must call load_general or check_general first) 183 | bullets_hit : int 184 | the amount of bullets the player has hit (must call load_general or check_general first) 185 | headshots : int 186 | the amount of headshots the player has hit (must call load_general or check_general first) 187 | terrorist_hunt : :class:`GameQueue` 188 | contains all of the above state (from deaths to headshots) inside a gamequeue object. 189 | """ 190 | 191 | def __init__(self, auth, data): 192 | self.auth = auth 193 | 194 | self.id = data.get("profileId") 195 | self.userid = data.get("userId") 196 | self.platform = data.get("platformType") 197 | self.platform_url = PlatformURLNames[self.platform] 198 | self.id_on_platform = data.get("idOnPlatform") 199 | self.name = data.get("nameOnPlatform") 200 | self.url_builder = PlayerUrlBuilder(self.spaceid, self.platform_url, self.id) 201 | 202 | self.url = "https://game-rainbow6.ubi.com/en-us/%s/player-statistics/%s/multiplayer" % (self.platform, self.id) 203 | self.icon_url = "https://ubisoft-avatars.akamaized.net/%s/default_146_146.png" % (self.id) 204 | 205 | self.ranks = {} 206 | self.operators = {} 207 | self.gamemodes = {} 208 | self.weapons = [] 209 | 210 | self.casual = None 211 | self.ranked = None 212 | self.terrorist_hunt = None 213 | 214 | self._last_data = None 215 | 216 | @property 217 | def spaceid(self): 218 | return self.auth.spaceids[self.platform] 219 | 220 | @asyncio.coroutine 221 | def _fetch_statistics(self, *statistics, data=None): 222 | if data is None: 223 | data = yield from self.auth.get(self.url_builder.fetch_statistic_url(statistics)) 224 | self._last_data = data 225 | 226 | if "results" not in data or self.id not in data["results"]: 227 | raise InvalidRequest("Missing results key in returned JSON object %s" % str(data)) 228 | 229 | data = data["results"][self.id] 230 | stats = {} 231 | 232 | for x in data: 233 | statistic = x.split(":")[0] 234 | if statistic in statistics: 235 | stats[statistic] = data[x] 236 | 237 | return stats 238 | 239 | @asyncio.coroutine 240 | def load_level(self, data=None): 241 | """|coro| 242 | 243 | Load the players XP and level""" 244 | if data is None: 245 | data = yield from self.auth.get(self.url_builder.load_level_url()) 246 | self._last_data = data 247 | 248 | if "player_profiles" in data and len(data["player_profiles"]) > 0: 249 | self.xp = data["player_profiles"][0].get("xp", 0) 250 | self.level = data["player_profiles"][0].get("level", 0) 251 | else: 252 | raise InvalidRequest("Missing key player_profiles in returned JSON object %s" % str(data)) 253 | 254 | @asyncio.coroutine 255 | def check_level(self): 256 | """|coro| 257 | 258 | Check the players XP and level, only loading it if it hasn't been loaded yet""" 259 | if not hasattr(self, "level"): 260 | yield from self.load_level() 261 | 262 | @asyncio.coroutine 263 | def load_rank(self, region, season=-1, data=None): 264 | """|coro| 265 | Loads the players rank for this region and season 266 | 267 | Parameters 268 | ---------- 269 | region : str 270 | the name of the region you want to get the rank for 271 | season : Optional[int] 272 | the season you want to get the rank for (defaults to -1, latest season) 273 | 274 | Returns 275 | ------- 276 | :class:`Rank` 277 | the players rank for this region and season""" 278 | if data is None: 279 | data = yield from self.auth.get(self.url_builder.load_rank_url(region, season)) 280 | self._last_data = data 281 | 282 | queried_season = seasons[season] 283 | rank_definitions = queried_season.season_ranks 284 | 285 | if "players" in data and self.id in data["players"]: 286 | regionkey = "%s:%s" % (region, season) 287 | self.ranks[regionkey] = Rank(data["players"][self.id], rank_definitions) 288 | return self.ranks[regionkey] 289 | else: 290 | raise InvalidRequest("Missing players key in returned JSON object %s" % str(data)) 291 | 292 | @asyncio.coroutine 293 | def get_rank(self, region, season=-1, data=None): 294 | """|coro| 295 | 296 | Checks the players rank for this region, only loading it if it hasn't already been found 297 | 298 | Parameters 299 | ---------- 300 | region : str 301 | the name of the region you want to get the rank for 302 | season : Optional[int] 303 | the season you want to get the rank for (defaults to -1, latest season) 304 | 305 | Returns 306 | ------- 307 | :class:`Rank` 308 | the players rank for this region and season""" 309 | cache_key = "%s:%s" % (region, season) 310 | if cache_key in self.ranks: 311 | return self.ranks[cache_key] 312 | 313 | result = yield from self.load_rank(region, season, data=data) 314 | return result 315 | 316 | @staticmethod 317 | def _process_basic_data(data): 318 | """ 319 | Filters out the basic data like kills, deaths etc that are common to all operators 320 | and processes them, removing the operator:infinite postfix and prefix, returning 321 | it as a new dictionary 322 | 323 | Note that the current implementation doesn't remove extraneous items 324 | """ 325 | return {x.split(":")[0].split("_", maxsplit=1)[1]: data[x] for x in data if x is not None} 326 | 327 | @staticmethod 328 | def _process_unique_data(data, operator_info): 329 | """ 330 | Filters out the unique attributes, based on the attributes of the operator_info passed. 331 | Returns them as a ordered dictionary of :class:`UniqueOperatorStat` to the value of the stat. 332 | The order is preserved to allow the 'main' attribute to always be inserted first and used for 333 | the operator's `statistic` and `statistic_name` values 334 | """ 335 | unique_data = OrderedDict() 336 | for ability in operator_info.unique_abilities: 337 | # try to match each ability to the data returned from the API 338 | # currently hard-coded to only return PVP stats 339 | match = "{stat_name}:{index}:infinite".format(stat_name=ability.pvp_stat_name, index=operator_info.index) 340 | if match in data: 341 | unique_data[ability] = data[match] 342 | else: 343 | unique_data[ability] = 0 # the stupid API just doesnt return anything if we have zero of that stat 344 | if "aruni" in match: 345 | logging.warning("aruni unique stat may not work. I haven't been able to find the correct API name " 346 | "so 0 will be returned. Use aruni's unique stat with caution") 347 | 348 | return unique_data 349 | 350 | 351 | @asyncio.coroutine 352 | def load_all_operators(self, data=None): 353 | """|coro| 354 | 355 | Loads the player stats for all operators 356 | 357 | Returns 358 | ------- 359 | dict[:class:`Operator`] 360 | the dictionary of all operators found""" 361 | # ask the api for all the basic stat names WITHOUT a postfix to ask for all (I assume) 362 | statistics = list(OperatorUrlStatisticNames) 363 | 364 | # also add in all the unique 365 | for operator_info in operators.get_all(): 366 | for ability in operator_info.unique_abilities: 367 | statistics.append("{stat_name}:{index}:infinite".format( 368 | stat_name=ability.pvp_stat_name, index=operator_info.index) 369 | ) 370 | 371 | statistics = ",".join(statistics) 372 | 373 | if data is None: 374 | data = yield from self.auth.get(self.url_builder.load_operator_url(statistics)) 375 | self._last_data = data 376 | 377 | if "results" not in data or self.id not in data["results"]: 378 | raise InvalidRequest("Missing results key in returned JSON object %s" % str(data)) 379 | 380 | data = data["results"][self.id] 381 | 382 | for operator_info in operators.get_all(): 383 | base_data = self._process_basic_data(data) 384 | unique_data = self._process_unique_data(data, operator_info) 385 | 386 | self.operators[operator_info.name.lower()] = Operator(operator_info.name.lower(), base_data, unique_data) 387 | 388 | return self.operators 389 | 390 | @asyncio.coroutine 391 | def get_all_operators(self, data=None): 392 | """|coro| 393 | 394 | Checks the player stats for all operators, loading them all again if any aren't found 395 | This is significantly more efficient than calling get_operator for every operator name. 396 | 397 | Returns 398 | ------- 399 | dict[:class:`Operator`] 400 | the dictionary of all operators found""" 401 | if len(self.operators) >= len(OperatorStatisticNames): 402 | return self.operators 403 | 404 | result = yield from self.load_all_operators(data=data) 405 | return result 406 | 407 | @asyncio.coroutine 408 | def load_operator(self, operator, data=None): 409 | """|coro| 410 | 411 | Loads the players stats for the operator 412 | 413 | Parameters 414 | ---------- 415 | operator : str 416 | the name of the operator 417 | 418 | Returns 419 | ------- 420 | :class:`Operator` 421 | the operator object found""" 422 | operator = operator.lower() 423 | 424 | # check if operator occurs in the definitions 425 | op = operators.from_name(operator) 426 | if op is None: 427 | raise ValueError("invalid operator %s" % operator) 428 | 429 | statistics = [] 430 | for stat_name in OperatorUrlStatisticNames: 431 | # the statistic key is the stat name e.g. `operatorpvp_kills` + an "operator index" 432 | # to filter the result + ":infinite" 433 | # the resulting key will look something like `operatorpvp_kills:1:2:infinite` 434 | # where :1:2: varies depending on the operator 435 | statistics.append("{stat_name}:{index}:infinite".format(stat_name=stat_name, index=op.index)) 436 | 437 | # now get the operator unique stats 438 | for ability in op.unique_abilities: 439 | statistics.append("{stat_name}:{index}:infinite".format(stat_name=ability.pvp_stat_name, index=op.index)) 440 | 441 | if data is None: 442 | # join the statistic name strings to build the url 443 | statistics = ",".join(statistics) 444 | data = yield from self.auth.get(self.url_builder.load_operator_url(statistics)) 445 | self._last_data = data 446 | 447 | if "results" not in data or self.id not in data["results"]: 448 | raise InvalidRequest("Missing results key in returned JSON object %s" % str(data)) 449 | 450 | data = data["results"][self.id] 451 | 452 | base_data = self._process_basic_data(data) 453 | unique_data = self._process_unique_data(data, op) 454 | 455 | oper = Operator(operator, base_data, unique_data) 456 | self.operators[operator] = oper 457 | return oper 458 | 459 | @asyncio.coroutine 460 | def get_operator(self, operator, data=None): 461 | """|coro| 462 | 463 | Checks the players stats for this operator, only loading them if they haven't already been found 464 | 465 | Parameters 466 | ---------- 467 | operator : str 468 | the name of the operator 469 | 470 | Returns 471 | ------- 472 | :class:`Operator` 473 | the operator object found""" 474 | if operator in self.operators: 475 | return self.operators[operator] 476 | 477 | result = yield from self.load_operator(operator, data=data) 478 | return result 479 | 480 | @asyncio.coroutine 481 | def load_weapons(self, data=None): 482 | """|coro| 483 | 484 | Load the players weapon stats 485 | 486 | Returns 487 | ------- 488 | list[:class:`Weapon`] 489 | list of all the weapon objects found""" 490 | if data is None: 491 | data = yield from self.auth.get(self.url_builder.load_weapon_url()) 492 | self._last_data = data 493 | 494 | if not "results" in data or not self.id in data["results"]: 495 | raise InvalidRequest("Missing key results in returned JSON object %s" % str(data)) 496 | 497 | data = data["results"][self.id] 498 | self.weapons = [Weapon(i, data) for i in range(7)] 499 | 500 | return self.weapons 501 | 502 | @asyncio.coroutine 503 | def check_weapons(self, data=None): 504 | """|coro| 505 | 506 | Check the players weapon stats, only loading them if they haven't already been found 507 | 508 | Returns 509 | ------- 510 | list[:class:`Weapon`] 511 | list of all the weapon objects found""" 512 | if len(self.weapons) == 0: 513 | yield from self.load_weapons(data=data) 514 | return self.weapons 515 | 516 | @asyncio.coroutine 517 | def load_gamemodes(self, data=None): 518 | """|coro| 519 | 520 | Loads the players gamemode stats 521 | 522 | Returns 523 | ------- 524 | dict 525 | dict of all the gamemodes found (gamemode_name: :class:`Gamemode`)""" 526 | 527 | stats = yield from self._fetch_statistics("secureareapvp_matchwon", "secureareapvp_matchlost", "secureareapvp_matchplayed", 528 | "secureareapvp_bestscore", "rescuehostagepvp_matchwon", "rescuehostagepvp_matchlost", 529 | "rescuehostagepvp_matchplayed", "rescuehostagepvp_bestscore", "plantbombpvp_matchwon", 530 | "plantbombpvp_matchlost", "plantbombpvp_matchplayed", "plantbombpvp_bestscore", 531 | "generalpvp_servershacked", "generalpvp_serverdefender", "generalpvp_serveraggression", 532 | "generalpvp_hostagerescue", "generalpvp_hostagedefense", data=data) 533 | 534 | self.gamemodes = {x: Gamemode(x, stats) for x in GamemodeNames} 535 | 536 | return self.gamemodes 537 | 538 | @asyncio.coroutine 539 | def check_gamemodes(self, data=None): 540 | """|coro| 541 | 542 | Checks the players gamemode stats, only loading them if they haven't already been found 543 | 544 | Returns 545 | ------- 546 | dict 547 | dict of all the gamemodes found (gamemode_name: :class:`Gamemode`)""" 548 | if len(self.gamemodes) == 0: 549 | yield from self.load_gamemodes(data=data) 550 | return self.gamemodes 551 | 552 | @asyncio.coroutine 553 | def load_general(self, data=None): 554 | """|coro| 555 | 556 | Loads the players general stats""" 557 | 558 | stats = yield from self._fetch_statistics("generalpvp_timeplayed", "generalpvp_matchplayed", "generalpvp_matchwon", 559 | "generalpvp_matchlost", "generalpvp_kills", "generalpvp_death", 560 | "generalpvp_bullethit", "generalpvp_bulletfired", "generalpvp_killassists", 561 | "generalpvp_revive", "generalpvp_headshot", "generalpvp_penetrationkills", 562 | "generalpvp_meleekills", "generalpvp_dbnoassists", "generalpvp_suicide", 563 | "generalpvp_barricadedeployed", "generalpvp_reinforcementdeploy", "generalpvp_totalxp", 564 | "generalpvp_rappelbreach", "generalpvp_distancetravelled", "generalpvp_revivedenied", 565 | "generalpvp_dbno", "generalpvp_gadgetdestroy", "generalpvp_blindkills", data=data) 566 | 567 | statname = "generalpvp_" 568 | self.deaths = stats.get(statname + "death", 0) 569 | self.penetration_kills = stats.get(statname + "penetrationkills", 0) 570 | self.matches_won = stats.get(statname + "matchwon", 0) 571 | self.bullets_hit = stats.get(statname + "bullethit", 0) 572 | self.melee_kills = stats.get(statname + "meleekills", 0) 573 | self.bullets_fired = stats.get(statname + "bulletfired", 0) 574 | self.matches_played = stats.get(statname + "matchplayed", 0) 575 | self.kill_assists = stats.get(statname + "killassists", 0) 576 | self.time_played = stats.get(statname + "timeplayed", 0) 577 | self.revives = stats.get(statname + "revive", 0) 578 | self.kills = stats.get(statname + "kills", 0) 579 | self.headshots = stats.get(statname + "headshot", 0) 580 | self.matches_lost = stats.get(statname + "matchlost", 0) 581 | self.dbno_assists = stats.get(statname + "dbnoassists", 0) 582 | self.suicides = stats.get(statname + "suicide", 0) 583 | self.barricades_deployed = stats.get(statname + "barricadedeployed", 0) 584 | self.reinforcements_deployed = stats.get(statname + "reinforcementdeploy", 0) 585 | self.total_xp = stats.get(statname + "totalxp", 0) 586 | self.rappel_breaches = stats.get(statname + "rappelbreach", 0) 587 | self.distance_travelled = stats.get(statname + "distancetravelled", 0) 588 | self.revives_denied = stats.get(statname + "revivedenied", 0) 589 | self.dbnos = stats.get(statname + "dbno", 0) 590 | self.gadgets_destroyed = stats.get(statname + "gadgetdestroy", 0) 591 | self.blind_kills = stats.get(statname + "blindkills") 592 | 593 | 594 | @asyncio.coroutine 595 | def check_general(self, data=None): 596 | """|coro| 597 | 598 | Checks the players general stats, only loading them if they haven't already been found""" 599 | if not hasattr(self, "kills"): 600 | yield from self.load_general(data=data) 601 | 602 | @asyncio.coroutine 603 | def load_queues(self, data=None): 604 | """|coro| 605 | 606 | Loads the players game queues""" 607 | 608 | stats = yield from self._fetch_statistics("casualpvp_matchwon", "casualpvp_matchlost", "casualpvp_timeplayed", 609 | "casualpvp_matchplayed", "casualpvp_kills", "casualpvp_death", 610 | "rankedpvp_matchwon", "rankedpvp_matchlost", "rankedpvp_timeplayed", 611 | "rankedpvp_matchplayed", "rankedpvp_kills", "rankedpvp_death", data=data) 612 | 613 | self.ranked = GameQueue("ranked", stats) 614 | self.casual = GameQueue("casual", stats) 615 | 616 | 617 | @asyncio.coroutine 618 | def check_queues(self, data=None): 619 | """|coro| 620 | 621 | Checks the players game queues, only loading them if they haven't already been found""" 622 | if self.casual is None: 623 | yield from self.load_queues(data=data) 624 | 625 | @asyncio.coroutine 626 | def load_terrohunt(self, data=None): 627 | """|coro| 628 | 629 | Loads the player's general stats for terrorist hunt""" 630 | stats = yield from self._fetch_statistics("generalpve_dbnoassists", "generalpve_death", "generalpve_revive", 631 | "generalpve_matchwon", "generalpve_suicide", "generalpve_servershacked", 632 | "generalpve_serverdefender", "generalpve_barricadedeployed", "generalpve_reinforcementdeploy", 633 | "generalpve_kills", "generalpve_hostagedefense", "generalpve_bulletfired", 634 | "generalpve_matchlost", "generalpve_killassists", "generalpve_totalxp", 635 | "generalpve_hostagerescue", "generalpve_penetrationkills", "generalpve_meleekills", 636 | "generalpve_rappelbreach", "generalpve_distancetravelled", "generalpve_matchplayed", 637 | "generalpve_serveraggression", "generalpve_timeplayed", "generalpve_revivedenied", 638 | "generalpve_dbno", "generalpve_bullethit", "generalpve_blindkills", "generalpve_headshot", 639 | "generalpve_gadgetdestroy", "generalpve_accuracy", data=data) 640 | 641 | self.terrorist_hunt = GameQueue("terrohunt") 642 | 643 | statname = "generalpve_" 644 | self.terrorist_hunt.deaths = stats.get(statname + "death", 0) 645 | self.terrorist_hunt.penetration_kills = stats.get(statname + "penetrationkills", 0) 646 | self.terrorist_hunt.matches_won = stats.get(statname + "matchwon", 0) 647 | self.terrorist_hunt.bullets_hit = stats.get(statname + "bullethit", 0) 648 | self.terrorist_hunt.melee_kills = stats.get(statname + "meleekills", 0) 649 | self.terrorist_hunt.bullets_fired = stats.get(statname + "bulletfired", 0) 650 | self.terrorist_hunt.matches_played = stats.get(statname + "matchplayed", 0) 651 | self.terrorist_hunt.kill_assists = stats.get(statname + "killassists", 0) 652 | self.terrorist_hunt.time_played = stats.get(statname + "timeplayed", 0) 653 | self.terrorist_hunt.revives = stats.get(statname + "revive", 0) 654 | self.terrorist_hunt.kills = stats.get(statname + "kills", 0) 655 | self.terrorist_hunt.headshots = stats.get(statname + "headshot", 0) 656 | self.terrorist_hunt.matches_lost = stats.get(statname + "matchlost", 0) 657 | self.terrorist_hunt.dbno_assists = stats.get(statname + "dbnoassists", 0) 658 | self.terrorist_hunt.suicides = stats.get(statname + "suicide", 0) 659 | self.terrorist_hunt.barricades_deployed = stats.get(statname + "barricadedeployed", 0) 660 | self.terrorist_hunt.reinforcements_deployed = stats.get(statname + "reinforcementdeploy", 0) 661 | self.terrorist_hunt.total_xp = stats.get(statname + "totalxp", 0) 662 | self.terrorist_hunt.rappel_breaches = stats.get(statname + "rappelbreach", 0) 663 | self.terrorist_hunt.distance_travelled = stats.get(statname + "distancetravelled", 0) 664 | self.terrorist_hunt.revives_denied = stats.get(statname + "revivedenied", 0) 665 | self.terrorist_hunt.dbnos = stats.get(statname + "dbno", 0) 666 | self.terrorist_hunt.gadgets_destroyed = stats.get(statname + "gadgetdestroy", 0) 667 | self.terrorist_hunt.areas_secured = stats.get(statname + "servershacked", 0) 668 | self.terrorist_hunt.areas_defended = stats.get(statname + "serverdefender", 0) 669 | self.terrorist_hunt.areas_contested = stats.get(statname + "serveraggression", 0) 670 | self.terrorist_hunt.hostages_rescued = stats.get(statname + "hostagerescue", 0) 671 | self.terrorist_hunt.hostages_defended = stats.get(statname + "hostagedefense", 0) 672 | self.terrorist_hunt.blind_kills = stats.get(statname + "blindkills", 0) 673 | 674 | return self.terrorist_hunt 675 | 676 | @asyncio.coroutine 677 | def check_terrohunt(self, data=None): 678 | """|coro| 679 | 680 | Checks the players general stats for terrorist hunt, only loading them if they haven't been loaded already""" 681 | if self.terrorist_hunt is None: 682 | yield from self.load_terrohunt(data=data) 683 | return self.terrorist_hunt 684 | 685 | @property 686 | def wins(self): 687 | return self.won 688 | 689 | @property 690 | def losses(self): 691 | return self.lost -------------------------------------------------------------------------------- /r6sapi/definitions/loadouts.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2016-2020 jackywathy 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | """ 10 | loadouts_const = [ 11 | { 12 | "id": "1MobOPbsFZoVpLWDUUgmeg", 13 | "name": "dp27", 14 | "weapon_type": "primary", 15 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7LoT7yAe0LK7bDOeq6MZZM/33995bc704667674af1b73fe962d4c7c/Primary_gun_DP27.png" 16 | }, 17 | { 18 | "id": "3iisbOg3JC9epuJDdrMcAk", 19 | "name": "9x19vsn", 20 | "weapon_type": "primary", 21 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/42gH96xTTYaTZsfXI3c0wL/a7edbf11af97091ee884b68e59fe6a4f/9x19VSN.png" 22 | }, 23 | { 24 | "id": "4EmVfbHbYqwRNnK02lU79C", 25 | "name": "pmm", 26 | "weapon_type": "secondary", 27 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3y4LIwwm8YNQHAv8oOkWCK/a2375901cee34e68fa39c976d85de8aa/PMM.png" 28 | }, 29 | { 30 | "id": "3Ch5Pac0IKVBJe5oYZzIol", 31 | "name": "gsh-18", 32 | "weapon_type": "secondary", 33 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5s5Q33j3MNcXf9lwfxfd7m/4eb3a6af1d431481b6ddcec44fbc7602/GSh-18.png" 34 | }, 35 | { 36 | "id": "3WoO6qQpm6SkD2ceFlpIVq", 37 | "name": "barbed-wire", 38 | "weapon_type": "gadget", 39 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7igaibxuCcSpWDkZensEJS/bfa2cef52f3d860b7a06c2b4d7a6340e/Barbed_wire.png" 40 | }, 41 | { 42 | "id": "5QtTa00eoscVRzAfGy44y6", 43 | "name": "proximity-alarm", 44 | "weapon_type": "gadget", 45 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2TsFLmb2O6LRZpbxzkZDck/c9146913388a9567500b704c95600621/Proximity_alarm.png" 46 | }, 47 | { 48 | "id": "4rJKd9S4S3Edu84n3jaWbq", 49 | "name": "shumikha-launcher", 50 | "weapon_type": "unique_ability", 51 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/37wX75QnY7XA6KbjM4aF5n/0ab116d398cf71463e11d43913818ec1/Shumikha-Launcher.png" 52 | }, 53 | { 54 | "id": "1LVSwzrXIEAd1O3vntSQMs", 55 | "name": "p10-roni", 56 | "weapon_type": "primary", 57 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7K86OBjL3zmYWt0ZvUcCLj/16a947334e39f27da177d787773593e4/r6-operator-weapon-smg-p10roni.png" 58 | }, 59 | { 60 | "id": "6xDz1HSwIn3ZcV9nKIeKUN", 61 | "name": "mk-14-ebr", 62 | "weapon_type": "primary", 63 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/6KIMqp5dA95z1RI3PrG9jv/eb939638169811a3fa858a44e6e5d97e/Mk_14_EBR.png" 64 | }, 65 | { 66 | "id": "5mI0sCcUxKW3Imv5ZMBBeL", 67 | "name": "prb92", 68 | "weapon_type": "secondary", 69 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/dl28J1HsE7mzhj66pmd5D/b8d8fc48d2dde13154047de94abbd8ca/PRB92.png" 70 | }, 71 | { 72 | "id": "7eb4vAG3ycZGuIRAoRl58a", 73 | "name": "surya-gate", 74 | "weapon_type": "unique_ability", 75 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4hLJAAVKrf50wosG0471od/cde1867daf863c03754969f159ac00de/r6s-operator-ability-aruni.png" 76 | }, 77 | { 78 | "id": "2Mh1URS57n4Yuc2vHOojl7", 79 | "name": "super-90", 80 | "weapon_type": "primary", 81 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1TLWSu0xHJlAsfEfafeC9X/f9647e70a18962bf1627095c8b46832e/Super_90.png" 82 | }, 83 | { 84 | "id": "4pZ8kx4SSqhhLJ1iaSyEAU", 85 | "name": "9mm-c1", 86 | "weapon_type": "primary", 87 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/60sbThKtOpNOwKu3OP0oGV/672fd9263f7786402a0d855273473a6f/9mm_C1.png" 88 | }, 89 | { 90 | "id": "2cDP1BjKw2UkKkDJhYLZAU", 91 | "name": "mk1-9mm", 92 | "weapon_type": "secondary", 93 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3tWoNeF3jQYs3w4EOydQYs/434409c96693df1fd3e969d778e70795/Mk1_9mm_BI.png" 94 | }, 95 | { 96 | "id": "6Urz2FjkmefuoCPGDoVZCm", 97 | "name": "ita12s", 98 | "weapon_type": "secondary", 99 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5G4DroaSdqHzJWCe7qqbHZ/5dd2e03f853182c78a1e7fcbc642f0cf/ITA12S.png" 100 | }, 101 | { 102 | "id": "7pAPyONkaR3xGR47gvXwSz", 103 | "name": "bulletproof-camera", 104 | "weapon_type": "gadget", 105 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/gZuOXvuTu2i8hQX0B6auy/259f379a6283bae618443d722a896f1a/Bulletproof_camera.png" 106 | }, 107 | { 108 | "id": "6ZPm8q8dyQXt1my5OHZWic", 109 | "name": "deployable-shield", 110 | "weapon_type": "gadget", 111 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/W0WE0X2VQlxwWIAFCJ6Jm/523650a39de5a23dd9520d7299c9e25a/Deployable_Shield.png" 112 | }, 113 | { 114 | "id": "2yKP1QdTJfIMQN9d7ZeTmU", 115 | "name": "welcome-mate", 116 | "weapon_type": "unique_ability", 117 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/xsIzH7XCAqvn7F3tEfAPe/c41e59a9d7f2ed7ee38b16ed0a882351/Welcome-Mate.png" 118 | }, 119 | { 120 | "id": "zs4Rebj67KAk06ASjJPxO", 121 | "name": "spas-12", 122 | "weapon_type": "primary", 123 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7Hp6Fbss6uI59OT4nZNB6e/a4d09954803cb2580353cfa03e8c778b/SPAS-12.png" 124 | }, 125 | { 126 | "id": "41pnpfTTAjzKYvpEBmNKdD", 127 | "name": "t-5-smg", 128 | "weapon_type": "primary", 129 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1Ne8bvX8BdCALevWKMllQN/4baa3e79d323de134dd182e0272b9c3b/T-5_SMG.png" 130 | }, 131 | { 132 | "id": "1fj1XX5YxggVcr5mU1OPy3", 133 | "name": "bailiff-410", 134 | "weapon_type": "secondary", 135 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/N8FLbo4fsNyBe8msKgRhT/8f403dc0b58087bcafab786dd95ba33f/Bailiff_410.png" 136 | }, 137 | { 138 | "id": "4HiVAhAUQe5BEXgBNg2ECe", 139 | "name": "usp40", 140 | "weapon_type": "secondary", 141 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7FxemzWRtlpAhK9MyKp1Gp/817cc25b6b7c3575dc1ba53a6a8170a9/USP40.png" 142 | }, 143 | { 144 | "id": "6kF8p8NlbGPvjRz42YxYYE", 145 | "name": "remah-dash", 146 | "weapon_type": "unique_ability", 147 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3dM2B3qCdU0woydIbiy2xn/55aa99443002ad794d3f78dada26d035/r6s-operator-ability-oryx.png" 148 | }, 149 | { 150 | "id": "eR3JkIxE5GyWvNpybHCRr", 151 | "name": "mp5", 152 | "weapon_type": "primary", 153 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/60YbOvSBQt6ZUlu8YDXoZm/51ef3857b2986de700262432e8433714/MP5.png" 154 | }, 155 | { 156 | "id": "5QeGmJGqn3gZxACxzF4kbR", 157 | "name": "rg15", 158 | "weapon_type": "secondary", 159 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2LNSsp7B7wUnnPUweir7Jm/9f66d53be7a63a17a55253a0bea6eec1/RG15.png" 160 | }, 161 | { 162 | "id": "7zuAWr4kVRJFYVj33Ltfex", 163 | "name": "impact-grenade", 164 | "weapon_type": "gadget", 165 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7iJK9B1Vr3PDO3rGftU00l/c3d8edc5564a80580e4ac2f9a4fc3937/Impact_Grenade.png" 166 | }, 167 | { 168 | "id": "4Lnu4kaDPzUIxgCStqfrbR", 169 | "name": "nitro-cell", 170 | "weapon_type": "gadget", 171 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4pBSTw9U6l9GRnT12G6Xln/e0991bc03b48d217f510af8b611c8828/Nitro_Cell.png" 172 | }, 173 | { 174 | "id": "5CkPFHPPJB3909Fff9BYBs", 175 | "name": "banshee", 176 | "weapon_type": "unique_ability", 177 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/49ixqWhGgjvHu0Ay8JzeSH/c6a3fe584847850186e15c7fb4244385/r6s-operator-ability-melusi.png" 178 | }, 179 | { 180 | "id": "wfzQPegCiVkDRgsx6MOjZ", 181 | "name": "c8-sfw", 182 | "weapon_type": "primary", 183 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1itXpz2GnvdwwRyhX1SYa2/b58ff71048fa3bb5ed09d5d935dc90f4/C8-SFW.png" 184 | }, 185 | { 186 | "id": "5b6dGdkffoVKAyDycG5hjg", 187 | "name": "camrs", 188 | "weapon_type": "primary", 189 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4dBzqVVmnpv1DZi91LAnEN/e374b4ea289fc992280b943cdbb94d60/CAMRS.png" 190 | }, 191 | { 192 | "id": "myf6Hy39exE9Cot5zDEis", 193 | "name": "claymore", 194 | "weapon_type": "gadget", 195 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4T4H5EJgUxorucGVtU2pkm/74fef324b89c220ce6426e8097f915b9/Claymore.png" 196 | }, 197 | { 198 | "id": "2NNtCVZhqQykqVEtze4fxJ", 199 | "name": "stun-grenade", 200 | "weapon_type": "gadget", 201 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3XnK8s1iQJQu5cfr6UyQfK/429480b96d6d6d6b830c32c75d2608f5/Stun_Grenade.png" 202 | }, 203 | { 204 | "id": "XNjuIs9nL1RQNnWOMNfC9", 205 | "name": "skeleton-key", 206 | "weapon_type": "unique_ability", 207 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2w8EQtN4FFtEMa9lBYyWGg/36bbc6d819761c11418c868d2e483991/Skeleton-Key.png" 208 | }, 209 | { 210 | "id": "65tbXPRuQxAV8RUaMoCYJh", 211 | "name": "lmg-e", 212 | "weapon_type": "primary", 213 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7JVJIew6t3iKwgByvrFXyi/7ba44dfda28b525506633e453104a604/LMG-E.png" 214 | }, 215 | { 216 | "id": "2dvLoMLwWSRwyK70gARboS", 217 | "name": "m762", 218 | "weapon_type": "primary", 219 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4oWAgi7tgQP1Tq0HooRtye/9109a74921ee17610d4bd85a61582823/M762.png" 220 | }, 221 | { 222 | "id": "75HMflo54bNBGSUyX2je5s", 223 | "name": "breach-charge", 224 | "weapon_type": "gadget", 225 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1OgTMhyF1FBsSAo4njk26m/9881001e9db03a4806b2eea6007e4a1a/Breach_Charge.png" 226 | }, 227 | { 228 | "id": "5tTXUrm4TLdSBHtsJ1p9d8", 229 | "name": "ks79-lifeline", 230 | "weapon_type": "unique_ability", 231 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1elqIEWJ6XsXKAbMNd0Cai/0b4c0591bad284d957e652cdae0b706b/KS79-Lifeline.png" 232 | }, 233 | { 234 | "id": "HCADlLBkfNlDRRvlq3iPo", 235 | "name": "mp5sd", 236 | "weapon_type": "primary", 237 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5HaMldwFltBwiiyDDfkPpD/6de3aa9aaa17458e7f6186ba59b8deff/MP5SD.png" 238 | }, 239 | { 240 | "id": "67fxpqXp4gjOQixPHPaQMB", 241 | "name": "supernova", 242 | "weapon_type": "primary", 243 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2tpjCRFLcc3hogjJGbKDsi/5ad0ab63b7245022aca5c1c1fb42d473/SuperNova.png" 244 | }, 245 | { 246 | "id": "4mbLbnjsLEQ27BEXQ1vqGs", 247 | "name": "p229", 248 | "weapon_type": "secondary", 249 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/76ja0RxqzHW9PpvWgpG7Sk/cb753b50b20fe67deaef54d8b2a46b54/P229.png" 250 | }, 251 | { 252 | "id": "7sblClEPf57IKm77UCqFSj", 253 | "name": "bearing-9", 254 | "weapon_type": "secondary", 255 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4mdftEOh5Vu9KhhpgKLKrT/abedcc75868774018295ec2a08a7b3de/Bearing_9.png" 256 | }, 257 | { 258 | "id": "5FUiujmYYXsvq1zQ0lZlVx", 259 | "name": "yokai", 260 | "weapon_type": "unique_ability", 261 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/TdDZyrKpjt9EQo8tHpIJk/d987db4da22046a0663be8be82dcda88/Yokai.png" 262 | }, 263 | { 264 | "id": "5cZ1wkLzuOHnnYjma40PwQ", 265 | "name": "bosg-12-2", 266 | "weapon_type": "primary", 267 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2ZjVndetsX8WEn5ZfyUQa0/e3a781be7eab22876d25f748e8fd0f5a/BOSG.12.2.png" 268 | }, 269 | { 270 | "id": "2zEjl6sxdsxgVBmAsDZxcq", 271 | "name": "c75-auto", 272 | "weapon_type": "secondary", 273 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3wUuefwPjU705mZkTdJ9UH/8ccb11884cfa34c176ac5500af139177/C75_Auto.png" 274 | }, 275 | { 276 | "id": "4W61sh5pt9Ghkw4g7Muvee", 277 | "name": "smg-12", 278 | "weapon_type": "secondary", 279 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/EwJgB7KdgOb6dDm7ro33u/b73f0890f992c1a365210f08efcc6db5/SMG-12.png" 280 | }, 281 | { 282 | "id": "7aslgBcBTFi4XKqlAkvvrc", 283 | "name": "smoke-grenade", 284 | "weapon_type": "gadget", 285 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3LaxoSLC49T5vgKnUAlTLT/c47c4636845a04478432c48be8c29aee/Smoke_Grenade.png" 286 | }, 287 | { 288 | "id": "5o5qaxqMxosu04407U4sBL", 289 | "name": "logic-bomb", 290 | "weapon_type": "unique_ability", 291 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5ej2g1iCMHdfjn8h8qgfmU/bf07fef4b063a46389ca650ed02b292a/Logic-Bomb.png" 292 | }, 293 | { 294 | "id": "kzR6vfRLXm9f1EvoK9dBP", 295 | "name": "m12", 296 | "weapon_type": "primary", 297 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4FxqA5pa8JY9QQ7FEcjwPw/ffc779fcde5b970e7b95db6653637dab/M12.png" 298 | }, 299 | { 300 | "id": "5gcX8x7LiBHg2LA1JIdEHp", 301 | "name": "spas-15", 302 | "weapon_type": "primary", 303 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/CyofBgipHq4RTafvPFWd4/bc3d0ecc871b70e57735855f852efacf/SPAS-15.png" 304 | }, 305 | { 306 | "id": "1Y7hJmAXWWqh1MIkXqUbKw", 307 | "name": "luison", 308 | "weapon_type": "secondary", 309 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5cSDFUWb8P1BAQUgnTozeM/fd3a3348f42c95d6afa9f105ae23f2e5/Luison.png" 310 | }, 311 | { 312 | "id": "1ojdoiQ8AbqFX3FB7Neqmk", 313 | "name": "silent-step", 314 | "weapon_type": "unique_ability", 315 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/6PTsBBBGTT5oixxzvYv1Y4/18e31c74ba1ca73ed2694134acd9c078/Silent-Step.png" 316 | }, 317 | { 318 | "id": "6Il345pPRhv4Xx4qzTFpmA", 319 | "name": "aug-a2", 320 | "weapon_type": "primary", 321 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1eO39zRe8XxJXH1KZiIWhM/02049ced0fbfa630833e8b0d3c03de07/AUG_A2.png" 322 | }, 323 | { 324 | "id": "3Xq4lwAY8Sc1Z687gD9mnD", 325 | "name": "mp5k", 326 | "weapon_type": "primary", 327 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1pk8nOI7ybQjYOSI4fuzOm/fcd78df0f729be545e75c09aae85c360/MP5K.png" 328 | }, 329 | { 330 | "id": "4PHq1TcVzAqQp11Ve7CFFC", 331 | "name": "d-40", 332 | "weapon_type": "secondary", 333 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4niSMDCeiryoMBXJZq60Vv/48339331d05e289868cf4050c49b1b2b/D-40.png" 334 | }, 335 | { 336 | "id": "5Y36nPWZ6lXp37GDupoLRV", 337 | "name": "p12", 338 | "weapon_type": "secondary", 339 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2mpM7rah7rwEW0bViIirUC/ed9caa4db58421519fa4db390b1aa164/P12.png" 340 | }, 341 | { 342 | "id": "6L5PL3qOQjjpNUdA9l0WLD", 343 | "name": "mag-net-system", 344 | "weapon_type": "unique_ability", 345 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1IKNZzLv63AJd9vlbXj3Bo/883371432ffb22e5bf35bc82dd706384/Mag-net_System.png" 346 | }, 347 | { 348 | "id": "3ePDML7HMucggZaNG2nL0a", 349 | "name": "t-95-lsw", 350 | "weapon_type": "primary", 351 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/23HCxaNTRUHBlFAvCTMZQm/fe319cc164fac034a29e9b114ae7d5cb/T-95_LSW.png" 352 | }, 353 | { 354 | "id": "6pPXSrzgAKEyTiiyrs1Qbn", 355 | "name": "six12", 356 | "weapon_type": "primary", 357 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2v6MwsHwjOZ5Muid53lyfN/e5f1c4997db93abfe3ac356fce23376c/SIX12.png" 358 | }, 359 | { 360 | "id": "3ECycrhAlLH7str0T4F2hp", 361 | "name": "q-929", 362 | "weapon_type": "secondary", 363 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2fRVszR5yGDHbV0AL8muso/0838dac90b66aa810daa49d36382fb64/Q-929.png" 364 | }, 365 | { 366 | "id": "31sOhkze6zBhWkkM8HR44n", 367 | "name": "secondary-breacher", 368 | "weapon_type": "gadget", 369 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3OvnVPWY1UyrDE913kU0a1/eae4b2a1584234ea2ff4ad6481239f3b/SecondaryBreacher.png" 370 | }, 371 | { 372 | "id": "168akpqtP52LsTYlMIqeHX", 373 | "name": "candela", 374 | "weapon_type": "unique_ability", 375 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4vpN9vu5wD9dyb2knMosTy/430796de3c0c2a5c2eb2ac6f4217eba0/Candela.png" 376 | }, 377 | { 378 | "id": "5hAVF2eVv7NyeJPAJL07sg", 379 | "name": "ak-74m", 380 | "weapon_type": "primary", 381 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1j5HiQP8aFphTe65fqDdg0/23eecb5c603c5ba9f59fc6cbc5e4a531/AK-74M.png" 382 | }, 383 | { 384 | "id": "Q4Q9LtkAztMdeUT53C39j", 385 | "name": "arx200", 386 | "weapon_type": "primary", 387 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/6VgkPBsr1WApI3rWc9kcM0/b18b8e25f3e951e8e722213f2ee59eb0/ARX200.png" 388 | }, 389 | { 390 | "id": "2NcOcqzfy4HnaHNUITnUYN", 391 | "name": "-44-mag-semi-auto", 392 | "weapon_type": "secondary", 393 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/6W3Jz0YcQzbZ6BOPr7VVel/4c67f342964132a652f7d5821e887050/.44_Mag_Semi-Auto.png" 394 | }, 395 | { 396 | "id": "6dV0styTnHMeqh4effTNF8", 397 | "name": "airjab-launcher", 398 | "weapon_type": "unique_ability", 399 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/6d0LN1QWzviEkcYu3mTn6v/e49511a479756f71224f14225ad9cbd8/Airjab-Launcher.png" 400 | }, 401 | { 402 | "id": "4jhzD37iXaCsWyBAo1PQ5J", 403 | "name": "p90", 404 | "weapon_type": "primary", 405 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4nGrNspOvII2oS3lEMkg5x/2398a493c298bc654f97c58767aa40f3/P90.png" 406 | }, 407 | { 408 | "id": "43jUNG843Bn0knjA3tXwXo", 409 | "name": "sg-cqb", 410 | "weapon_type": "primary", 411 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5JoL3b36Fsztt9Q2XYmrbJ/dacec96948d3f8fe92914a69b9aac593/SG-CQB.png" 412 | }, 413 | { 414 | "id": "ElQvUTqCd5JbW2PIJ0lTS", 415 | "name": "p9", 416 | "weapon_type": "secondary", 417 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/6Fd1cl17KA0CtgodEiiY6v/d0f145ea72f2aacbd04260ba7d8f1c74/P9.png" 418 | }, 419 | { 420 | "id": "55BZj1JeqRvuczMpa04gRU", 421 | "name": "lfp586", 422 | "weapon_type": "secondary", 423 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1zc7UtdBfCZakwbiYqBvSz/1fd3f1584de38ca7c9315d498f094276/LFP586.png" 424 | }, 425 | { 426 | "id": "6XCPWiyRqIM6rfCYnSRFKg", 427 | "name": "armor-pack", 428 | "weapon_type": "unique_ability", 429 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/MeoKw7iPY6EFYvjS07CRg/b2d7eba623f3c63d6b7097a8f2253954/Armor-Pack.png" 430 | }, 431 | { 432 | "id": "3ATrltpsW24BFhZMHNmhfI", 433 | "name": "fmg-9", 434 | "weapon_type": "primary", 435 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/0oneJNsBR06QjuowxwtHG/bd3b391c6eec2bd615f2ed83197a13ac/FMG-9.png" 436 | }, 437 | { 438 | "id": "4ggSgqX4ixVHJZwhnenHC1", 439 | "name": "six12-sd", 440 | "weapon_type": "primary", 441 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1GTua079Xbtkpjhx96sRsW/079ed1a71a0d12b5e48e1b0d40b87110/SIX12_SD.png" 442 | }, 443 | { 444 | "id": "01zcYOKDgxP24MPkEaswD7", 445 | "name": "5.7-usg", 446 | "weapon_type": "secondary", 447 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/tkYcSAJSe5yGkeUhzZqBO/e81feb86df4a7eb6951052bec26b6ed7/5.7_USG.png" 448 | }, 449 | { 450 | "id": "7GKyGyCXQ9vVZ0kCdSPJl4", 451 | "name": "d-50", 452 | "weapon_type": "secondary", 453 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/6mMQRDsrComRFa7bC6cNkG/8cd17e545e3d28dcc11a040d000cfa16/D-50.png" 454 | }, 455 | { 456 | "id": "1p5ZdYWvISi4qDV0S2fDP4", 457 | "name": "frag-grenade", 458 | "weapon_type": "gadget", 459 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4GZsPHbm9H0k5EWz7TMkwO/33b9007bc6ee03dab15cfa15eb69e096/Frag_Grenade.png" 460 | }, 461 | { 462 | "id": "6GQ8on95B9PMLjMDrZjXgD", 463 | "name": "hel-presence-reduction", 464 | "weapon_type": "unique_ability", 465 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/57miqbOn8xWBh7ne7za3CV/35364108d49380a0ed33998f970e104f/HEL-Presence-Reduction.png" 466 | }, 467 | { 468 | "id": "56o4y5mOsXlFhnzWlq9xMJ", 469 | "name": "commando-9", 470 | "weapon_type": "primary", 471 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4P9dpUph5w3MSsLNnW6be/04baba24990fcb75a9c0bcfd01b7d190/Commando_9.png" 472 | }, 473 | { 474 | "id": "J5YsiIB8uvpeZgrWXrhlA", 475 | "name": "sdp-9mm", 476 | "weapon_type": "secondary", 477 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/Tgsdyz3XEqmgUYi9aZZgb/6755f4da7af7a7179ffab92acf8d477e/SDP_9mm.png" 478 | }, 479 | { 480 | "id": "6m6vEqsps3Mhn6cOHo9yKS", 481 | "name": "pest-launcher", 482 | "weapon_type": "unique_ability", 483 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5L0fFKVOwozKMcmJoenfef/56e4efdf77363556b35a76fd4e0e60f6/Pest-Launcher.png" 484 | }, 485 | { 486 | "id": "5AVE3Ok87dbmTwuI5K5fZg", 487 | "name": "le-rock-shield", 488 | "weapon_type": "primary", 489 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1bmXJOakdA6SOrGxBKA70T/1e489e366d6db287f475963df2040d3d/Extendable-Shield.png" 490 | }, 491 | { 492 | "id": "7J9icaPnaxguoiBWfdqomb", 493 | "name": "le-rock-shield", 494 | "weapon_type": "unique_ability", 495 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1bmXJOakdA6SOrGxBKA70T/1e489e366d6db287f475963df2040d3d/Extendable-Shield.png" 496 | }, 497 | { 498 | "id": "64NDkY7SFav037M3uh6KRD", 499 | "name": "vector-45-acp", 500 | "weapon_type": "primary", 501 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7D1cDf13FqUhoLihzvuPln/068aa7e507155598449c58c0a49a90d6/Vector_.45_ACP.png" 502 | }, 503 | { 504 | "id": "1M88HlyLX6jD774vkptDLV", 505 | "name": "ita12l", 506 | "weapon_type": "primary", 507 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4Y6ziRzm9RiPii83fm8BV1/1f472744d2c2dec8d9206f4d8733d92c/ITA12L.png" 508 | }, 509 | { 510 | "id": "6X2RibCre3jpetmCoFZaUu", 511 | "name": "black-mirror", 512 | "weapon_type": "unique_ability", 513 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1a1w8epOhWE8VtzvvCJG9d/b20cbb221f7d45e5838f839ce042f409/Black-mirror.png" 514 | }, 515 | { 516 | "id": "2xDl4cDXX48FuUiMPApZHo", 517 | "name": "ar-15-50", 518 | "weapon_type": "primary", 519 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4lGGEGZLkbldz114Wl5hCo/78a04c46654f80fae03e730bd79f3563/AR-15.50.png" 520 | }, 521 | { 522 | "id": "4zn5v7GdQhRyojYT6qAwwM", 523 | "name": "m4", 524 | "weapon_type": "primary", 525 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3jhi90ycmuc8mAiuSXFoCi/bcf354459e7becd6ede52ee97917c832/M4.png" 526 | }, 527 | { 528 | "id": "4GfGPq4g6TDwHvQEJII9ee", 529 | "name": "1911-tacops", 530 | "weapon_type": "secondary", 531 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/189UukZ6fVnvQR6LJtLYry/6eec29603d5b7b0ca8cab6ac0ef083ac/1911_TACOPS.png" 532 | }, 533 | { 534 | "id": "3ECK2BieW8MOShqE0XJVwd", 535 | "name": "breaching-torch", 536 | "weapon_type": "unique_ability", 537 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4rPBvxDKsKiQCMjt7GxJMw/09e45c68bbc41c1721acbbe0257e2465/Breaching-Torch.png" 538 | }, 539 | { 540 | "id": "4F64StqLivWX4lHm7iNgqG", 541 | "name": "v308", 542 | "weapon_type": "primary", 543 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5YBZe76NUDO32eF66wW90g/488c315743d59230962a4d67618223d6/V308.png" 544 | }, 545 | { 546 | "id": "7MC9QIlZkFL8AAqkAfGIbV", 547 | "name": "417", 548 | "weapon_type": "primary", 549 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5djkS4YtAtOF0vBmg0T60x/ea2b1ff7e5367e66c99bc7ad7e95bfe3/417.png" 550 | }, 551 | { 552 | "id": "zRjInzDWpoahREdiE2RDM", 553 | "name": "ee-one-d", 554 | "weapon_type": "unique_ability", 555 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7fRknnWl2K2qjKle1t79j/0506d25798aeb0691c8a576665050f7d/EE-ONE-D.png" 556 | }, 557 | { 558 | "id": "7tItrCBHMWLbtvlBxYDWfS", 559 | "name": "g36c", 560 | "weapon_type": "primary", 561 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2SZoqSXKoNPvZFIJsFsDE5/cb109885bf19c8697abf832f10cfd9a6/G36C.png" 562 | }, 563 | { 564 | "id": "4FYdLQfxYBnKCfY2cZ9flD", 565 | "name": "gemini-replicator", 566 | "weapon_type": "unique_ability", 567 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/K8E4EHWbD8wTjVqro6wVl/62339b2fbe1d3a2319dcd320f7a0b070/r6s-operator-ability-iana.png" 568 | }, 569 | { 570 | "id": "E0pKBweJkY0ok4BvfDSxv", 571 | "name": "csrx-300", 572 | "weapon_type": "primary", 573 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7tUB9ZNXJhdN6ejAkCEeFQ/99691bcc19f641cf872925905d08a539/CSRX_300.png" 574 | }, 575 | { 576 | "id": "5EraRZbq9P8VR8Sd0Sarh9", 577 | "name": "spsmg9", 578 | "weapon_type": "secondary", 579 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5EtwSgylXckBNg4n6gDR9J/bc6fc6c5c12ae11da59aee95828ebd76/SPSMG9.png" 580 | }, 581 | { 582 | "id": "2Eik88OMmWOse0qBVegpjG", 583 | "name": "lv-explosive-lance", 584 | "weapon_type": "unique_ability", 585 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/75eebt48ELO4eGGdIMVMpY/9533c7dc8f36651f5b5ad50c8ccb6c5a/LV_Explosive_Lance.png" 586 | }, 587 | { 588 | "id": "3rjbxjDZx9mwvN5xHkZDWp", 589 | "name": "aug-a3", 590 | "weapon_type": "primary", 591 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3W9XJdMOgpHSw55HfwRSAv/cf8f220678d503e6c3e535c00b2e636a/AUG_A3.png" 592 | }, 593 | { 594 | "id": "3vCxcPpLsOovwCKEuXLJrN", 595 | "name": "tcsg12", 596 | "weapon_type": "primary", 597 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2NDbY7BTBJ9R09LUilTlRf/3728337cd3ba14ed6ab9de0c22e879af/TCSG12.png" 598 | }, 599 | { 600 | "id": "4hAJAIXdGAU0uCmTcwLoha", 601 | "name": "rtila-electroclaw", 602 | "weapon_type": "unique_ability", 603 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7rUOk2LhYIUjvYLot7GT8Y/94b72bfbbfdf50c2c807cdbf9f5b276e/Rtila-Electroclaw.png" 604 | }, 605 | { 606 | "id": "7gAppJYmlz1A8xXgPt0a5m", 607 | "name": "m870", 608 | "weapon_type": "primary", 609 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2rkU6g4Rlg0e0U4rczWGTV/a51589a54c43f476d8eb984c0ea881e9/M870.png" 610 | }, 611 | { 612 | "id": "2Hre4GaBWs92I37LII1O8M", 613 | "name": "416-c-carbine", 614 | "weapon_type": "primary", 615 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2I86r2a2QD8EHTZVZnxcxy/2913450ba952a16c29fac1f5ce58ba1a/416-C_Carbine.png" 616 | }, 617 | { 618 | "id": "17DSq6qMxwSmARSjIDwDKE", 619 | "name": "active-defense-system", 620 | "weapon_type": "unique_ability", 621 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1YCujceutAcJ7F10yhHC41/c5f870e7789b6396c9997ed45ccd3beb/Active-Defense-System.png" 622 | }, 623 | { 624 | "id": "2BpqLwwDeSr7QpNZqsLvBt", 625 | "name": "f90", 626 | "weapon_type": "primary", 627 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/62tE3th2ThcGHlrcqWkmEX/d69c9de199542e25fa55f6d293f15671/r6-operator-weapon-ar-f90.png" 628 | }, 629 | { 630 | "id": "3zbsuyeTh78X5KHV3M7Ctt", 631 | "name": "m249-saw", 632 | "weapon_type": "primary", 633 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3p0oG7GsLIoHaRullf7xsF/e2a9e135af63e8897355023cd34538c4/M249_SAW.png" 634 | }, 635 | { 636 | "id": "WliOiho6hjQFZ7BiJT7uV", 637 | "name": "super-shorty", 638 | "weapon_type": "secondary", 639 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7Dq8LDmIxAveRqXM17orUW/cbd96b47cd8ca74a7827b16ef73fe7cf/r6-operator-weapon-sa-supershorty.png" 640 | }, 641 | { 642 | "id": "2IZvSVScGT9SAKL7oedtlN", 643 | "name": "trax-stingers", 644 | "weapon_type": "unique_ability", 645 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/QGVvmZeZ91FC2X4mvMzgn/601fa45e635872aea31f15ffebb9c366/Trax-Stingers.png" 646 | }, 647 | { 648 | "id": "5nXwwDj4qtPaGvCKrTqdpC", 649 | "name": "ots-03", 650 | "weapon_type": "primary", 651 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4fXznwDtLt61VCF8QIF4N3/34e2e6d6c33d4c504c945bdd13c322f6/OTs-03.png" 652 | }, 653 | { 654 | "id": "5JiIaIiidLpM5wZGRmbZxO", 655 | "name": "flip-sight", 656 | "weapon_type": "unique_ability", 657 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/73bNPGhlIuhlWvi497sYqE/b68414436088f62f9da44cd42f702df7/Flip-Sight.png" 658 | }, 659 | { 660 | "id": "3seBqopkUJQZwKAodylxXj", 661 | "name": "volcan-shield", 662 | "weapon_type": "unique_ability", 663 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1JqlRdbaVA73jDq8y46vX4/82e89f39c479526ace294ba246d0b085/Volcan-Shield.png" 664 | }, 665 | { 666 | "id": "bz7Z7LsOpGGFaLxmNl5nY", 667 | "name": "ak-12", 668 | "weapon_type": "primary", 669 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7KAZZgnpqD07y47jVVXEuh/e0d7e67101f8f966aa6e1c59e835454f/AK-12.png" 670 | }, 671 | { 672 | "id": "4t1fOF2T7bKerBD9VJA5HH", 673 | "name": "6p41", 674 | "weapon_type": "primary", 675 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1wxS2HOCvoPAfnJEDFWjfw/7feddb98582ec37b500243d3f3e19eca/6P41.png" 676 | }, 677 | { 678 | "id": "2dRlzAkeuYgN8yAw3538qs", 679 | "name": "ballistic-shield", 680 | "weapon_type": "primary", 681 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2C21gwsjOka5Rwp8qSM5hA/a38937032260bce4f690fb9bb8adf4c0/Ballistic_Shield.png" 682 | }, 683 | { 684 | "id": "5mHxExG3OPZkUmuXk4bzD6", 685 | "name": "cluster-charge", 686 | "weapon_type": "unique_ability", 687 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3YaoPPUbFYeVSCemdj57EL/a4a4a8c0a935640f7d9a1d1ea82bc48c/Cluster-Charge.png" 688 | }, 689 | { 690 | "id": "5wGib1JAMhp1o32ZKqXmm6", 691 | "name": "spear-308", 692 | "weapon_type": "primary", 693 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/29LjYuJ4s6yA8k9Uv2u28C/89ec812559e7d74b7c269279f4c46d92/Spear_.308.png" 694 | }, 695 | { 696 | "id": "5drcQCH9GYIQ02G2qL1lUJ", 697 | "name": "sasg-12", 698 | "weapon_type": "primary", 699 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2Q6mL4CbifmIgifV2yV3Hi/2bb2b323f055b03a2c1ba516c262c24e/SASG-12.png" 700 | }, 701 | { 702 | "id": "Gef0UGqp5PwYOIHnYuMqM", 703 | "name": "adrenal-surge", 704 | "weapon_type": "unique_ability", 705 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/9xGRNPNznBKssvgQAtQNQ/9352fc88f2911ab40789412856b3e20e/Adrenal-Surge.png" 706 | }, 707 | { 708 | "id": "3ix2ui28VAIlHII80zcM5w", 709 | "name": "ump45", 710 | "weapon_type": "primary", 711 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/6X2EZPq2s8UKrP67uxz5FI/f0df4c57d5890c79311e4eb62d4470e7/UMP45.png" 712 | }, 713 | { 714 | "id": "1DQ0Gw0othORiig1DeyG9p", 715 | "name": "m1014", 716 | "weapon_type": "primary", 717 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2pUiVbwNnQnDTesmWXktqW/f27c1fab9a354bb89cbe309a688f5e02/M1014.png" 718 | }, 719 | { 720 | "id": "3z0HQKCIxJGY6oyDt03sKb", 721 | "name": "armor-panel", 722 | "weapon_type": "unique_ability", 723 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/29N9nMqB8ZZxGCPz128ccD/439cb1fcb2f6d5385378cf073a5fbc30/Armor-Panel.png" 724 | }, 725 | { 726 | "id": "1LHrJG8fIAJhMI6aNtPkAK", 727 | "name": "para-308", 728 | "weapon_type": "primary", 729 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/6ub8y2Cs5EYhVPfDWuVVkW/82ca131a41ee4ba2e0b75f2dc52ed9e3/PARA-308.png" 730 | }, 731 | { 732 | "id": "2u4Ha4SV5Gc18jlkABp5m5", 733 | "name": "m249", 734 | "weapon_type": "primary", 735 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7z8UpVPS3P14OC1oL9dDIn/39c0c657f154218003fd4b2a9250b92f/M249.png" 736 | }, 737 | { 738 | "id": "48ucSL0frcAy6enBiqoCT7", 739 | "name": "tactical-crossbow", 740 | "weapon_type": "unique_ability", 741 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5ur3NZUGos3i2HR8f0HIzj/46cf23c97453ebfedeaa42a1088ff32f/Tactical-Crossbow.png" 742 | }, 743 | { 744 | "id": "2K9MC1d7AwBgBsELz8LqGt", 745 | "name": "selma-aqua-breacher", 746 | "weapon_type": "unique_ability", 747 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2sjKOnwHeOX2xn3iIpja2A/e265f675c905ac25c23ed11fc85589bb/r6s-operator-ability-ace.png" 748 | }, 749 | { 750 | "id": "6zo1HGo261dNdW2J7dBKNF", 751 | "name": "g8a1", 752 | "weapon_type": "primary", 753 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4TIb7oeJesaROOOfTlCBaZ/ffd6a802f9a779a0d39b2122c49b3254/G8A1.png" 754 | }, 755 | { 756 | "id": "6RefWdx10DL4hO0egguU6k", 757 | "name": "smg-11", 758 | "weapon_type": "secondary", 759 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3WExw7Kepz9uAiWAbWW457/875fc631a3cf9fcc2849d9db2989cbcd/SMG-11.png" 760 | }, 761 | { 762 | "id": "2EyyQjA0RqjezxwBQRjs9i", 763 | "name": "garra-hook", 764 | "weapon_type": "unique_ability", 765 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3WejtMAtiITfpjDMuq6j4t/b52e58da6b2625839aa23f940c8e6639/Garra-Hook.png" 766 | }, 767 | { 768 | "id": "3VKlSROsHKgSvtDPoTEkqA", 769 | "name": "sc3000k", 770 | "weapon_type": "primary", 771 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7x7eDTm2NNpfGiFMrfQqEX/9898e74c780537be3ca6d88db32ea21e/F2000.png" 772 | }, 773 | { 774 | "id": "01BtNAaccSZAwYXHWPvftF", 775 | "name": "mp7", 776 | "weapon_type": "primary", 777 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3a4dgTWGdiJqALhtRp4pKy/f2568d3de3cfe7e4b53179e8653cd2a2/MP7.png" 778 | }, 779 | { 780 | "id": "2JJFeZIJQhdNGXTnlihNlC", 781 | "name": "argus-launcher", 782 | "weapon_type": "unique_ability", 783 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/6h4hyVSzG8IwAmEl1Objrd/6e51e64eeffcc68746b8ff59445fb103/r6s-operator-ability-zero.png" 784 | }, 785 | { 786 | "id": "3EA7ghdrMfBGGjUNcE4MBE", 787 | "name": "r4-c", 788 | "weapon_type": "primary", 789 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/dQbqK9VxczuiscwBDSkT8/777a062f6095dde0371eab5200dcb451/R4-C.png" 790 | }, 791 | { 792 | "id": "4yuTxBnHbo06UOT2lN9aH7", 793 | "name": "m45-meusoc", 794 | "weapon_type": "secondary", 795 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3u5cecgWYl3WuJK50mKEGd/a4a0eb15c710edfc0d29e98c2ee7ea33/M45_MEUSOC.png" 796 | }, 797 | { 798 | "id": "52qdmZ4OCXOiyJY1ZKOaCS", 799 | "name": "breaching-rounds", 800 | "weapon_type": "unique_ability", 801 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/0114WqhzsMsnvaKc4FypkN/5ebb9b86e216a2d9e6b2ea01eb3346e8/Breaching-Rounds.png" 802 | }, 803 | { 804 | "id": "61aQ9zUboTRqi1enZxK9ly", 805 | "name": "f2", 806 | "weapon_type": "primary", 807 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5HTvw1cJInVAGxOLXR0war/2f142437f5c0944fdcfcce8a03c37676/F2.png" 808 | }, 809 | { 810 | "id": "4tazZObB7cojVKPOSe7ECB", 811 | "name": "shock-drones", 812 | "weapon_type": "unique_ability", 813 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5dZ9kaUfUSF3piuFIUKf2t/7ebfc51caee42a776492b56251d45d92/Shock-Drones.png" 814 | }, 815 | { 816 | "id": "76NdyGdH2niECMq7R1mmcc", 817 | "name": "cce-shield", 818 | "weapon_type": "primary", 819 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5mmGgrYdJJHw2moBIEW9An/64e9727d959d7afdbb4fb06e2f75574a/CCE_Shield.png" 820 | }, 821 | { 822 | "id": "H9nxKMekxjjC0VY4dPkFl", 823 | "name": "p-10c", 824 | "weapon_type": "secondary", 825 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2l4qwB50zSFhFZVYRLNwqg/20df8114f69f96f2adc54779ccc5bbaa/P-10C.png" 826 | }, 827 | { 828 | "id": "6uUbnIyQCMCeOMHYOMY6U5", 829 | "name": "cce-shield", 830 | "weapon_type": "unique_ability", 831 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1jck6fnzAMbMQrUMVsnA0M/d04a60eab0132e6bcc202a4f99186cdd/CCE-Shield.png" 832 | }, 833 | { 834 | "id": "4NYCY16B7qUBs0HYRIc7vB", 835 | "name": "alda-5.56", 836 | "weapon_type": "primary", 837 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/39yB6TFl9ph6Rb4bDV4lqK/7f9b3abf8dff19bacc026a7212849ca4/ALDA_5.56.png" 838 | }, 839 | { 840 | "id": "52fFOJXcNhgjPzKJTXY7pM", 841 | "name": "acs12", 842 | "weapon_type": "primary", 843 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/13z63kT1NLzn1U99o7WC4T/8655d3200f24b87246c36f2622603457/ACS12_PB.png" 844 | }, 845 | { 846 | "id": "1syKrLJCDUI7WxAhuesUGJ", 847 | "name": "keratos-357", 848 | "weapon_type": "secondary", 849 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/15caVAsSCr8Rsb5Hid36uc/59632c4f90727931041ced62a620018b/Keratos_.357.png" 850 | }, 851 | { 852 | "id": "6YdURVQbxwxmZRjKY1ZwBP", 853 | "name": "evil-eye", 854 | "weapon_type": "unique_ability", 855 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/n2rfPidCv630jQEfnEWwb/42d454d0771218eb8f27f6d17d8a073e/Evil-Eye.png" 856 | }, 857 | { 858 | "id": "3tnLGjoWpUTeSpfzXNZcyr", 859 | "name": "gu-mines", 860 | "weapon_type": "unique_ability", 861 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/6PJv86R8CtQCWA7a24sJE2/24f3751b2ed941ce80a4c1ef394ab7d5/Gu-mines.png" 862 | }, 863 | { 864 | "id": "1v4iBk8OSQ5FCg3RDPeIAN", 865 | "name": "mk17-cqb", 866 | "weapon_type": "primary", 867 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4LytczDQmu0M63gO2WtCCm/331ef3b1938352ae71d7c0bd23de3596/Mk17_CQB.png" 868 | }, 869 | { 870 | "id": "6mq7Ochfrvvq8qX52OOR70", 871 | "name": "sr-25", 872 | "weapon_type": "primary", 873 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3H3sICdj6BK8LhtQPRd2aJ/26826ebba73e0e5fd503256d069f3256/SR-25.png" 874 | }, 875 | { 876 | "id": "7xWTsMnS6KCAogW4wJAG8o", 877 | "name": "rifle-shield", 878 | "weapon_type": "unique_ability", 879 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2dZeBTlDDdFQKb4PYb8F5v/162d60178a75cde9f65be362eacc880a/Rifle-Shield.png" 880 | }, 881 | { 882 | "id": "2o6tAempnPqYVtMSkBAiN7", 883 | "name": "type-89", 884 | "weapon_type": "primary", 885 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7wLf325q9amF8bnVP1QGr0/2faff1a197f90dcded4472852a317d6b/Type-89.png" 886 | }, 887 | { 888 | "id": "5p6Nw5U3jQGLOn3u9VjkbI", 889 | "name": "x-kairos", 890 | "weapon_type": "unique_ability", 891 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1QSzVxpGhswXix3vn8XGKj/c4f64fa0895bdaf164448e3ae49950a0/X-Kairos.png" 892 | }, 893 | { 894 | "id": "eFmXKWVc4sXT4dOujQ75d", 895 | "name": "l85a2", 896 | "weapon_type": "primary", 897 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5vYQpoyk36foDzDq49jBd0/1479a2d7189e545555ceccecf6bd7cc3/L85A2.png" 898 | }, 899 | { 900 | "id": "3Vx4zT0vcf5CkxoQL1xJQi", 901 | "name": "m590a1", 902 | "weapon_type": "primary", 903 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2zRHmgqENNiZqXQxC9Rsbj/e6542407c642f9b7c5a4546afb6db30a/M590A1.png" 904 | }, 905 | { 906 | "id": "5hjFvX6r4GLEqYNQu7p2yi", 907 | "name": "p226-mk-25", 908 | "weapon_type": "secondary", 909 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/RTQvPQcywlRwUS1FjIKCX/6fc72fee2191c2e723276bc10ae4114e/P226_Mk_25.png" 910 | }, 911 | { 912 | "id": "23YMIzsQbz1cld6LVhH9gL", 913 | "name": "tactical-breaching-hammer", 914 | "weapon_type": "unique_ability", 915 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2Vyo9CrQ1J7IZe43XpT4pV/4bc02e829d1b1745b9a527ff34f8fafb/Tactical-Breaching-Hammer.png" 916 | }, 917 | { 918 | "id": "6bqK3TkPT2RsEya3tCYiyS", 919 | "name": "entry-denial-device", 920 | "weapon_type": "unique_ability", 921 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/FLgwGbMiZTrWcK62KxPq8/d4e584420f85fa61c09e5e57e12d9dd9/Entry-Denial-Device.png" 922 | }, 923 | { 924 | "id": "7xrnNbilGusIwlgOWGgPci", 925 | "name": "c7e", 926 | "weapon_type": "primary", 927 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/63vTDjkXeKq7rOoSBhoJD4/08603e6603d564e0fa38af9ec86b7c1f/C7E.png" 928 | }, 929 | { 930 | "id": "5Hum7CZF4TohcA6nWHd9pO", 931 | "name": "pdw9", 932 | "weapon_type": "primary", 933 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4yYCuRnduMq35CTHfq6wwU/b7d49cdbcb05917e014c99efeaadd33b/PDW9.png" 934 | }, 935 | { 936 | "id": "1Yd2g2vX5zLxInwgvLdkoS", 937 | "name": "eyenox-model-iii", 938 | "weapon_type": "unique_ability", 939 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2gexf5zLDsa74J7urCoDxk/50da09626395cbe1bf2a58e00a57a514/Eyenox-Model-III.png" 940 | }, 941 | { 942 | "id": "213tBMKn095fROa9B1cV6n", 943 | "name": "552-commando", 944 | "weapon_type": "primary", 945 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1LT0N89YaOHvRwn3Pphr8K/02d4a3da9cda132d8201fd134f24fede/552_Commando.png" 946 | }, 947 | { 948 | "id": "6s1kidUloe8vrTrRtaj8On", 949 | "name": "electronics-detector", 950 | "weapon_type": "unique_ability", 951 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/23Nk2ie06rb3DcZnStryIY/e06226196dd582c905c33fad87dfdd63/Electronics-Detector.png" 952 | }, 953 | { 954 | "id": "633M8tPgKNHXSTWB7czGD7", 955 | "name": "shock-wire", 956 | "weapon_type": "unique_ability", 957 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/129HTNU2A5kIcMj0KZ5UjU/858b60dd0e9b8692e2dc693eded50e14/Shock-Wire.png" 958 | }, 959 | { 960 | "id": "3V9mmNyrWdClHpvgajW7Vb", 961 | "name": "remote-gas-grenade", 962 | "weapon_type": "unique_ability", 963 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/3ZbADU6FxBqdvcA8vCpYhn/6c69d61202364fa420e2a319d817c6f3/Remote-Gas-Grenade.png" 964 | }, 965 | { 966 | "id": "1jYxUQekCIJRQJLsZeRU2u", 967 | "name": "stim-pistol", 968 | "weapon_type": "unique_ability", 969 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7njaeUjJj27iYH27HnH6jn/c5533d2d7191b879c313013f278f5f59/Stim-Pistol.png" 970 | }, 971 | { 972 | "id": "3NPuy5qu8ubwkr9kkWUqdz", 973 | "name": "mpx", 974 | "weapon_type": "primary", 975 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5HFewpAJ8npDDCKFnEadhL/d398bb477d6b56fe41bfdb5862ed31c0/MPX.png" 976 | }, 977 | { 978 | "id": "3y2d4dOdhOGKDc01uC9igS", 979 | "name": "glance-smart-glasses", 980 | "weapon_type": "unique_ability", 981 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/40RkJUEmmBCf7bmfTL8ao1/1d973adfe4d002c94655d9818776fb41/Glance-Smart-Glasses.png" 982 | }, 983 | { 984 | "id": "90Ex0AIJDFcQzHkKhyprN", 985 | "name": "scorpion-evo-3-a1", 986 | "weapon_type": "primary", 987 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/6OdwaLWxcnFvhlVwWbP2Du/4f7e94bdb6d34d5c0aa7b7b147b4092e/Scorpion_EVO_3_A1.png" 988 | }, 989 | { 990 | "id": "291z9C5QiWXQ5CzvQBSNe0", 991 | "name": "f0-12", 992 | "weapon_type": "primary", 993 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4TDWnhbgvLkc6HBWDJp2ST/f50cbd83d6d295ab59f17f7e21d713bc/FO-12.png" 994 | }, 995 | { 996 | "id": "7DXXdxSwrCVMgBPNrOE8Lv", 997 | "name": "grzmot-mine", 998 | "weapon_type": "unique_ability", 999 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/10Md7ccaUO0pE0nCWimeoZ/35dddc67a4141e844d7904051a0314dc/Grzmot-Mine.png" 1000 | }, 1001 | { 1002 | "id": "6KgccwgjLPqJjluHgRiryh", 1003 | "name": "556xi", 1004 | "weapon_type": "primary", 1005 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/2dgpAeAWb3SkZV7rxDbVdQ/fa32323256b7c6f8a1977d3f71e7d4b2/556xi.png" 1006 | }, 1007 | { 1008 | "id": "1FOQiOCNC7d7ZnLTBN8CuK", 1009 | "name": "exothermic-charge", 1010 | "weapon_type": "unique_ability", 1011 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/R5giHT90R2XOMMuUENZeK/840a5a391ed57a0c62208e72258407a7/Exothermic-Charge.png" 1012 | }, 1013 | { 1014 | "id": "1JYfCA53I45QLVbP4b66Ir", 1015 | "name": "heartbeat-sensor", 1016 | "weapon_type": "unique_ability", 1017 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7dPXIadD3D2a3uEqrCPvj2/103ad9d0d3b71adee3b92a5db96fe24d/Heartbeat-Sensor.png" 1018 | }, 1019 | { 1020 | "id": "43JYQ0Gmjgy8D1QMkyThcg", 1021 | "name": "black-eye", 1022 | "weapon_type": "unique_ability", 1023 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1EPfd4xeuMpt5nItOYm2Eb/b59223248a508d205264ece3c3553d36/Black-Eye.png" 1024 | }, 1025 | { 1026 | "id": "47OwyR2cjFodSNwL8dlejO", 1027 | "name": "signal-disruptor", 1028 | "weapon_type": "unique_ability", 1029 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/1M5fsUELbaAzImzMte2ESa/9de588693ec317c87ef1a2021bd43b86/Signal-Disruptor.png" 1030 | }, 1031 | { 1032 | "id": "3af3vtoUw8qRsUtSyDVJVt", 1033 | "name": "ar33", 1034 | "weapon_type": "primary", 1035 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/16U6xEvX8I5xQd9duveBLN/45d22960872cfa3fb6be9eb47fa0be4e/AR33.png" 1036 | }, 1037 | { 1038 | "id": "1WAHBiTy7O9LYcNcPWIZB9", 1039 | "name": "emp-grenade", 1040 | "weapon_type": "unique_ability", 1041 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4p4srpOH4sq55OHryHhn5t/d31728d1432ed28c429ea566caf0e083/EMP-Grenade.png" 1042 | }, 1043 | { 1044 | "id": "5q7YklKN5fREPQk7jcSWiR", 1045 | "name": "g52-tactical-shield", 1046 | "weapon_type": "primary", 1047 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7qmWjGZayvK4t6E80Gvu7g/8b789d6d639744dce100c2cfb9709e6a/G52-Tactical_Shield.png" 1048 | }, 1049 | { 1050 | "id": "5xUtwLz2ADRyf0cfAjgCOj", 1051 | "name": "flash-shield", 1052 | "weapon_type": "unique_ability", 1053 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7EXDIOjPFMhPKZWY5OcEQC/f2df48ebe5673dca7773d81efd940b66/Flash-Shield.png" 1054 | }, 1055 | { 1056 | "id": "2ejX9LEWZ8bnfTRfiYjuAc", 1057 | "name": "k1a", 1058 | "weapon_type": "primary", 1059 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/5mUa2p8WXbiyD71qUI8sGk/ed753b6f0ae30ab5737486dfcf32ee9f/K1A.png" 1060 | }, 1061 | { 1062 | "id": "5gU6SEAl07LepHSaNypGFy", 1063 | "name": "erc-7", 1064 | "weapon_type": "unique_ability", 1065 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/6WbhiNk0evsKWChPneCES6/af08476e2f917878e0326727d2d5fb8a/ERC-7.png" 1066 | }, 1067 | { 1068 | "id": "4rdWW7rPhzJUlVnP5dGs8f", 1069 | "name": "mx4-storm", 1070 | "weapon_type": "primary", 1071 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/4qRh1frGkQZxNyeKA4D6n1/20f89cd1d9953f06207b7340ea77fb17/Mx4_Storm.png" 1072 | }, 1073 | { 1074 | "id": "Rdsy0lubw5hfwmW00FbR9", 1075 | "name": "prisma", 1076 | "weapon_type": "unique_ability", 1077 | "weapon_image_url": "https://staticctf.akamaized.net/J3yJr34U2pZ2Ieem48Dwy9uqj5PNUQTn/7sJYir66zAPq2omSvYeT2u/8fbe3370d32fb5433fb6d3a86d46a1b9/Prisma.png" 1078 | } 1079 | ] 1080 | --------------------------------------------------------------------------------