From 82fa381720e3c2bf3e151e3694e24e203e7bac55 Mon Sep 17 00:00:00 2001 From: jylpah Date: Sun, 10 Sep 2023 18:03:46 +0300 Subject: [PATCH 1/5] add: blitz-utils --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a6b3d9c..081a908 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ beautifulsoup4 lxml xmltodict progress -pytest-asyncio \ No newline at end of file +pytest-asyncio +blitz-utils @ git+https://github.com/Jylpah/blitz-utils \ No newline at end of file From c27d7ce4fc7aabe640d052d5838194ebd14a1111 Mon Sep 17 00:00:00 2001 From: jylpah Date: Sun, 10 Sep 2023 19:58:39 +0300 Subject: [PATCH 2/5] rename legacy blitzutils --- blitzutils.py | 2165 ------------------------------------ blitzutils1.py | 2384 ++++++++++++++++++++++++++++++++++++++++ upload_wotb_replays.py | 684 +++++++----- 3 files changed, 2777 insertions(+), 2456 deletions(-) delete mode 100755 blitzutils.py create mode 100755 blitzutils1.py diff --git a/blitzutils.py b/blitzutils.py deleted file mode 100755 index 5590dc5..0000000 --- a/blitzutils.py +++ /dev/null @@ -1,2165 +0,0 @@ -#!/usr/bin/env python3 -## PYTHON VERSION MUST BE 3.7 OR HIGHER - -import sys, os, json, time, base64, urllib, inspect, hashlib, re, string, random -import asyncio, aiofiles, aiohttp, aiosqlite, lxml -from pathlib import Path -from bs4 import BeautifulSoup -from progress.bar import IncrementalBar -from progress.counter import Counter -from decimal import Decimal -from datetime import datetime - -MAX_RETRIES = 3 -SLEEP = 1.5 - -LOG_LEVELS = {'silent': 0, 'normal': 1, 'verbose': 2, 'debug': 3} -SILENT = 0 -NORMAL = 1 -VERBOSE = 2 -DEBUG = 3 -_log_level = NORMAL -LOG = False -LOGGER = None - -MODE_TANK_STATS = 'tank_stats' -MODE_PLAYER_STATS = 'player_stats' -MODE_PLAYER_ACHIEVEMENTS= 'player_achievements' -MODE_TANKOPEDIA = 'tankopedia' - -# Progress display -_progress_N = 100 -_progress_i = 0 -_progress_id = None -_progress_obj = None - -UMASK = os.umask(0) -os.umask(UMASK) - -## ----------------------------------------------------------- -#### Class ThrottledClientSession(aiohttp.ClientSession) -# -# Rate-limited async http client session -# -# Inherits aiohttp.ClientSession -## ----------------------------------------------------------- - -from typing import Optional, Union -import aiohttp -import asyncio -import time -import logging -import re - -class ThrottledClientSession(aiohttp.ClientSession): - """Rate-throttled client session class inherited from aiohttp.ClientSession)""" - - def __init__(self, rate_limit: float = 0, filters: list[str] = list() , - limit_filtered: bool = False, re_filter: bool = False, *args,**kwargs) -> None: - assert isinstance(rate_limit, (int, float)), "rate_limit has to be float" - assert isinstance(filters, list), "filters has to be list" - assert isinstance(limit_filtered, bool),"limit_filtered has to be bool" - assert isinstance(re_filter, bool), "re_filter has to be bool" - - super().__init__(*args,**kwargs) - - self.rate_limit : float - self._fillerTask : Optional[asyncio.Task] = None - self._queue : Optional[asyncio.Queue] = None - self._start_time : float = time.time() - self._count : int = 0 - self._limit_filtered: bool = limit_filtered - self._re_filter : bool = re_filter - self._filters : list[Union[str, re.Pattern]] = list() - - if re_filter: - for filter in filters: - self._filters.append(re.compile(filter)) - else: - for filter in filters: - self._filters.append(filter) - self.set_rate_limit(rate_limit) - - - def _get_sleep(self) -> float: - if self.rate_limit > 0: - return 1/self.rate_limit - return 0 - - - def get_rate(self) -> float: - """Return rate of requests""" - return self._count / (time.time() - self._start_time) - - - def get_stats(self) -> dict[str, float]: - """Get session statistics""" - res = {'rate' : self.get_rate(), 'rate_limit': self.rate_limit, 'count' : self._count } - return res - - - def get_stats_str(self) -> str: - """Print session statistics""" - return f"rate limit: {str(self.rate_limit if self.rate_limit is not None else '-')} \ - rate: {0:.1f}.format(self.get_rate()) requests: {str(self._count)}" - - - def reset_counters(self) -> dict[str, float]: - """Reset rate counters and return current results""" - res = self.get_stats() - self._start_time = time.time() - self._count = 0 - return res - - - def set_rate_limit(self, rate_limit: float = 0) -> float: - assert rate_limit is not None, "rate_limit must not be None" - assert isinstance(rate_limit, (int,float)) and rate_limit >= 0, "rate_limit has to be type of 'float' >= 0" - - self.rate_limit = rate_limit - if rate_limit > 0: - self._queue = asyncio.Queue(int(rate_limit)+1) - if self._fillerTask is not None: - self._fillerTask.cancel() - self._fillerTask = asyncio.create_task(self._filler()) - return self.rate_limit - - - async def close(self) -> None: - """Close rate-limiter's "bucket filler" task""" - # DEBUG - logging.debug(self.get_stats_str()) - try: - if self._fillerTask is not None: - self._fillerTask.cancel() - await asyncio.wait_for(self._fillerTask, timeout=0.5) - except asyncio.TimeoutError as err: - logging.error(str(err)) - await super().close() - - - async def _filler(self) -> None: - """Filler task to fill the leaky bucket algo""" - try: - if self._queue is None: - return None - logging.debug('SLEEP: ' + str(self._get_sleep())) - updated_at = time.monotonic() - extra_increment : float = 0 - for i in range(0, self._queue.maxsize): - await self._queue.put(i) - while True: - if not self._queue.full(): - now = time.monotonic() - increment = self.rate_limit * (now - updated_at) - items_2_add = int(min(self._queue.maxsize - self._queue.qsize(), int(increment + extra_increment))) - extra_increment = (increment + extra_increment) % 1 - for i in range(0,items_2_add): - self._queue.put_nowait(i) - updated_at = now - await asyncio.sleep(self._get_sleep()) - except asyncio.CancelledError: - logging.debug('Cancelled') - except Exception as err: - logging.error(str(err)) - return None - - - async def _request(self, *args,**kwargs) -> aiohttp.ClientResponse: - """Throttled _request()""" - if self._queue is not None and self.is_limited(*args): - await self._queue.get() - self._queue.task_done() - self._count += 1 - return await super()._request(*args,**kwargs) - - - def is_limited(self, *args: str) -> bool: - """Check wether the rate limit should be applied""" - try: - url: str = args[1] - for filter in self._filters: - if isinstance(filter, re.Pattern) and filter.match(url) is not None: - return self._limit_filtered - elif isinstance(filter, str) and url.startswith(filter): - return self._limit_filtered - - return not self._limit_filtered - except Exception as err: - logging.error(str(err)) - return True - -# ----------------------------------------------------------- -# Class AsyncLogger() -# ----------------------------------------------------------- - -class AsyncLogger(): - """Async file logger""" - - def __init__(self) -> None: - self._queue = asyncio.Queue() - self._task = None - self._file = None - - async def open(self, logfn: str = None): - """Set logging to file""" - if logfn is None: - logfn = 'LOG_' + _randomword(6) + '.log' - try: - self._file = await aiofiles.open(logfn, mode='a') - self._task = asyncio.create_task(self.logger()) - return True - except Exception as err: - error('Error opening file: ' + logfn, err) - self._file = None - return False - - async def logger(self): - """Async file logger""" - if self._file is None: - error('No log file defined') - return False - while True: - try: - msg = await self._queue.get() - await self._file.write(msg + '\n') - self._queue.task_done() - except asyncio.CancelledError as err: - return None - except Exception as err: - error(exception=err) - - def log(self, msg: str = ''): - self._queue.put_nowait(msg) - - async def close(self): - try: - # empty queue & close - await self._queue.join() - self._task.cancel() - self._file.close() - except Exception as err: - error('Error closing log file', err) - return None - -# ----------------------------------------------------------- -# Utils -# ----------------------------------------------------------- - - -def set_debug(debug: bool): - global _log_level - _log_level = DEBUG - - -def set_verbose(verbose: bool): - global _log_level - _log_level = VERBOSE - - -def set_silent(silent: bool): - global _log_level - _log_level = SILENT - - -def set_log_level_normal(): - global _log_level - _log_level = NORMAL - - -def is_debug() -> bool: - return _log_level == DEBUG - - -def is_verbose() -> bool: - return _log_level == VERBOSE - - -def is_normal() -> bool: - return _log_level == NORMAL - - -def is_silent() -> bool: - return _log_level == SILENT - - -def set_log_level(silent: bool, verbose: bool, debug: bool): - global _log_level - _log_level = NORMAL - if silent: _log_level = SILENT - if verbose: _log_level = VERBOSE - if debug: _log_level = DEBUG - - -def get_log_level() -> int: - return _log_level - - -def get_log_level_str() -> str: - for log_level in LOG_LEVELS: - if _log_level == LOG_LEVELS[log_level]: - return log_level - error('Unknown log level: ' + str(_log_level)) - - -async def set_file_logging(logfn=None, add_timestamp=False): - """Set logging to file""" - global LOG, LOGGER - LOG = True - if logfn is None: - logfn = 'LOG_' + _randomword(6) - else: - dateTimeObj = datetime.now() - timestampStr = dateTimeObj.strftime("%y%m%d%H%M%S") - logfn = logfn + '_' + timestampStr - logfn = logfn + '.log' - try: - LOGGER = AsyncLogger() - await LOGGER.open(logfn) - except Exception as err: - error('Error starting logger: ' + logfn, err) - LOG = False - LOGGER = None - return LOG - - -async def close_file_logging(): - global LOG, LOGGER - LOG = False - await LOGGER.close() - LOGGER = None - - -def _randomword(length): - letters = string.ascii_lowercase - return ''.join(random.choice(letters) for i in range(length)) - - -def verbose(msg="", id=None) -> bool: - """Print a message""" - return _print_log_msg('', msg, exception=None, id=id, print_msg=(_log_level >= VERBOSE)) - - -def verbose_std(msg="", id=None) -> bool: - """Print a message""" - return _print_log_msg('', msg, exception=None, id=id, print_msg=(_log_level >= NORMAL)) - - -def warning(msg="", id=None, force: bool = False) -> bool: - """Print a warning message""" - return _print_log_msg('', 'Warning: ' + msg, None, id, print_msg=(force or (_log_level >= NORMAL))) - - -def debug(msg="", id=None, exception=None, force: bool = False) -> bool: - """print a conditional debug message""" - if (_log_level >= DEBUG) or force: - return _print_log_msg('DEBUG', msg, exception, id) - return False - - -def error(msg="", exception=None, id=None) -> bool: - """Print an error message""" - return _print_log_msg('ERROR', msg, exception, id) - - -def log(msg="", id=None, exception=None) -> bool: - """print a conditional debug message""" - return _print_log_msg('LOG', msg=msg, exception=exception, id=id, print_msg=(_log_level >= DEBUG)) - - -## Copy with pride: https://stackoverflow.com/questions/2203424/python-how-to-retrieve-class-information-from-a-frame-object -def get_class_from_frame(fr): - args, _, _, value_dict = inspect.getargvalues(fr) - # we check the first parameter for the frame function is - # named 'self' - if len(args) and args[0] == 'self': - # in that case, 'self' will be referenced in value_dict - instance = value_dict.get('self', None) - if instance: - # return its class - return getattr(instance, '__class__', None) - # return None otherwise - return None - - -def _print_log_msg(prefix: str = 'LOG', msg:str = '', - exception: Exception | None = None, - id: int|None = None, print_msg : bool = True): - # Use empty prefix to determine standard verbose messages - if not (print_msg or LOG): - return False - retval = False - if prefix != '': - curframe = inspect.currentframe() - calframe = inspect.getouterframes(curframe) - class_name = get_class_from_frame(curframe) - caller = calframe[2].function - if class_name is not None: - prefix = f'{prefix}: {class_name}.{caller}' - else: - prefix = prefix + ': ' + caller - prefix = prefix + '(): ' - - if id is not None: - prefix = f'{prefix} [ {id}]: ' - - exception_msg = '' - if (exception is not None) and isinstance(exception, Exception): - exception_msg = f' : Exception: {type(exception)} : {exception}' - - msg = f'{prefix}{msg}{exception_msg}' - if print_msg: - print(msg) - retval = True - if _log_msg(msg): - retval = True - return retval - - -def _log_msg(msg :str=''): - if LOG and (LOGGER is not None): - LOGGER.log(msg) - return True - return False - - -def set_progress_step(n: int): - """Set the frequency of the progress dots. The bigger 'n', the fewer dots""" - global _progress_N, _progress_i - if n > 0: - _progress_N = n - _progress_i = 0 - return - - -def get_progress_step(): - """Get the frequency of the progress dots. The bigger 'n', the fewer dots""" - return _progress_N - - -def set_progress_bar(heading: str, max_value: int, step: int = None, slow: bool = False, id: str = None): - global _progress_obj, _progress_N, _progress_i, _progress_id - _progress_id = id - if step is None: - _progress_N = int(max_value / 1000) if (max_value > 1000) else 2 - else: - _progress_N = step - if _progress_obj is not None: - finish_progress_bar() - if slow: - _progress_obj = SlowBar(heading, max=max_value) - else: - _progress_obj = IncrementalBar(heading, max=max_value, suffix='%(index)d/%(max)d %(percent)d%%') - _progress_i = 0 - - _log_msg(heading + str(max_value)) - return - - -def set_counter(heading: str, step = None): - global _progress_obj, _progress_i - _progress_i = 0 - if _progress_obj is not None: - finish_progress_bar() - _progress_obj = Counter(heading + ': ') - if step is not None: - set_progress_step(step) - return - - -def print_progress(force = False, id : str = None) -> bool: - """Print progress bar/dots. Returns True if the dot is being printed.""" - global _progress_i - - _progress_i += 1 - if ((_progress_i % _progress_N) == 0): - if (_log_level > SILENT) and ( force or (_log_level < DEBUG ) ): - if (_progress_obj is not None): - if (_progress_id == id): - _progress_obj.next(_progress_N) - return True - else: - return False - else: - print('.', end='', flush=True) - return True - return False - - -def finish_progress_bar(): - """Finish and close progress bar object""" - global _progress_obj - - # print_nl = True - if _progress_obj is not None: - # if isinstance(_progress_obj, Counter): - # print_nl = False - _progress_obj.finish() - # if print_nl: - print_new_line() - _progress_obj = None - return None - - -def wait(sec : int): - for i in range(0, sec): - i=i ## to get rid of the warning... - time.sleep(1) - print_progress(True) - print('', flush=True) - - -def print_new_line(force = False): - if (_log_level > SILENT) and ( force or (_log_level < DEBUG ) ): - print('', flush=True) - _log_msg('') - - -def NOW() -> int: - return int(time.time()) - - -def get_date_str(timestamp:int = NOW(), date_format = '%Y%m%d_%H%M%S'): - """Return YYYYMMDD_HHmm date string""" - try: - ts = datetime.fromtimestamp(timestamp) - return ts.strftime(date_format) - except Exception as err: - error(exception=err) - return None - - -def rebase_file_args(current_dir : str, files: list[str]) -> list[str]: - """Rebase file command line params after moving working dir to the script's dir""" - assert isinstance(files, list), "files has to be a list" - - if (files[0] == '-') or (files[0] == 'db:'): - return [ files[0] ] - else: - return [ os.path.join(current_dir, fn) for fn in files ] - - -async def read_int_list(filename: str) -> list[int]: - """Read file to a list and return list of integers in the input file""" - - input_list = [] - try: - async with aiofiles.open(filename) as fp: - async for line in fp: - try: - input_list.append(int(line)) - except (ValueError, TypeError) as err: - pass - except Exception as err: - error('Unexpected error when reading file: ' + filename, err) - return input_list - - -async def save_JSON(filename: str, json_data: dict, sort_keys = False, pretty = True) -> bool: - """Save JSON data into file""" - try: - dirname = os.path.dirname(filename) - if (dirname != '') and not os.path.isdir(dirname): - os.makedirs(dirname, 0o770-UMASK) - async with aiofiles.open(filename,'w', encoding="utf8") as outfile: - if pretty: - await outfile.write(json.dumps(json_data, ensure_ascii=False, indent=4, sort_keys=sort_keys)) - else: - await outfile.write(json.dumps(json_data, ensure_ascii=False, sort_keys=sort_keys)) - return True - except Exception as err: - error('Error saving JSON', err) - return False - - -async def open_JSON(filename: str, chk_JSON_func = None) -> dict: - try: - async with aiofiles.open(filename) as fp: - json_data = json.loads(await fp.read()) - if (chk_JSON_func is None): - debug("JSON file content not checked: " + filename) - return json_data - elif chk_JSON_func(json_data): - debug("JSON File is valid: " + filename) - return json_data - else: - debug('JSON File has invalid content: ' + filename) - except Exception as err: - error('Unexpected error when reading file: ' + filename, err) - return None - - -async def get_url_JSON(session: aiohttp.ClientSession, url: str, chk_JSON_func = None, max_tries = MAX_RETRIES) -> dict: - """Retrieve (GET) an URL and return JSON object""" - if session is None: - error('Session must be initialized first') - sys.exit(1) - if url is None: - return None - - # To avoid excessive use of servers - for retry in range(1,max_tries+1): - try: - async with session.get(url) as resp: - if resp.status == 200: - debug('HTTP request OK') - json_resp = await resp.json() - if (chk_JSON_func is None) or chk_JSON_func(json_resp): - # debug("Received valid JSON: " + str(json_resp)) - return json_resp - # Sometimes WG API returns JSON error even a retry gives valid JSON - elif resp.status == 407: - json_resp_err = await resp.json() - error('WG API returned 407: ' + json_resp_err['error']['message']) - if retry == max_tries: - break - debug('Retrying URL [' + str(retry) + '/' + str(max_tries) + ']: ' + url ) - await asyncio.sleep(SLEEP) - - except aiohttp.ClientError as err: - debug("Could not retrieve URL: " + url, exception=err) - except asyncio.CancelledError as err: - debug('Queue gets cancelled while still working.', exception=err) - except Exception as err: - debug('Unexpected Exception', exception=err) - debug("Could not retrieve URL: " + url) - return None - - -def bld_dict_hierarcy(d : dict, key : str, value) -> dict: - """Build hierarcical dict based on multi-level key separated with """ - try: - key_hier = key.split('.') - sub_key = key_hier.pop(0) - if len(key_hier) == 0: - d[sub_key] = value - elif sub_key not in d: - d[sub_key] = bld_dict_hierarcy({}, '.'.join(key_hier), value) - else: - d[sub_key] = bld_dict_hierarcy(d[sub_key], '.'.join(key_hier), value) - - return d - except KeyError as err: - error('Key not found', err) - except Exception as err: - error('Unexpected Exception', err) - return None - -def get_JSON_keypath(keypath: str, key: str): - if keypath is None: - return key - else: - return '.'.join([keypath, key]) - -def get_JSON_value(json, key : str | None = None, keys : list | None = None, - keypath : str | None = None): - if (keys is None): - if key is not None: - keys = key.split('.') - else: - return json - if len(keys) == 0: - return json - key = keys.pop(0) - if type(json) == dict: - if key in json: - return get_JSON_value(json[key], keys=keys, keypath=get_JSON_keypath(keypath, key)) - else: - raise KeyError('Key: '+ get_JSON_keypath(keypath, key) + ' not found') - if type(json) == list: - p = re.compile(r'^\[(\d+)\]$') - m = p.match(key) - if len(m.groups()) != 1: - raise KeyError('Invalid key given: ' + get_JSON_keypath(keypath, key)) - ndx = m.group(1) - try: - return get_JSON_value(json[ndx], keys=keys, keypath=get_JSON_keypath(keypath, key)) - except IndexError: - raise KeyError('JSON array index out of range: ' + get_JSON_keypath(keypath, key)) - raise KeyError('Key not found: ' + get_JSON_keypath(keypath, keys[0])) - - -def sort_dict(d: dict, number: bool = False) -> dict: - """Sort a dict by keys""" - if number: - return dict(sorted(d.items(), key=lambda item: int(item[0]))) - else: - return dict(sorted(d.items(), key=lambda item: str(item[0]))) - - -# ----------------------------------------------------------- -# Class SlowBar -# ----------------------------------------------------------- - -class SlowBar(IncrementalBar): - suffix = '%(index)d/%(max)d %(percent)d%% ETA %(remaining_hours).0f h %(remaining_mins).0f mins' - @property - def remaining_hours(self): - return self.eta // 3600 - - @property - def remaining_mins(self): - return (self.eta - (self.eta // 3600)*3600) // 60 - - -# ----------------------------------------------------------- -# Class StatsNotFound -# ----------------------------------------------------------- - -class StatsNotFound(Exception): - pass - - -# ----------------------------------------------------------- -# Class WG -# ----------------------------------------------------------- - -class WG: - - URL_WG_CLAN_INFO = 'clans/info/?application_id=' - # URL_WG_PLAYER_TANK_LIST = 'tanks/stats/?fields=tank_id%2Clast_battle_time&application_id=' - # URL_WG_PLAYER_TANK_LIST = 'tanks/stats/?fields=account_id%2Ctank_id%2Clast_battle_time%2Cbattle_life_time%2Call&application_id=' - URL_WG_PLAYER_TANK_STATS = 'tanks/stats/?application_id=' - URL_WG_ACCOUNT_ID = 'account/list/?fields=account_id%2Cnickname&application_id=' - URL_WG_PLAYER_STATS = 'account/info/?application_id=' - URL_WG_PLAYER_ACHIEVEMENTS = 'account/achievements/?application_id=' - CACHE_DB_FILE = '.blitzutils_cache.sqlite3' - CACHE_GRACE_TIME = 30*24*3600 # 30 days cache - - # sql_create_player_stats_tbl = """CREATE TABLE IF NOT EXISTS player_stats ( - # account_id INTEGER NOT NULL, - # date INTEGER NOT NULL, - # stat TEXT, - # value FLOAT - # ); """ - - # sql_create_player_tank_stats_tbl = """CREATE TABLE IF NOT EXISTS player_tank_stats ( - # account_id INTEGER NOT NULL, - # tank_id INTEGER DEFAULT NULL, - # date INTEGER NOT NULL, - # stat TEXT, - # value FLOAT - # ); """ - - # sql_select_player_stats = """SELECT value FROM player_stats ORDERBY date ASC - # WHERE account_id = {} AND stat = {} - # AND date >= {} LIMIT 1;""" - - # TANK_STATS - SQL_TANK_STATS_TBL = 'tank_stats' - - SQL_TANK_STATS_CREATE_TBL = 'CREATE TABLE IF NOT EXISTS ' + SQL_TANK_STATS_TBL + \ - """ ( account_id INTEGER NOT NULL, - tank_id INTEGER NOT NULL, - update_time INTEGER NOT NULL, - stats TEXT, - PRIMARY KEY (account_id, tank_id) )""" - - SQL_TANK_STATS_COUNT = 'SELECT COUNT(*) FROM ' + SQL_TANK_STATS_TBL - - SQL_TANK_STATS_UPDATE = 'REPLACE INTO ' + SQL_TANK_STATS_TBL + '(account_id, tank_id, update_time, stats) VALUES(?,?,?,?)' - - # PLAYER_STATS - SQL_PLAYER_STATS_TBL = 'player_stats' - - SQL_PLAYER_STATS_CREATE_TBL = 'CREATE TABLE IF NOT EXISTS ' + SQL_PLAYER_STATS_TBL + \ - """ ( account_id INTEGER PRIMARY KEY, - update_time INTEGER NOT NULL, - stats TEXT)""" - - SQL_PLAYER_STATS_COUNT = 'SELECT COUNT(*) FROM ' + SQL_PLAYER_STATS_TBL - - SQL_PLAYER_STATS_UPDATE = 'REPLACE INTO ' + SQL_PLAYER_STATS_TBL + '(account_id, update_time, stats) VALUES(?,?,?)' - - SQL_PLAYER_STATS_CACHED = 'SELECT * FROM ' + SQL_PLAYER_STATS_TBL + ' WHERE account_id = ? AND update_time > ?' - - # PLAYER_ACHIEVEMENTS - SQL_PLAYER_ACHIEVEMENTS_TBL = 'player_achievements' - - SQL_PLAYER_ACHIEVEMENTS_CREATE_TBL = 'CREATE TABLE IF NOT EXISTS ' + SQL_PLAYER_ACHIEVEMENTS_TBL + \ - """ ( account_id INTEGER PRIMARY KEY, - update_time INTEGER NOT NULL, - stats TEXT)""" - - SQL_PLAYER_ACHIEVEMENTS_COUNT = 'SELECT COUNT(*) FROM ' + SQL_PLAYER_ACHIEVEMENTS_TBL - - SQL_PLAYER_ACHIEVEMENTS_CACHED = 'SELECT * FROM ' + SQL_PLAYER_ACHIEVEMENTS_TBL + ' WHERE account_id = ? AND update_time > ?' - - SQL_PLAYER_ACHIEVEMENTS_UPDATE = 'REPLACE INTO ' + SQL_PLAYER_ACHIEVEMENTS_TBL + '(account_id, update_time, stats) VALUES(?,?,?)' - - - SQL_TABLES = [ SQL_PLAYER_STATS_TBL, SQL_TANK_STATS_TBL, SQL_PLAYER_ACHIEVEMENTS_TBL ] - - SQL_CHECK_TABLE_EXITS = """SELECT name FROM sqlite_master WHERE type='table' AND name=?""" - - SQL_PRUNE_CACHE = """DELETE from {} WHERE update_time < {}""" - -# Default data. Please use the latest maps.json - - maps = { - "Random": "Random map", - "amigosville": "Falls Creek", - "asia": "Lost Temple", - "canal": "Canal", - "canyon": "Canyon", - "desert_train": "Desert Sands", - "erlenberg": "Middleburg", - "faust": "Faust", - "fort": "Macragge", - "grossberg": "Dynasty's Pearl", - "himmelsdorf": "Himmelsdorf", - "italy": "Vineyards", - "karelia": "Rockfield", - "karieri": "Copperfield", - "lake": "Mirage", - "lumber": "Alpenstadt", - "malinovka": "Winter Malinovka", - "medvedkovo": "Dead Rail", - "milbase": "Yamato Harbor", - "mountain": "Black Goldville", - "north": "North", - "ordeal": "Trial by Fire", - "pliego": "Castilla", - "port": "Port Bay", - "rock": "Mayan Ruins", - "rudniki": "Mines", - "savanna": "Oasis Palms", - "skit": "Naval Frontier", - "test": "World of Ducks", - "tutorial": "Proving Grounds" - } - - tanks = None - tanks_by_tier = None - - NATION = [ 'ussr', 'germany', 'usa', 'china', 'france', 'uk', 'japan', 'other', 'european'] - NATION_STR = [ 'Soviet', 'Germany', 'USA', 'China', 'France', 'UK', 'Japan', 'Other', 'European'] - NATION_ID = { - 'ussr' : 0, - 'germany' : 1, - 'usa' : 2, - 'china' : 3, - 'france' : 4, - 'uk' : 5, - 'japan' : 6, - 'other' : 7, - 'european' : 8 - } - - TANK_TYPE = [ 'lightTank', 'mediumTank', 'heavyTank', 'AT-SPG' ] - TANK_TYPE_STR = [ 'Light Tank', 'Medium Tank', 'Heavy Tank', 'Tank Destroyer' ] - - TANK_TYPE_ID = { - 'lightTank' : 0, - 'mediumTank' : 1, - 'heavyTank' : 2, - 'AT-SPG' : 3 - } - - URL_WG_SERVER = { - 'eu' : 'https://api.wotblitz.eu/wotb/', - 'ru' : 'https://api.wotblitz.ru/wotb/', - 'na' : 'https://api.wotblitz.com/wotb/', - 'asia' : 'https://api.wotblitz.asia/wotb/', - 'china' : None - } - - ACCOUNT_ID_SERVER= { - 'ru' : range(0, int(5e8)), - 'eu' : range(int(5e8), int(10e8)), - 'na' : range(int(1e9),int(2e9)), - 'asia' : range(int(2e9),int(31e8)), - 'china' : range(int(31e8),int(4e9)) - } - - def __init__(self, WG_app_id : str = None, tankopedia_fn : str = None, maps_fn : str = None, - stats_cache: bool = False, rate_limit: int = 10, global_rate_limit = True): - - self.WG_app_id = WG_app_id - self.load_tanks(tankopedia_fn) - WG.tanks = self.tanks - self.global_rate_limit = global_rate_limit - - if (maps_fn is not None): - if os.path.exists(maps_fn) and os.path.isfile(maps_fn): - try: - with open(maps_fn, 'rt', encoding='utf8') as f: - WG.maps = json.loads(f.read()) - except Exception as err: - error('Could not read maps file: ' + maps_fn + '\n' + str(err)) - else: - verbose('Could not find maps file: ' + maps_fn) - if self.WG_app_id is not None: - headers = {'Accept-Encoding': 'gzip, deflate'} - if self.global_rate_limit: - self.session = ThrottledClientSession(rate_limit=rate_limit, headers=headers) - else: - self.session = dict() - for server in list(self.URL_WG_SERVER)[:4]: # China (5th) server is unknown, thus excluded - self.session[server] = ThrottledClientSession(rate_limit=rate_limit, headers=headers) - debug('WG aiohttp session initiated') - else: - self.session = None - debug('WG aiohttp session NOT initiated') - - # cache - self.cache = None - self.statsQ = None - self.stat_saver_task = None - if stats_cache: - try: - self.statsQ = asyncio.Queue() - self.stat_saver_task = asyncio.create_task(self.stat_saver()) - except Exception as err: - error(exception=err) - sys.exit(1) - - - async def close(self): - # close stats queue - try: - if self.statsQ is not None: - debug('WG.close(): Waiting for statsQ to finish') - await self.statsQ.join() - debug('WG.close(): statsQ finished') - self.stat_saver_task.cancel() - debug('statsCacheTask cancelled') - await self.stat_saver_task - - # close cacheDB - if self.cache is not None: - # prune old cache records - await self.cleanup_cache() - await self.cache.commit() - await self.cache.close() - - if self.session is not None: - if self.global_rate_limit: - await self.session.close() - else: - for server in self.session: - await self.session[server].close() - except Exception as err: - error(exception=err) - - - # Class methods ---------------------------------------------------------- - - @classmethod - def get_server(cls, account_id: int) -> str: - """Get Realm/server of an account based on account ID""" - if account_id >= 1e9: - if account_id >= 31e8: - debug('Chinese account/server: no stats available') - return None - if account_id >= 2e9: - return 'asia' - return 'na' - else: - if account_id < 5e8: - return 'ru' - return 'eu' - return None - - - @classmethod - def update_maps(cls, map_data: dict): - """Update maps data""" - cls.maps = map_data - return None - - - @classmethod - def get_map(self, map_str: str) -> str: - """Return map name from short map string in replays""" - try: - return self.maps[map_str] - except: - error('Map ' + map_str + ' not found') - return None - - - @classmethod - def get_tank_user_strs(self) -> str: - return self.tanks["userStr"].keys() - - - @classmethod - def chk_JSON(cls, json_obj, check = None) -> bool: - try: - if (check is None): - # nothing to check - return True - if (check == 'tank_stats'): - if cls.chk_JSON_tank_stats(json_obj): - return True - else: - debug('Checking tank list JSON failed.') - return False - elif (check == 'player_stats'): - if cls.chk_JSON_player_stats(json_obj): - return True - else: - debug('Checking player JSON failed.') - return False - elif (check == 'tankopedia'): - if cls.chk_JSON_tankopedia(json_obj): - return True - else: - debug('Checking tank JSON failed.') - return False - elif (check == 'account_id'): - if cls.chk_JSON_get_account_id(json_obj): - return True - else: - debug('Checking account_id JSON failed.') - return False - except (TypeError, ValueError) as err: - debug(str(err)) - return False - - - @classmethod - def chk_JSON_status(cls, json_resp: dict) -> bool: - try: - if (json_resp is None) or ('status' not in json_resp) or (json_resp['status'] is None): - return False - elif (json_resp['status'] == 'ok') and ('data' in json_resp): - return True - elif json_resp['status'] == 'error': - if ('error' in json_resp): - error_msg = 'Received an error' - if ('message' in json_resp['error']) and (json_resp['error']['message'] is not None): - error_msg = error_msg + ': ' + json_resp['error']['message'] - if ('value' in json_resp['error']) and (json_resp['error']['value'] is not None): - error_msg = error_msg + ' Value: ' + json_resp['error']['value'] - debug(error_msg) - return False - else: - error('Unknown status-code: ' + json_resp['status']) - return False - except KeyError as err: - error('No field found in JSON data', err) - except Exception as err: - error("JSON format error", err) - return False - - - @classmethod - def chk_JSON_get_account_id(cls, json_resp: dict) -> bool: - try: - if cls.chk_JSON_status(json_resp): - if (json_resp['meta']['count'] > 0): - return True - except KeyError as err: - error('Key not found', err) - except Exception as err: - error(exception=err) - return False - - # @classmethod - # def chkJSONplayer(cls, json_resp: dict) -> bool: - # """"Check String for being a valid Player JSON file""" - # try: - # if cls.chk_JSON_status(json_resp): - # if int(json_resp[0]['account_id']) > 0: - # return True - # except KeyError as err: - # error('Key not found', err) - # except: - # debug("JSON check failed") - # return False - - @classmethod - def chk_JSON_tankopedia(cls, json_resp: dict) -> bool: - """"Check String for being a valid Tankopedia JSON file""" - try: - if cls.chk_JSON_status(json_resp): - if int(json_resp[0]['tank_id']) > 0: - return True - except KeyError as err: - error('Key not found', err) - except: - debug("JSON check failed") - return False - - - @classmethod - def chk_JSON_player_stats(cls, json_resp: dict) -> bool: - """"Check String for being a valid Tank JSON file""" - try: - if cls.chk_JSON_status(json_resp): - for acc in json_resp['data']: - if json_resp['data'][acc] is not None: - return True - except KeyError as err: - error('Key not found', err) - except: - debug("JSON check failed") - return False - - - @classmethod - def chk_JSON_tank_stats(cls, json_resp: dict) -> bool: - """"Check String for being a valid Tank list JSON file""" - try: - if cls.chk_JSON_status(json_resp): - if ('data' in json_resp) and (len(json_resp['data']) > 0): - debug('JSON tank list check OK') - return True - except Exception as err: - error('JSON check FAILED: ' + str(json_resp) ) - error(exception=err) - return False - - - # Methods -------------------------------------------------- - def load_tanks(self, tankopedia_fn: str): - """Load tanks from tankopedia JSON""" - if tankopedia_fn is None: - return False - try: - with open(tankopedia_fn, 'rt', encoding='utf8') as f: - self.tanks = json.loads(f.read()) - self.tanks['tankStr'] = dict() - p = re.compile('.+_short$') - for usr_str in self.tanks['userStr']: - if p.match(usr_str) is None: - self.tanks['tankStr'][self.tanks['userStr'][usr_str]] = usr_str - self.tanks_by_tier = dict() - for tier in range(1,11): - self.tanks_by_tier[str(tier)] = list() - for tank in self.tanks['data'].values(): - self.tanks_by_tier[str(tank['tier'])].append(tank['tank_id']) - return True - except Exception as err: - error('Could not read tankopedia: ' + tankopedia_fn, err) - return False - - - def get_replay_filename(self, replay: dict): - try: - summary = replay['data']['summary'] - timestamp = int(summary['battle_start_timestamp']) - timestamp = get_date_str(timestamp, date_format='%Y%m%d_%H%M') - player = summary['player_name'] - arena_id = int(summary['arena_unique_id']) - vehicle = self.name2tank_str(summary['vehicle']) - vehicle.replace(' ','_') - - return '{:s}_{:s}_{:s}_{:d}.wotbreplay.json'.format(timestamp, player, vehicle, arena_id) - except Exception as err: - error('Unexpected Exception', err) - return None - - def get_map_user_strs(self) -> str: - return self.maps.keys() - - - def tank_str2name(self, tank_str: str) -> str: - """Return tank name from short tank string in replays""" - try: - return self.tanks["userStr"][tank_str] - except: - error('Tank ' + tank_str + ' not found') - return tank_str - - - def name2tank_str(self, name: str) -> str: - """Return shorttank string from tank string in replays""" - try: - return self.tanks["tankStr"][name] - except: - debug('Tank ' + name + ' not found') - return name - - - def get_tanks_by_tier(self, tier: int) -> list(): - """Returns tank_ids by tier""" - try: - return self.tanks_by_tier[str(tier)] - except KeyError as err: - error('Invalid tier', err) - return None - - - def get_url_clan_info(self, server: str, clan_id: int) -> str: - try: - if server is None: - return None - return self.URL_WG_SERVER[server] + self.URL_WG_CLAN_INFO + self.WG_app_id + '&clan_id=' + str(clan_id) - except Exception as err: - if (server is None) or (server.lower() not in WG.ACCOUNT_ID_SERVER.keys()): - error('No server name or invalid server name given: ' + server if (server != None) else '') - error('Available servers: ' + ', '.join(WG.ACCOUNT_ID_SERVER.keys())) - error(exception=err) - return None - - - def get_url_player_tank_list(self, account_id: int) -> str: - return self.get_url_player_tanks_stats(account_id, fields='tank_id') - - - def get_url_player_tanks_stats(self, account_id: int, tank_ids: list = [], fields: list = []) -> str: - server = self.get_server(account_id) - if server is None: - return None - if (tank_ids is not None) and (len(tank_ids) > 0): - tank_id_str= '&tank_id=' + '%2C'.join([ str(x) for x in tank_ids]) - else: - # emtpy tank-id list returns all the player's tanks - tank_id_str = '' - - if (fields is not None) and (len(fields) > 0): - field_str = '&fields=' + '%2C'.join(fields) - else: - # return all the fields - field_str = '' - - return self.URL_WG_SERVER[server] + self.URL_WG_PLAYER_TANK_STATS + self.WG_app_id + '&account_id=' + str(account_id) + tank_id_str + field_str - - - def get_url_player_stats(self, account_id, fields) -> str: - try: - server = self.get_server(account_id) - if server is None: - return None - if (fields is not None) and (len(fields) > 0): - field_str = '&fields=' + '%2C'.join(fields) - else: - # return all the fields - field_str = '' - - return self.URL_WG_SERVER[server] + self.URL_WG_PLAYER_STATS + self.WG_app_id + '&account_id=' + str(account_id) + field_str - except Exception as err: - if (server is None): - error('Invalid account_id') - error(exception=err) - return None - - - def get_url_player_achievements(self, account_ids: list, fields : str = 'max_series') -> str: - try: - # assumming that all account_ids are from the same server. This has to be taken care. - server = self.get_server(account_ids[0]) - - if server is None: - return None - account_ids_str = '%2C'.join(str(id) for id in account_ids) - if (fields is not None) and (len(fields) > 0): - field_str = '&fields=' + '%2C'.join(fields) - else: - # return all the fields - field_str = '' - - return self.URL_WG_SERVER[server] + self.URL_WG_PLAYER_ACHIEVEMENTS + self.WG_app_id + '&account_id=' + account_ids_str + field_str - except Exception as err: - if (server is None): - error('Invalid account_id') - error(exception=err) - return None - - - def get_url_account_id(self, nickname, server) -> int: - try: - return self.URL_WG_SERVER[server] + self.URL_WG_ACCOUNT_ID + self.WG_app_id + '&search=' + urllib.parse.quote(nickname) - except Exception as err: - if nickname is None or len(nickname) == 0: - error('No nickname given') - if (server is None) or (server.lower() not in WG.ACCOUNT_ID_SERVER.keys()): - error('No server name or invalid server name given: ' + server if (server != None) else '') - error('Available servers: ' + ', '.join(WG.ACCOUNT_ID_SERVER.keys())) - error(exception=err) - return None - - - def url_get_server(self, url: str) -> str: - """Decode WG server from the URL""" - try: - for server in self.session: - if url.startswith(self.URL_WG_SERVER[server]): - return server - except Exception as err: - error(exception=err) - return 'eu' # default - - - def print_request_stats(self): - """Print session statics""" - if self.global_rate_limit: - verbose_std('Globar rate limit: ' + self.session.get_stats_str()) - else: - for server in self.session: - verbose_std('Per server rate limits: ' + server + ': '+ self.session[server].get_stats_str()) - - - async def get_url_JSON(self, url: str, chk_JSON_func = None, max_tries = MAX_RETRIES) -> dict: - """Class WG get_url_JSON() for load balancing between WG servers - that have individial rate limits""" - - if self.global_rate_limit: - session = self.session - else: - server = self.url_get_server(url) - session = self.session[server] - debug('server:' + server) - return await get_url_JSON(session, url, chk_JSON_func, max_tries) - - - async def get_account_id(self, nickname: str) -> int: - """Get WG account_id for a nickname""" - try: - nick = None - server = None - nick, server = nickname.split('@') - debug(nick + ' @ '+ server) - server = server.lower() - if nick is None or server is None: - raise ValueError('Invalid nickname given: ' + nickname) - url = self.get_url_account_id(nick, server) - - json_data = await self.get_url_JSON(url, self.chk_JSON_status) - for res in json_data['data']: - if res['nickname'].lower() == nick.lower(): - return res['account_id'] - error('No WG account_id found: ' + nickname) - - except Exception as err: - error(exception=err) - return None - - - async def get_player_tank_stats(self, account_id: int, tank_ids = [], fields = [], cache=True, cache_only = False) -> dict: - """Get player's stats (WR, # of battles) in a tank or all tanks (empty tank_ids[])""" - try: - stats = None - - # try cached stats first: - if cache: - stats = await self.get_cached_tank_stats(account_id, tank_ids, fields) - if stats is not None: - return stats - if cache_only: - return None - - # Cached stats not found, fetching new ones - url = self.get_url_player_tanks_stats(account_id, tank_ids, fields) - json_data = await self.get_url_JSON(url, self.chk_JSON_status) - if json_data is not None: - # debug('JSON Response received: ' + str(json_data)) - stats = json_data['data'][str(account_id)] - if cache: - await self.save_tank_stats(account_id, tank_ids, stats) - return stats - except Exception as err: - error(exception=err) - return None - - - async def get_player_stats(self, account_id: int, fields = [], cache=True, cache_only = False) -> dict: - """Get player's global stats """ - try: - # debug('account_id: ' + str(account_id) ) - stats = None - - # try cached stats first: - if cache: - stats = await self.get_cached_player_stats(account_id,fields) - # stats found unless StatsNotFound exception is raised - return stats - - except StatsNotFound as err: - debug('No Cached stats for account_id=' + str(account_id)) - if cache_only: - return None - # No cached stats found, need to retrieve - - try: - # Cached stats not found, fetching new ones - url = self.get_url_player_stats(account_id, fields) - json_data = await self.get_url_JSON(url, self.chk_JSON_status) - if json_data is not None: - # debug('JSON Response received: ' + str(json_data)) - stats = json_data['data'][str(account_id)] - if cache: - await self.save_player_stats(account_id, stats) - return stats - except Exception as err: - error(exception=err) - return None - - - async def get_player_achievements(self, account_ids: list, fields = [], cache=True) -> dict: - """Get player's achievements stats """ - try: - account_ids = set(account_ids) - stats = dict() - if len(account_ids) == 0: - debug('Zero account_ids given') - return None - - # try cached stats first: - if cache: - debug('Checking for cached stats') - account_ids_cached = set() - for account_id in account_ids: - try: - stats[str(account_id)] = await self.get_cached_player_achievements(account_id,fields) - account_ids_cached.add(account_id) - except StatsNotFound as err: - # No cached stats found, need to retrieve - debug(exception=err) - account_ids = account_ids.difference(account_ids_cached) - if len(account_ids) == 0: - return stats - debug('fetching new stats') - # Cached stats not found, fetching new ones - url = self.get_url_player_achievements(list(account_ids), fields) - json_data = await self.get_url_JSON(url, self.chk_JSON_status) - if (json_data is not None) and ('data' in json_data): - # debug('JSON Response received: ' + str(json_data)) - for account_id in json_data['data'].keys(): - stats[account_id] = json_data['data'][account_id] - if cache: - await self.save_player_achievements(int(account_id), json_data['data'][account_id]) - return stats - except Exception as err: - error(exception=err) - return None - - - def merge_player_stats(self, stats1: dict, stats2: dict) -> dict: - try: - if stats2 is None: return stats1 - for keyA in stats2: - if keyA not in stats1: - stats1[keyA] = stats2[keyA] - else: - for keyB in stats2[keyA]: - stats1[keyA][keyB] = stats2[keyA][keyB] - return stats1 - except KeyError as err: - error('Key not found', err) - return None - - - def get_tank_data(self, tank_id: int, field: str): - if self.tanks is None: - return None - try: - return self.tanks['data'][str(tank_id)][field] - except KeyError as err: - error('Key not found', err) - return None - - - def get_tank_tier(self, tank_id: int) -> str: - try: - return self.get_tank_data(tank_id, 'tier') - except Exception as err: - error(exception=err) - return None - - - def get_tank_name(self, tank_id: int) -> str: - try: - return self.get_tank_data(tank_id, 'name') - except Exception as err: - error(exception=err) - return None - - - def get_tank_type(self, tank_id: int) -> str: - try: - return self.get_tank_data(tank_id, 'type') - except Exception as err: - error(exception=err) - return None - - - def get_tank_nation(self, tank_id: int) -> str: - try: - return self.get_tank_data(tank_id, 'nation') - except Exception as err: - error(exception=err) - return None - - - def get_tank_type_str(self, tank_id: int) -> str: - try: - tank_type = self.get_tank_data(tank_id, 'type') - return self.TANK_TYPE_STR[self.TANK_TYPE_ID[tank_type]] - except Exception as err: - error(exception=err) - return None - - - def get_tank_nation_str(self, tank_id: int) -> str: - try: - nation = self.get_tank_data(tank_id, 'nation') - return self.NATION_STR[self.NATION_ID[nation]] - except Exception as err: - error(exception=err) - return None - - - def get_tank_type_id(self, tank_id: int) -> str: - try: - tank_type = self.get_tank_data(tank_id, 'type') - return self.TANK_TYPE_ID[tank_type] - except Exception as err: - error(exception=err) - return None - - - def get_tank_nation_id(self, tank_id: int) -> str: - try: - nation = self.get_tank_data(tank_id, 'nation') - return self.NATION_ID[nation] - except Exception as err: - error(exception=err) - return None - - def is_premium(self, tank_id: int) -> bool: - try: - return self.get_tank_data(tank_id, 'is_premium') - except Exception as err: - error(exception=err) - return None - - - async def save_stats(self, statsType: str, key: list, stats: list): - """Save stats to a async queue to be saved by the stat_saver -task""" - if self.statsQ is None: - return False - else: - await self.statsQ.put([ statsType, key, stats, NOW() ]) - return True - - - async def save_player_stats(self, account_id: int, stats: dict): - try: - await self.save_stats(MODE_PLAYER_STATS, [int(account_id)], stats) - except Exception as err: - error(exception=err) - return None - - - async def save_player_achievements(self, account_id: int, stats: dict): - try: - await self.save_stats(MODE_PLAYER_ACHIEVEMENTS, [int(account_id)], stats) - except Exception as err: - error(exception=err) - return None - - - async def save_tank_stats(self, account_id: int, tank_ids: list, stats: dict): - try: - await self.save_stats(MODE_TANK_STATS, [int(account_id), tank_ids], stats) - except Exception as err: - error(exception=err) - return None - - - async def stat_saver(self): - """Async task for saving stats into cache in background""" - - if self.statsQ is None: - error('No statsQ defined') - return None - try: - self.cache = await aiosqlite.connect(WG.CACHE_DB_FILE) - # Create cache tables table - await self.cache.execute(WG.SQL_TANK_STATS_CREATE_TBL) - await self.cache.execute(WG.SQL_PLAYER_STATS_CREATE_TBL) - await self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_CREATE_TBL) - - await self.cache.commit() - - if is_debug(): - async with self.cache.execute(WG.SQL_PLAYER_STATS_COUNT) as cursor: - debug('Cache contains: ' + str((await cursor.fetchone())[0]) + ' player stat records' ) - async with self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_COUNT) as cursor: - debug('Cache contains: ' + str((await cursor.fetchone())[0]) + ' player achievement records' ) - async with self.cache.execute(WG.SQL_TANK_STATS_COUNT) as cursor: - debug('Cache contains: ' + str((await cursor.fetchone())[0]) + ' player tank stat records' ) - except Exception as err: - error(exception=err) - sys.exit(1) - - while True: - try: - stats = await self.statsQ.get() - - stat_type = stats[0] - key = stats[1] - stats_data = stats[2] - update_time = stats[3] - - #debug('stat_type: ' + stat_type + ', key: ' + str(key)) - if stat_type == MODE_TANK_STATS: - await self.store_tank_stats(key, stats_data, update_time) - elif stat_type == MODE_PLAYER_STATS: - await self.store_player_stats(key, stats_data, update_time) - elif stat_type == MODE_PLAYER_ACHIEVEMENTS: - await self.store_player_achievements(key, stats_data, update_time) - else: - error('Function to saves stats type \'' + stat_type + '\' is not implemented yet') - - except asyncio.CancelledError: - # this is an eternal loop that will wait until cancelled - return None - - except Exception as err: - error(exception=err) - self.statsQ.task_done() - return None - - - async def cleanup_cache(self, grace_time = CACHE_GRACE_TIME): - """Clean old cache records""" - if self.cache is None: - debug('No active cache') - return None - for table in WG.SQL_TABLES: - async with self.cache.execute(WG.SQL_CHECK_TABLE_EXITS, (table,)) as cursor: - if (await cursor.fetchone()) is not None: - debug('Pruning cache table: ' + table) - await self.cache.execute(WG.SQL_PRUNE_CACHE.format(table, NOW() - grace_time)) - await self.cache.commit() - return None - - - async def store_tank_stats(self, key: list , stats_data: list, update_time: int): - """Save tank stats into cache""" - assert self.cache is not None, "cache must be initiated" - - try: - account_id = key[0] - tank_ids = set(key[1]) - if stats_data is not None: - for stat in stats_data: - tank_id = stat['tank_id'] - await self.cache.execute(WG.SQL_TANK_STATS_UPDATE, (account_id, tank_id, update_time, json.dumps(stat))) - tank_ids.discard(tank_id) - # no stats found => Add None to mark that - for tank_id in tank_ids: - await self.cache.execute(WG.SQL_TANK_STATS_UPDATE, (account_id, tank_id, update_time, None)) - await self.cache.commit() - debug('Cached tank stats saved for account_id: ' + str(account_id) ) - return True - except Exception as err: - error(exception=err) - return False - - - async def store_player_stats(self, key: list , stats_data: list, update_time: int): - """Save player stats into cache""" - assert self.cache is not None, "cache must be initiated" - - try: - account_id = key[0] - if stats_data is not None: - await self.cache.execute(WG.SQL_PLAYER_STATS_UPDATE, (account_id, update_time, json.dumps(stats_data))) - else: - await self.cache.execute(WG.SQL_PLAYER_STATS_UPDATE, (account_id, update_time, None)) - await self.cache.commit() - debug('Cached player stats saved for account_id: ' + str(account_id) ) - return True - except Exception as err: - error(exception=err) - return False - - - async def store_player_achievements(self, key: list , stats_data: list, update_time: int): - """Save player stats into cache""" - assert self.cache is not None, "cache must be initiated" - - try: - account_id = key[0] - if stats_data is not None: - await self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_UPDATE, (account_id, update_time, json.dumps(stats_data))) - else: - await self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_UPDATE, (account_id, update_time, None)) - await self.cache.commit() - debug('Cached player achievements saved for account_id: ' + str(account_id) ) - return True - except Exception as err: - error(exception=err) - return False - - - async def get_cached_tank_stats(self, account_id: int, tank_ids: list, fields: list ): - assert self.cache is not None, "cache must be initiated" - - try: - # test for cacheDB existence - debug('Trying cached stats first') - if self.cache is None: - debug('No cache DB') - return None - - stats = [] - if len(tank_ids) > 0: - sql_query = 'SELECT * FROM ' + WG.SQL_TANK_STATS_TBL + ' WHERE account_id = ? AND update_time > ? AND tank_id IN (' + ','.join([str(x) for x in tank_ids]) + ')' - else: - sql_query = 'SELECT * FROM ' + WG.SQL_TANK_STATS_TBL + ' WHERE account_id = ? AND update_time > ?' - - async with self.cache.execute(sql_query, [account_id, NOW() - WG.CACHE_GRACE_TIME] ) as cursor: - tank_ids = set(tank_ids) - async for row in cursor: - # debug('account_id: ' + str(account_id) + ': 1') - if row[3] is None: - # None/null stats found in cache - # i.e. stats have been requested, but not returned from WG API - tank_ids.remove(row[1]) - continue - # debug('account_id: ' + str(account_id) + ': 2') - stats.append(json.loads(row[3])) - # debug('account_id: ' + str(account_id) + ': 3') - tank_ids.remove(row[1]) - - # return stats ONLY if ALL the requested stats were found in cache - if tank_ids == set(): - debug('Cached stats found: ' + str(account_id)) - return stats - - except Exception as err: - error(exception=err) - debug('No cached stats found') - return None - - - async def get_cached_player_stats(self, account_id, fields): - try: - # test for cacheDB existence - debug('Trying cached stats first') - if self.cache is None: - # debug('No cache DB') - raise StatsNotFound('No cache DB in use') - - async with self.cache.execute(WG.SQL_PLAYER_STATS_CACHED, [account_id, NOW() - WG.CACHE_GRACE_TIME] ) as cursor: - row = await cursor.fetchone() - # debug('account_id: ' + str(account_id) + ': 1') - if row is None: - # no cached stats found, marked with an empty array - # debug('No cached stats found') - raise StatsNotFound('No cached stats found') - - debug('Cached stats found') - if row[2] is None: - # None/null stats found in cache - # i.e. stats have been requested, but not returned from WG API - return None - else: - # Return proper stats - return json.loads(row[2]) - except StatsNotFound as err: - debug(exception=err) - raise - except Exception as err: - error('Error trying to look for cached stats', exception=err) - return None - - - async def get_cached_player_achievements(self, account_id, fields): - try: - # test for cacheDB existence - debug('Trying cached stats first') - if self.cache is None: - # debug('No cache DB') - raise StatsNotFound('No cache DB in use') - - async with self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_CACHED, [account_id, NOW() - WG.CACHE_GRACE_TIME] ) as cursor: - row = await cursor.fetchone() - # debug('account_id: ' + str(account_id) + ': 1') - if row is None: - # no cached stats found, marked with an empty array - # debug('No cached stats found') - raise StatsNotFound('No cached stats found') - - debug('Cached stats found') - if row[2] is None: - # None/null stats found in cache - # i.e. stats have been requested, but not returned from WG API - return None - else: - # Return proper stats - return json.loads(row[2]) - except StatsNotFound as err: - debug(exception=err) - raise - except Exception as err: - error('Error trying to look for cached stats', exception=err) - return None - - -# ----------------------------------------------------------- -# Class WoTinspector -# ----------------------------------------------------------- - -class WoTinspector: - URL_WI = 'https://replays.wotinspector.com' - URL_REPLAY_LIST = URL_WI + '/en/sort/ut/page/' - URL_REPLAY_DL = URL_WI + '/en/download/' - URL_REPLAY_VIEW = URL_WI +'/en/view/' - URL_REPLAY_UL = 'https://api.wotinspector.com/replay/upload?' - URL_REPLAY_INFO = 'https://api.wotinspector.com/replay/upload?details=full&key=' - URL_TANK_DB = "https://wotinspector.com/static/armorinspector/tank_db_blitz.js" - - REPLAY_N = 1 - DEFAULT_RATE_LIMIT : float = 10/60 # 10 requests / min - DEFAULT_TOKEN : str = 'a5428af49bf7495986577d59b0e5bcff' - - def __init__(self, rate_limit: float = DEFAULT_RATE_LIMIT, auth_token: Optional[str] = None): - - headers : Optional[dict[str, str]] = None - - if auth_token is None: - auth_token = self.DEFAULT_TOKEN - headers = dict() - headers['Authorization'] = 'Token ' + auth_token - self.session = ThrottledClientSession(rate_limit=rate_limit, - filters=[self.URL_REPLAY_LIST, self.URL_REPLAY_INFO], - re_filter=False, limit_filtered=True, headers = headers) - - - async def close(self) -> None: - if self.session is not None: - debug('Closing aiohttp session') - await self.session.close() - - - async def get_tankopedia(self, filename = 'tanks.json'): - """Retrieve Tankpedia from WoTinspector.com""" - - async with self.session.get(self.URL_TANK_DB) as r: - if r.status == 200: - WI_tank_db=await r.text() - WI_tank_db = WI_tank_db.split("\n") - else: - print('Error: Could not get valid HTTPS response. HTTP: ' + str(r.status) ) - sys.exit(1) - tanks = {} - n = 0 - p = re.compile('\\s*(\\d+):\\s{"en":"([^"]+)",.*?"tier":(\\d+), "type":(\\d), "premium":(\\d).*') - for line in WI_tank_db[1:-1]: - try: - m = p.match(line) - tank = {} - tank['tank_id'] = int(m.group(1)) - tank['name'] = m.group(2) - tank['tier'] = int(m.group(3)) - tank['type'] = WG.TANK_TYPE[int(m.group(4))] - tank['is_premium'] = (int(m.group(5)) == 1) - tanks[str(m.group(1))] = tank - n += 1 - except Exception as err: - error(exception=err) - - tankopedia = {} - tankopedia['status'] = "ok" - tankopedia['meta'] = {"count" : n} - tankopedia['data'] = tanks - - verbose_std("Tankopedia has " + str(n) + " tanks in: " + filename) - with open(filename,'w') as outfile: - outfile.write(json.dumps(tankopedia, ensure_ascii=False, indent=4, sort_keys=False)) - return None - - - async def get_replay_JSON(self, replay_id: str): - json_resp = await get_url_JSON(self.session, self.URL_REPLAY_INFO + replay_id, chk_JSON_func=None) - try: - if self.chk_JSON_replay(json_resp): - return json_resp - else: - return None - except Exception as err: - error('Unexpected Exception', err) - return None - - - async def post_replay(self, data, filename = 'Replay', account_id = 0, title = 'Replay', priv = False, N = None): - try: - N = N if N is not None else self.REPLAY_N - self.REPLAY_N += 1 - - hash = hashlib.md5() - hash.update(data) - replay_id = hash.hexdigest() - - ## Testing if the replay has already been posted - json_resp = await self.get_replay_JSON(replay_id) - if json_resp is not None: - debug('Already uploaded: ' + title, id=N) - return json_resp - - params = { - 'title' : title, - 'private' : (1 if priv else 0), - 'uploaded_by' : account_id, - 'details' : 'full', - 'key' : replay_id - } - - url = self.URL_REPLAY_UL + urllib.parse.urlencode(params, quote_via=urllib.parse.quote) - #debug('URL: ' + url) - headers ={'Content-type': 'application/x-www-form-urlencoded'} - payload = { 'file' : (filename, base64.b64encode(data)) } - except Exception as err: - error('Unexpected Exception', exception=err, id=N) - return None - - json_resp = None - for retry in range(MAX_RETRIES): - debug('Posting: ' + title + ' Try #: ' + str(retry + 1) + '/' + str(MAX_RETRIES), id=N ) - try: - async with self.session.post(url, headers=headers, data=payload) as resp: - debug('HTTP response: '+ str(resp.status), id=N) - if resp.status == 200: - debug('HTTP POST 200 = Success. Reading response data', id=N) - json_resp = await resp.json() - if self.chk_JSON_replay(json_resp): - debug('Response data read. Status OK', id=N) - return json_resp - debug(title + ' : Receive invalid JSON', id=N) - else: - debug('Got HTTP/' + str(resp.status), id=N) - except Exception as err: - debug(exception=err, id=N) - await asyncio.sleep(SLEEP) - - debug(' Could not post replay: ' + title, id=N) - return json_resp - - - async def get_replay_listing(self, page: int = 0) -> aiohttp.ClientResponse: - url = self.get_url_replay_listing(page) - return await self.session.get(url) - - - @classmethod - def get_url_replay_listing(cls, page : int): - return cls.URL_REPLAY_LIST + str(page) + '?vt=#filters' - - - @classmethod - def get_url_replay_view(cls, replay_id): - return cls.URL_REPLAY_VIEW + replay_id - - - @classmethod - def get_replay_links(cls, doc: str): - """Get replay download links from WoTinspector.com replay listing page""" - try: - soup = BeautifulSoup(doc, 'lxml') - links = soup.find_all('a') - replay_links = set() - for tag in links: - link = tag.get('href',None) - if (link is not None) and link.startswith(cls.URL_REPLAY_DL): - replay_links.add(link) - debug('Adding replay link:' + link) - except Exception as err: - error(exception=err) - return replay_links - - - @classmethod - def read_replay_id(cls, json_replay): - """Read replay_id from replay JSON file""" - try: - url = json_replay['data']['view_url'] - return cls.get_replay_id(url) - except Exception as err: - error(exception=err) - return None - - - @classmethod - def get_replay_id(cls, url): - return url.rsplit('/', 1)[-1] - - - @classmethod - def chk_JSON_replay(cls, json_resp) -> bool: - """"Check String for being a valid JSON file""" - try: - if ('status' in json_resp) and json_resp['status'] == 'ok' and \ - (get_JSON_value(json_resp, key='data.summary.exp_base') is not None) : - debug("JSON check OK") - return True - except KeyError as err: - debug('Replay JSON check failed', exception=err) - except: - debug("Replay JSON check failed: " + str(json_resp)) - return False - -# ----------------------------------------------------------- -# Class BlitzStars -# ----------------------------------------------------------- - -class BlitzStars: - - URL_BLITZSTARS = 'https://www.blitzstars.com' - URL_PLAYER_STATS = URL_BLITZSTARS + '/api/playerstats' - URL_PLAYER_TANK_STATS = URL_BLITZSTARS + '/api/tanks' - URL_TANK_AVERAGES = URL_BLITZSTARS + '/tankaverages.json' - URL_ACTIVE_PLAYERS = URL_BLITZSTARS + '/api/playerstats/activeinlast30days' - - - def __init__(self, rate_limit=30): - headers = {'Accept-Encoding': 'gzip, deflate'} - self.session = ThrottledClientSession(rate_limit=rate_limit, headers=headers) - - async def close(self): - if self.session is not None: - await self.session.close() - - - @classmethod - def get_url_tank_averages(cls): - return cls.URL_TANK_AVERAGES - - - @classmethod - def get_url_player_stats(cls, account_id: int): - return cls.URL_PLAYER_STATS + '/' + str(account_id) - - - @classmethod - def get_url_player_tank_stats(cls, account_id: int, tank_id: int): - return cls.URL_PLAYER_TANK_STATS + '/' + str(account_id) + '/' + str(tank_id) - - - @classmethod - def get_url_player_tanks_stats(cls, account_id: int): - return cls.URL_PLAYER_TANK_STATS + '/' + str(account_id) - - - @classmethod - def get_url_active_players(cls): - return cls.URL_ACTIVE_PLAYERS - - - @classmethod - def chk_JSON_tank_stats(cls, json_resp): - """Check BlitzStars player tank stats""" - try: - if (len(json_resp) > 0) and ('tank_id' in json_resp[0]): - debug('JSON check OK') - return True - except Exception as err: - error('JSON check FAILED: ' + str(json_resp) ) - error(exception=err) - return False - - @classmethod - def chk_JSON_player_stats(cls, json_resp : list): - """Check BlitzStars player stats""" - try: - if (len(json_resp) > 0) and ('account_id' in json_resp[0]): - debug('JSON check OK') - return True - except Exception as err: - error('JSON check FAILED: ' + str(json_resp) ) - error(exception=err) - return False - - - @classmethod - async def tank_stats2WG(cls, BS_tank_stats: list) -> list: - """Convert BlitzStars player tank stats to WG API format""" - try: - if BS_tank_stats is None: - return None - WG_tank_stats = [] - for stat in BS_tank_stats: - try: - tmp = {} - for field in ['all', 'last_battle_time', 'tank_id', 'battle_life_time', 'account_id']: - tmp[field] = stat[field] - WG_tank_stats.append(tmp) - except KeyError as err: - error(exception=err) - if len(WG_tank_stats) > 0: - return WG_tank_stats - - except Exception as err: - error(exception=err) - pass - return None - - - async def get_player_stats(self, account_id: int, cache=True): - """Get player stats from BlitzStars""" - try: - # debug('account_id: ' + str(account_id) ) - stats = None - - if cache: - error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()') - sys.exit(1) - # stats = await self.get_cached_player_stats(account_id,fields) - # stats found unless StatsNotFound exception is raised - return stats - - except StatsNotFound as err: - # No cached stats found, need to retrieve - debug(exception=err) - pass - - try: - # Cached stats not found, fetching new ones - url = self.get_url_player_stats(account_id) - stats = await get_url_JSON(self.session, url, self.chk_JSON_player_stats) - if stats is not None: - # debug('JSON Response received: ' + str(json_data)) - if cache: - error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()') - sys.exit(1) - # await self.save_stats('player_stats', [account_id], stats) - return stats - except Exception as err: - error(exception=err) - return None - - async def get_player_tank_stats(self, account_id: int, tank_id = None, cache=True): - """Get player stats for all his tanks from BlitzStars""" - try: - # debug('account_id: ' + str(account_id) ) - stats = None - - if cache: - error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()') - sys.exit(1) - # stats = await self.get_cached_player_stats(account_id,fields) - # stats found unless StatsNotFound exception is raised - return stats - - except StatsNotFound as err: - # No cached stats found, need to retrieve - debug(exception=err) - pass - - try: - # Cached stats not found, fetching new ones - if tank_id is None: - # get stats for all the player's tanks - url = self.get_url_player_tanks_stats(account_id) - else: - url = self.get_url_player_tank_stats(account_id, tank_id) - stats = await get_url_JSON(self.session, url, self.chk_JSON_tank_stats) - if stats is not None: - # debug('JSON Response received: ' + str(json_data)) - if cache: - error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()') - sys.exit(1) - # await self.save_stats('player_stats', [account_id], stats) - return stats - except Exception as err: - error(exception=err) - return None - - diff --git a/blitzutils1.py b/blitzutils1.py new file mode 100755 index 0000000..5d43555 --- /dev/null +++ b/blitzutils1.py @@ -0,0 +1,2384 @@ +#!/usr/bin/env python3 +## PYTHON VERSION MUST BE 3.7 OR HIGHER + +import sys, os, json, time, base64, urllib, inspect, hashlib, re, string, random +import asyncio, aiofiles, aiohttp, aiosqlite, lxml +from pathlib import Path +from bs4 import BeautifulSoup +from progress.bar import IncrementalBar +from progress.counter import Counter +from decimal import Decimal +from datetime import datetime + +from blitzutils.wg_api import WGApiTankopedia, WGTank + +MAX_RETRIES = 3 +SLEEP = 1.5 + +LOG_LEVELS = {"silent": 0, "normal": 1, "verbose": 2, "debug": 3} +SILENT = 0 +NORMAL = 1 +VERBOSE = 2 +DEBUG = 3 +_log_level = NORMAL +LOG = False +LOGGER = None + +MODE_TANK_STATS = "tank_stats" +MODE_PLAYER_STATS = "player_stats" +MODE_PLAYER_ACHIEVEMENTS = "player_achievements" +MODE_TANKOPEDIA = "tankopedia" + +# Progress display +_progress_N = 100 +_progress_i = 0 +_progress_id = None +_progress_obj = None + +UMASK = os.umask(0) +os.umask(UMASK) + +## ----------------------------------------------------------- +#### Class ThrottledClientSession(aiohttp.ClientSession) +# +# Rate-limited async http client session +# +# Inherits aiohttp.ClientSession +## ----------------------------------------------------------- + +from typing import Optional, Union +import aiohttp +import asyncio +import time +import logging +import re + + +class ThrottledClientSession(aiohttp.ClientSession): + """Rate-throttled client session class inherited from aiohttp.ClientSession)""" + + def __init__( + self, + rate_limit: float = 0, + filters: list[str] = list(), + limit_filtered: bool = False, + re_filter: bool = False, + *args, + **kwargs, + ) -> None: + assert isinstance(rate_limit, (int, float)), "rate_limit has to be float" + assert isinstance(filters, list), "filters has to be list" + assert isinstance(limit_filtered, bool), "limit_filtered has to be bool" + assert isinstance(re_filter, bool), "re_filter has to be bool" + + super().__init__(*args, **kwargs) + + self.rate_limit: float + self._fillerTask: Optional[asyncio.Task] = None + self._queue: Optional[asyncio.Queue] = None + self._start_time: float = time.time() + self._count: int = 0 + self._limit_filtered: bool = limit_filtered + self._re_filter: bool = re_filter + self._filters: list[Union[str, re.Pattern]] = list() + + if re_filter: + for filter in filters: + self._filters.append(re.compile(filter)) + else: + for filter in filters: + self._filters.append(filter) + self.set_rate_limit(rate_limit) + + def _get_sleep(self) -> float: + if self.rate_limit > 0: + return 1 / self.rate_limit + return 0 + + def get_rate(self) -> float: + """Return rate of requests""" + return self._count / (time.time() - self._start_time) + + def get_stats(self) -> dict[str, float]: + """Get session statistics""" + res = { + "rate": self.get_rate(), + "rate_limit": self.rate_limit, + "count": self._count, + } + return res + + def get_stats_str(self) -> str: + """Print session statistics""" + return f"rate limit: {str(self.rate_limit if self.rate_limit is not None else '-')} \ + rate: {0:.1f}.format(self.get_rate()) requests: {str(self._count)}" + + def reset_counters(self) -> dict[str, float]: + """Reset rate counters and return current results""" + res = self.get_stats() + self._start_time = time.time() + self._count = 0 + return res + + def set_rate_limit(self, rate_limit: float = 0) -> float: + assert rate_limit is not None, "rate_limit must not be None" + assert ( + isinstance(rate_limit, (int, float)) and rate_limit >= 0 + ), "rate_limit has to be type of 'float' >= 0" + + self.rate_limit = rate_limit + if rate_limit > 0: + self._queue = asyncio.Queue(int(rate_limit) + 1) + if self._fillerTask is not None: + self._fillerTask.cancel() + self._fillerTask = asyncio.create_task(self._filler()) + return self.rate_limit + + async def close(self) -> None: + """Close rate-limiter's "bucket filler" task""" + # DEBUG + logging.debug(self.get_stats_str()) + try: + if self._fillerTask is not None: + self._fillerTask.cancel() + await asyncio.wait_for(self._fillerTask, timeout=0.5) + except asyncio.TimeoutError as err: + logging.error(str(err)) + await super().close() + + async def _filler(self) -> None: + """Filler task to fill the leaky bucket algo""" + try: + if self._queue is None: + return None + logging.debug("SLEEP: " + str(self._get_sleep())) + updated_at = time.monotonic() + extra_increment: float = 0 + for i in range(0, self._queue.maxsize): + await self._queue.put(i) + while True: + if not self._queue.full(): + now = time.monotonic() + increment = self.rate_limit * (now - updated_at) + items_2_add = int( + min( + self._queue.maxsize - self._queue.qsize(), + int(increment + extra_increment), + ) + ) + extra_increment = (increment + extra_increment) % 1 + for i in range(0, items_2_add): + self._queue.put_nowait(i) + updated_at = now + await asyncio.sleep(self._get_sleep()) + except asyncio.CancelledError: + logging.debug("Cancelled") + except Exception as err: + logging.error(str(err)) + return None + + async def _request(self, *args, **kwargs) -> aiohttp.ClientResponse: + """Throttled _request()""" + if self._queue is not None and self.is_limited(*args): + await self._queue.get() + self._queue.task_done() + self._count += 1 + return await super()._request(*args, **kwargs) + + def is_limited(self, *args: str) -> bool: + """Check wether the rate limit should be applied""" + try: + url: str = args[1] + for filter in self._filters: + if isinstance(filter, re.Pattern) and filter.match(url) is not None: + return self._limit_filtered + elif isinstance(filter, str) and url.startswith(filter): + return self._limit_filtered + + return not self._limit_filtered + except Exception as err: + logging.error(str(err)) + return True + + +# ----------------------------------------------------------- +# Class AsyncLogger() +# ----------------------------------------------------------- + + +class AsyncLogger: + """Async file logger""" + + def __init__(self) -> None: + self._queue = asyncio.Queue() + self._task = None + self._file = None + + async def open(self, logfn: str = None): + """Set logging to file""" + if logfn is None: + logfn = "LOG_" + _randomword(6) + ".log" + try: + self._file = await aiofiles.open(logfn, mode="a") + self._task = asyncio.create_task(self.logger()) + return True + except Exception as err: + error("Error opening file: " + logfn, err) + self._file = None + return False + + async def logger(self): + """Async file logger""" + if self._file is None: + error("No log file defined") + return False + while True: + try: + msg = await self._queue.get() + await self._file.write(msg + "\n") + self._queue.task_done() + except asyncio.CancelledError as err: + return None + except Exception as err: + error(exception=err) + + def log(self, msg: str = ""): + self._queue.put_nowait(msg) + + async def close(self): + try: + # empty queue & close + await self._queue.join() + self._task.cancel() + self._file.close() + except Exception as err: + error("Error closing log file", err) + return None + + +# ----------------------------------------------------------- +# Utils +# ----------------------------------------------------------- + + +def set_debug(debug: bool): + global _log_level + _log_level = DEBUG + + +def set_verbose(verbose: bool): + global _log_level + _log_level = VERBOSE + + +def set_silent(silent: bool): + global _log_level + _log_level = SILENT + + +def set_log_level_normal(): + global _log_level + _log_level = NORMAL + + +def is_debug() -> bool: + return _log_level == DEBUG + + +def is_verbose() -> bool: + return _log_level == VERBOSE + + +def is_normal() -> bool: + return _log_level == NORMAL + + +def is_silent() -> bool: + return _log_level == SILENT + + +def set_log_level(silent: bool, verbose: bool, debug: bool): + global _log_level + _log_level = NORMAL + if silent: + _log_level = SILENT + if verbose: + _log_level = VERBOSE + if debug: + _log_level = DEBUG + + +def get_log_level() -> int: + return _log_level + + +def get_log_level_str() -> str: + for log_level in LOG_LEVELS: + if _log_level == LOG_LEVELS[log_level]: + return log_level + error("Unknown log level: " + str(_log_level)) + + +async def set_file_logging(logfn=None, add_timestamp=False): + """Set logging to file""" + global LOG, LOGGER + LOG = True + if logfn is None: + logfn = "LOG_" + _randomword(6) + else: + dateTimeObj = datetime.now() + timestampStr = dateTimeObj.strftime("%y%m%d%H%M%S") + logfn = logfn + "_" + timestampStr + logfn = logfn + ".log" + try: + LOGGER = AsyncLogger() + await LOGGER.open(logfn) + except Exception as err: + error("Error starting logger: " + logfn, err) + LOG = False + LOGGER = None + return LOG + + +async def close_file_logging(): + global LOG, LOGGER + LOG = False + await LOGGER.close() + LOGGER = None + + +def _randomword(length): + letters = string.ascii_lowercase + return "".join(random.choice(letters) for i in range(length)) + + +def verbose(msg="", id=None) -> bool: + """Print a message""" + return _print_log_msg( + "", msg, exception=None, id=id, print_msg=(_log_level >= VERBOSE) + ) + + +def verbose_std(msg="", id=None) -> bool: + """Print a message""" + return _print_log_msg( + "", msg, exception=None, id=id, print_msg=(_log_level >= NORMAL) + ) + + +def warning(msg="", id=None, force: bool = False) -> bool: + """Print a warning message""" + return _print_log_msg( + "", "Warning: " + msg, None, id, print_msg=(force or (_log_level >= NORMAL)) + ) + + +def debug(msg="", id=None, exception=None, force: bool = False) -> bool: + """print a conditional debug message""" + if (_log_level >= DEBUG) or force: + return _print_log_msg("DEBUG", msg, exception, id) + return False + + +def error(msg="", exception=None, id=None) -> bool: + """Print an error message""" + return _print_log_msg("ERROR", msg, exception, id) + + +def log(msg="", id=None, exception=None) -> bool: + """print a conditional debug message""" + return _print_log_msg( + "LOG", msg=msg, exception=exception, id=id, print_msg=(_log_level >= DEBUG) + ) + + +## Copy with pride: https://stackoverflow.com/questions/2203424/python-how-to-retrieve-class-information-from-a-frame-object +def get_class_from_frame(fr): + args, _, _, value_dict = inspect.getargvalues(fr) + # we check the first parameter for the frame function is + # named 'self' + if len(args) and args[0] == "self": + # in that case, 'self' will be referenced in value_dict + instance = value_dict.get("self", None) + if instance: + # return its class + return getattr(instance, "__class__", None) + # return None otherwise + return None + + +def _print_log_msg( + prefix: str = "LOG", + msg: str = "", + exception: Exception | None = None, + id: int | None = None, + print_msg: bool = True, +): + # Use empty prefix to determine standard verbose messages + if not (print_msg or LOG): + return False + retval = False + if prefix != "": + curframe = inspect.currentframe() + calframe = inspect.getouterframes(curframe) + class_name = get_class_from_frame(curframe) + caller = calframe[2].function + if class_name is not None: + prefix = f"{prefix}: {class_name}.{caller}" + else: + prefix = prefix + ": " + caller + prefix = prefix + "(): " + + if id is not None: + prefix = f"{prefix} [ {id}]: " + + exception_msg = "" + if (exception is not None) and isinstance(exception, Exception): + exception_msg = f" : Exception: {type(exception)} : {exception}" + + msg = f"{prefix}{msg}{exception_msg}" + if print_msg: + print(msg) + retval = True + if _log_msg(msg): + retval = True + return retval + + +def _log_msg(msg: str = ""): + if LOG and (LOGGER is not None): + LOGGER.log(msg) + return True + return False + + +def set_progress_step(n: int): + """Set the frequency of the progress dots. The bigger 'n', the fewer dots""" + global _progress_N, _progress_i + if n > 0: + _progress_N = n + _progress_i = 0 + return + + +def get_progress_step(): + """Get the frequency of the progress dots. The bigger 'n', the fewer dots""" + return _progress_N + + +def set_progress_bar( + heading: str, max_value: int, step: int = None, slow: bool = False, id: str = None +): + global _progress_obj, _progress_N, _progress_i, _progress_id + _progress_id = id + if step is None: + _progress_N = int(max_value / 1000) if (max_value > 1000) else 2 + else: + _progress_N = step + if _progress_obj is not None: + finish_progress_bar() + if slow: + _progress_obj = SlowBar(heading, max=max_value) + else: + _progress_obj = IncrementalBar( + heading, max=max_value, suffix="%(index)d/%(max)d %(percent)d%%" + ) + _progress_i = 0 + + _log_msg(heading + str(max_value)) + return + + +def set_counter(heading: str, step=None): + global _progress_obj, _progress_i + _progress_i = 0 + if _progress_obj is not None: + finish_progress_bar() + _progress_obj = Counter(heading + ": ") + if step is not None: + set_progress_step(step) + return + + +def print_progress(force=False, id: str = None) -> bool: + """Print progress bar/dots. Returns True if the dot is being printed.""" + global _progress_i + + _progress_i += 1 + if (_progress_i % _progress_N) == 0: + if (_log_level > SILENT) and (force or (_log_level < DEBUG)): + if _progress_obj is not None: + if _progress_id == id: + _progress_obj.next(_progress_N) + return True + else: + return False + else: + print(".", end="", flush=True) + return True + return False + + +def finish_progress_bar(): + """Finish and close progress bar object""" + global _progress_obj + + # print_nl = True + if _progress_obj is not None: + # if isinstance(_progress_obj, Counter): + # print_nl = False + _progress_obj.finish() + # if print_nl: + print_new_line() + _progress_obj = None + return None + + +def wait(sec: int): + for i in range(0, sec): + i = i ## to get rid of the warning... + time.sleep(1) + print_progress(True) + print("", flush=True) + + +def print_new_line(force=False): + if (_log_level > SILENT) and (force or (_log_level < DEBUG)): + print("", flush=True) + _log_msg("") + + +def NOW() -> int: + return int(time.time()) + + +def get_date_str(timestamp: int = NOW(), date_format="%Y%m%d_%H%M%S"): + """Return YYYYMMDD_HHmm date string""" + try: + ts = datetime.fromtimestamp(timestamp) + return ts.strftime(date_format) + except Exception as err: + error(exception=err) + return None + + +def rebase_file_args(current_dir: str, files: list[str]) -> list[str]: + """Rebase file command line params after moving working dir to the script's dir""" + assert isinstance(files, list), "files has to be a list" + + if (files[0] == "-") or (files[0] == "db:"): + return [files[0]] + else: + return [os.path.join(current_dir, fn) for fn in files] + + +async def read_int_list(filename: str) -> list[int]: + """Read file to a list and return list of integers in the input file""" + + input_list = [] + try: + async with aiofiles.open(filename) as fp: + async for line in fp: + try: + input_list.append(int(line)) + except (ValueError, TypeError) as err: + pass + except Exception as err: + error("Unexpected error when reading file: " + filename, err) + return input_list + + +async def save_JSON( + filename: str, json_data: dict, sort_keys=False, pretty=True +) -> bool: + """Save JSON data into file""" + try: + dirname = os.path.dirname(filename) + if (dirname != "") and not os.path.isdir(dirname): + os.makedirs(dirname, 0o770 - UMASK) + async with aiofiles.open(filename, "w", encoding="utf8") as outfile: + if pretty: + await outfile.write( + json.dumps( + json_data, ensure_ascii=False, indent=4, sort_keys=sort_keys + ) + ) + else: + await outfile.write( + json.dumps(json_data, ensure_ascii=False, sort_keys=sort_keys) + ) + return True + except Exception as err: + error("Error saving JSON", err) + return False + + +async def open_JSON(filename: str, chk_JSON_func=None) -> dict: + try: + async with aiofiles.open(filename) as fp: + json_data = json.loads(await fp.read()) + if chk_JSON_func is None: + debug("JSON file content not checked: " + filename) + return json_data + elif chk_JSON_func(json_data): + debug("JSON File is valid: " + filename) + return json_data + else: + debug("JSON File has invalid content: " + filename) + except Exception as err: + error("Unexpected error when reading file: " + filename, err) + return None + + +async def get_url_JSON( + session: aiohttp.ClientSession, url: str, chk_JSON_func=None, max_tries=MAX_RETRIES +) -> dict: + """Retrieve (GET) an URL and return JSON object""" + if session is None: + error("Session must be initialized first") + sys.exit(1) + if url is None: + return None + + # To avoid excessive use of servers + for retry in range(1, max_tries + 1): + try: + async with session.get(url) as resp: + if resp.status == 200: + debug("HTTP request OK") + json_resp = await resp.json() + if (chk_JSON_func is None) or chk_JSON_func(json_resp): + # debug("Received valid JSON: " + str(json_resp)) + return json_resp + # Sometimes WG API returns JSON error even a retry gives valid JSON + elif resp.status == 407: + json_resp_err = await resp.json() + error("WG API returned 407: " + json_resp_err["error"]["message"]) + if retry == max_tries: + break + debug( + "Retrying URL [" + str(retry) + "/" + str(max_tries) + "]: " + url + ) + await asyncio.sleep(SLEEP) + + except aiohttp.ClientError as err: + debug("Could not retrieve URL: " + url, exception=err) + except asyncio.CancelledError as err: + debug("Queue gets cancelled while still working.", exception=err) + except Exception as err: + debug("Unexpected Exception", exception=err) + debug("Could not retrieve URL: " + url) + return None + + +def bld_dict_hierarcy(d: dict, key: str, value) -> dict: + """Build hierarcical dict based on multi-level key separated with""" + try: + key_hier = key.split(".") + sub_key = key_hier.pop(0) + if len(key_hier) == 0: + d[sub_key] = value + elif sub_key not in d: + d[sub_key] = bld_dict_hierarcy({}, ".".join(key_hier), value) + else: + d[sub_key] = bld_dict_hierarcy(d[sub_key], ".".join(key_hier), value) + + return d + except KeyError as err: + error("Key not found", err) + except Exception as err: + error("Unexpected Exception", err) + return None + + +def get_JSON_keypath(keypath: str, key: str): + if keypath is None: + return key + else: + return ".".join([keypath, key]) + + +def get_JSON_value( + json, key: str | None = None, keys: list | None = None, keypath: str | None = None +): + if keys is None: + if key is not None: + keys = key.split(".") + else: + return json + if len(keys) == 0: + return json + key = keys.pop(0) + if type(json) == dict: + if key in json: + return get_JSON_value( + json[key], keys=keys, keypath=get_JSON_keypath(keypath, key) + ) + else: + raise KeyError("Key: " + get_JSON_keypath(keypath, key) + " not found") + if type(json) == list: + p = re.compile(r"^\[(\d+)\]$") + m = p.match(key) + if len(m.groups()) != 1: + raise KeyError("Invalid key given: " + get_JSON_keypath(keypath, key)) + ndx = m.group(1) + try: + return get_JSON_value( + json[ndx], keys=keys, keypath=get_JSON_keypath(keypath, key) + ) + except IndexError: + raise KeyError( + "JSON array index out of range: " + get_JSON_keypath(keypath, key) + ) + raise KeyError("Key not found: " + get_JSON_keypath(keypath, keys[0])) + + +def sort_dict(d: dict, number: bool = False) -> dict: + """Sort a dict by keys""" + if number: + return dict(sorted(d.items(), key=lambda item: int(item[0]))) + else: + return dict(sorted(d.items(), key=lambda item: str(item[0]))) + + +# ----------------------------------------------------------- +# Class SlowBar +# ----------------------------------------------------------- + + +class SlowBar(IncrementalBar): + suffix = "%(index)d/%(max)d %(percent)d%% ETA %(remaining_hours).0f h %(remaining_mins).0f mins" + + @property + def remaining_hours(self): + return self.eta // 3600 + + @property + def remaining_mins(self): + return (self.eta - (self.eta // 3600) * 3600) // 60 + + +# ----------------------------------------------------------- +# Class StatsNotFound +# ----------------------------------------------------------- + + +class StatsNotFound(Exception): + pass + + +# ----------------------------------------------------------- +# Class WG +# ----------------------------------------------------------- + + +class WG: + URL_WG_CLAN_INFO = "clans/info/?application_id=" + # URL_WG_PLAYER_TANK_LIST = 'tanks/stats/?fields=tank_id%2Clast_battle_time&application_id=' + # URL_WG_PLAYER_TANK_LIST = 'tanks/stats/?fields=account_id%2Ctank_id%2Clast_battle_time%2Cbattle_life_time%2Call&application_id=' + URL_WG_PLAYER_TANK_STATS = "tanks/stats/?application_id=" + URL_WG_ACCOUNT_ID = "account/list/?fields=account_id%2Cnickname&application_id=" + URL_WG_PLAYER_STATS = "account/info/?application_id=" + URL_WG_PLAYER_ACHIEVEMENTS = "account/achievements/?application_id=" + CACHE_DB_FILE = ".blitzutils_cache.sqlite3" + CACHE_GRACE_TIME = 30 * 24 * 3600 # 30 days cache + _DEFAULT_WG_APP_ID = "81381d3f45fa4aa75b78a7198eb216ad" + + # sql_create_player_stats_tbl = """CREATE TABLE IF NOT EXISTS player_stats ( + # account_id INTEGER NOT NULL, + # date INTEGER NOT NULL, + # stat TEXT, + # value FLOAT + # ); """ + + # sql_create_player_tank_stats_tbl = """CREATE TABLE IF NOT EXISTS player_tank_stats ( + # account_id INTEGER NOT NULL, + # tank_id INTEGER DEFAULT NULL, + # date INTEGER NOT NULL, + # stat TEXT, + # value FLOAT + # ); """ + + # sql_select_player_stats = """SELECT value FROM player_stats ORDERBY date ASC + # WHERE account_id = {} AND stat = {} + # AND date >= {} LIMIT 1;""" + + # TANK_STATS + SQL_TANK_STATS_TBL = "tank_stats" + + SQL_TANK_STATS_CREATE_TBL = ( + "CREATE TABLE IF NOT EXISTS " + + SQL_TANK_STATS_TBL + + """ ( account_id INTEGER NOT NULL, + tank_id INTEGER NOT NULL, + update_time INTEGER NOT NULL, + stats TEXT, + PRIMARY KEY (account_id, tank_id) )""" + ) + + SQL_TANK_STATS_COUNT = "SELECT COUNT(*) FROM " + SQL_TANK_STATS_TBL + + SQL_TANK_STATS_UPDATE = ( + "REPLACE INTO " + + SQL_TANK_STATS_TBL + + "(account_id, tank_id, update_time, stats) VALUES(?,?,?,?)" + ) + + # PLAYER_STATS + SQL_PLAYER_STATS_TBL = "player_stats" + + SQL_PLAYER_STATS_CREATE_TBL = ( + "CREATE TABLE IF NOT EXISTS " + + SQL_PLAYER_STATS_TBL + + """ ( account_id INTEGER PRIMARY KEY, + update_time INTEGER NOT NULL, + stats TEXT)""" + ) + + SQL_PLAYER_STATS_COUNT = "SELECT COUNT(*) FROM " + SQL_PLAYER_STATS_TBL + + SQL_PLAYER_STATS_UPDATE = ( + "REPLACE INTO " + + SQL_PLAYER_STATS_TBL + + "(account_id, update_time, stats) VALUES(?,?,?)" + ) + + SQL_PLAYER_STATS_CACHED = ( + "SELECT * FROM " + + SQL_PLAYER_STATS_TBL + + " WHERE account_id = ? AND update_time > ?" + ) + + # PLAYER_ACHIEVEMENTS + SQL_PLAYER_ACHIEVEMENTS_TBL = "player_achievements" + + SQL_PLAYER_ACHIEVEMENTS_CREATE_TBL = ( + "CREATE TABLE IF NOT EXISTS " + + SQL_PLAYER_ACHIEVEMENTS_TBL + + """ ( account_id INTEGER PRIMARY KEY, + update_time INTEGER NOT NULL, + stats TEXT)""" + ) + + SQL_PLAYER_ACHIEVEMENTS_COUNT = ( + "SELECT COUNT(*) FROM " + SQL_PLAYER_ACHIEVEMENTS_TBL + ) + + SQL_PLAYER_ACHIEVEMENTS_CACHED = ( + "SELECT * FROM " + + SQL_PLAYER_ACHIEVEMENTS_TBL + + " WHERE account_id = ? AND update_time > ?" + ) + + SQL_PLAYER_ACHIEVEMENTS_UPDATE = ( + "REPLACE INTO " + + SQL_PLAYER_ACHIEVEMENTS_TBL + + "(account_id, update_time, stats) VALUES(?,?,?)" + ) + + SQL_TABLES = [SQL_PLAYER_STATS_TBL, SQL_TANK_STATS_TBL, SQL_PLAYER_ACHIEVEMENTS_TBL] + + SQL_CHECK_TABLE_EXITS = ( + """SELECT name FROM sqlite_master WHERE type='table' AND name=?""" + ) + + SQL_PRUNE_CACHE = """DELETE from {} WHERE update_time < {}""" + + # Default data. Please use the latest maps.json + + maps = { + "Random": "Random map", + "amigosville": "Falls Creek", + "asia": "Lost Temple", + "canal": "Canal", + "canyon": "Canyon", + "desert_train": "Desert Sands", + "erlenberg": "Middleburg", + "faust": "Faust", + "fort": "Macragge", + "grossberg": "Dynasty's Pearl", + "himmelsdorf": "Himmelsdorf", + "italy": "Vineyards", + "karelia": "Rockfield", + "karieri": "Copperfield", + "lake": "Mirage", + "lumber": "Alpenstadt", + "malinovka": "Winter Malinovka", + "medvedkovo": "Dead Rail", + "milbase": "Yamato Harbor", + "mountain": "Black Goldville", + "north": "North", + "ordeal": "Trial by Fire", + "pliego": "Castilla", + "port": "Port Bay", + "rock": "Mayan Ruins", + "rudniki": "Mines", + "savanna": "Oasis Palms", + "skit": "Naval Frontier", + "test": "World of Ducks", + "tutorial": "Proving Grounds", + } + + tanks: WGApiTankopedia + tanks_by_tier = None + + NATION = [ + "ussr", + "germany", + "usa", + "china", + "france", + "uk", + "japan", + "other", + "european", + ] + NATION_STR = [ + "Soviet", + "Germany", + "USA", + "China", + "France", + "UK", + "Japan", + "Other", + "European", + ] + NATION_ID = { + "ussr": 0, + "germany": 1, + "usa": 2, + "china": 3, + "france": 4, + "uk": 5, + "japan": 6, + "other": 7, + "european": 8, + } + + TANK_TYPE = ["lightTank", "mediumTank", "heavyTank", "AT-SPG"] + TANK_TYPE_STR = ["Light Tank", "Medium Tank", "Heavy Tank", "Tank Destroyer"] + + TANK_TYPE_ID = {"lightTank": 0, "mediumTank": 1, "heavyTank": 2, "AT-SPG": 3} + + URL_WG_SERVER = { + "eu": "https://api.wotblitz.eu/wotb/", + "ru": "https://api.wotblitz.ru/wotb/", + "na": "https://api.wotblitz.com/wotb/", + "asia": "https://api.wotblitz.asia/wotb/", + "china": None, + } + + ACCOUNT_ID_SERVER = { + "ru": range(0, int(5e8)), + "eu": range(int(5e8), int(10e8)), + "na": range(int(1e9), int(2e9)), + "asia": range(int(2e9), int(31e8)), + "china": range(int(31e8), int(4e9)), + } + + def __init__( + self, + tankopedia_fn: str, + WG_app_id: str = _DEFAULT_WG_APP_ID, + maps_fn: str | None = None, + stats_cache: bool = False, + rate_limit: int = 10, + global_rate_limit=True, + ): + with open(tankopedia_fn, mode="r") as file: + try: + self.tanks: WGApiTankopedia = WGApiTankopedia.parse_raw(file.read()) + except Exception as err: + error(f"could not parse Tankopedia: {tankopedia_fn}") + + self.WG_app_id = WG_app_id + # self.load_tanks(tankopedia_fn) + WG.tanks = self.tanks + self.global_rate_limit = global_rate_limit + + if maps_fn is not None: + if os.path.exists(maps_fn) and os.path.isfile(maps_fn): + try: + with open(maps_fn, "rt", encoding="utf8") as f: + WG.maps = json.loads(f.read()) + except Exception as err: + error("Could not read maps file: " + maps_fn + "\n" + str(err)) + else: + verbose("Could not find maps file: " + maps_fn) + if self.WG_app_id is not None: + headers = {"Accept-Encoding": "gzip, deflate"} + if self.global_rate_limit: + self.session = ThrottledClientSession( + rate_limit=rate_limit, headers=headers + ) + else: + self.session = dict() + for server in list(self.URL_WG_SERVER)[ + :4 + ]: # China (5th) server is unknown, thus excluded + self.session[server] = ThrottledClientSession( + rate_limit=rate_limit, headers=headers + ) + debug("WG aiohttp session initiated") + else: + self.session = None + debug("WG aiohttp session NOT initiated") + + # cache + self.cache = None + self.statsQ = None + self.stat_saver_task = None + if stats_cache: + try: + self.statsQ = asyncio.Queue() + self.stat_saver_task = asyncio.create_task(self.stat_saver()) + except Exception as err: + error(exception=err) + sys.exit(1) + + async def close(self): + # close stats queue + try: + if self.statsQ is not None: + debug("WG.close(): Waiting for statsQ to finish") + await self.statsQ.join() + debug("WG.close(): statsQ finished") + self.stat_saver_task.cancel() + debug("statsCacheTask cancelled") + await self.stat_saver_task + + # close cacheDB + if self.cache is not None: + # prune old cache records + await self.cleanup_cache() + await self.cache.commit() + await self.cache.close() + + if self.session is not None: + if self.global_rate_limit: + await self.session.close() + else: + for server in self.session: + await self.session[server].close() + except Exception as err: + error(exception=err) + + # Class methods ---------------------------------------------------------- + + @classmethod + def get_server(cls, account_id: int) -> str: + """Get Realm/server of an account based on account ID""" + if account_id >= 1e9: + if account_id >= 31e8: + debug("Chinese account/server: no stats available") + return None + if account_id >= 2e9: + return "asia" + return "na" + else: + if account_id < 5e8: + return "ru" + return "eu" + return None + + @classmethod + def update_maps(cls, map_data: dict): + """Update maps data""" + cls.maps = map_data + return None + + @classmethod + def get_map(self, map_str: str) -> str: + """Return map name from short map string in replays""" + try: + return self.maps[map_str] + except: + error("Map " + map_str + " not found") + return None + + # @classmethod + # def get_tank_user_strs(self) -> str: + # return self.tanks["userStr"].keys() + + @classmethod + def chk_JSON(cls, json_obj, check=None) -> bool: + try: + if check is None: + # nothing to check + return True + if check == "tank_stats": + if cls.chk_JSON_tank_stats(json_obj): + return True + else: + debug("Checking tank list JSON failed.") + return False + elif check == "player_stats": + if cls.chk_JSON_player_stats(json_obj): + return True + else: + debug("Checking player JSON failed.") + return False + elif check == "tankopedia": + if cls.chk_JSON_tankopedia(json_obj): + return True + else: + debug("Checking tank JSON failed.") + return False + elif check == "account_id": + if cls.chk_JSON_get_account_id(json_obj): + return True + else: + debug("Checking account_id JSON failed.") + return False + except (TypeError, ValueError) as err: + debug(str(err)) + return False + + @classmethod + def chk_JSON_status(cls, json_resp: dict) -> bool: + try: + if ( + (json_resp is None) + or ("status" not in json_resp) + or (json_resp["status"] is None) + ): + return False + elif (json_resp["status"] == "ok") and ("data" in json_resp): + return True + elif json_resp["status"] == "error": + if "error" in json_resp: + error_msg = "Received an error" + if ("message" in json_resp["error"]) and ( + json_resp["error"]["message"] is not None + ): + error_msg = error_msg + ": " + json_resp["error"]["message"] + if ("value" in json_resp["error"]) and ( + json_resp["error"]["value"] is not None + ): + error_msg = error_msg + " Value: " + json_resp["error"]["value"] + debug(error_msg) + return False + else: + error("Unknown status-code: " + json_resp["status"]) + return False + except KeyError as err: + error("No field found in JSON data", err) + except Exception as err: + error("JSON format error", err) + return False + + @classmethod + def chk_JSON_get_account_id(cls, json_resp: dict) -> bool: + try: + if cls.chk_JSON_status(json_resp): + if json_resp["meta"]["count"] > 0: + return True + except KeyError as err: + error("Key not found", err) + except Exception as err: + error(exception=err) + return False + + # @classmethod + # def chkJSONplayer(cls, json_resp: dict) -> bool: + # """"Check String for being a valid Player JSON file""" + # try: + # if cls.chk_JSON_status(json_resp): + # if int(json_resp[0]['account_id']) > 0: + # return True + # except KeyError as err: + # error('Key not found', err) + # except: + # debug("JSON check failed") + # return False + + @classmethod + def chk_JSON_tankopedia(cls, json_resp: dict) -> bool: + """ "Check String for being a valid Tankopedia JSON file""" + try: + if cls.chk_JSON_status(json_resp): + if int(json_resp[0]["tank_id"]) > 0: + return True + except KeyError as err: + error("Key not found", err) + except: + debug("JSON check failed") + return False + + @classmethod + def chk_JSON_player_stats(cls, json_resp: dict) -> bool: + """ "Check String for being a valid Tank JSON file""" + try: + if cls.chk_JSON_status(json_resp): + for acc in json_resp["data"]: + if json_resp["data"][acc] is not None: + return True + except KeyError as err: + error("Key not found", err) + except: + debug("JSON check failed") + return False + + @classmethod + def chk_JSON_tank_stats(cls, json_resp: dict) -> bool: + """ "Check String for being a valid Tank list JSON file""" + try: + if cls.chk_JSON_status(json_resp): + if ("data" in json_resp) and (len(json_resp["data"]) > 0): + debug("JSON tank list check OK") + return True + except Exception as err: + error("JSON check FAILED: " + str(json_resp)) + error(exception=err) + return False + + # # Methods -------------------------------------------------- + # def load_tanks(self, tankopedia_fn: str): + # """Load tanks from tankopedia JSON""" + # if tankopedia_fn is None: + # return False + # try: + # with open(tankopedia_fn, "rt", encoding="utf8") as f: + # self.tanks = json.loads(f.read()) + # self.tanks["tankStr"] = dict() + # p = re.compile(".+_short$") + # for usr_str in self.tanks["userStr"]: + # if p.match(usr_str) is None: + # self.tanks["tankStr"][self.tanks["userStr"][usr_str]] = usr_str + # self.tanks_by_tier = dict() + # for tier in range(1, 11): + # self.tanks_by_tier[str(tier)] = list() + # for tank in self.tanks["data"].values(): + # self.tanks_by_tier[str(tank["tier"])].append(tank["tank_id"]) + # return True + # except Exception as err: + # error("Could not read tankopedia: " + tankopedia_fn, err) + # return False + + def get_replay_filename(self, replay: dict): + try: + summary = replay["data"]["summary"] + timestamp = int(summary["battle_start_timestamp"]) + timestamp = get_date_str(timestamp, date_format="%Y%m%d_%H%M") + player = summary["player_name"] + arena_id = int(summary["arena_unique_id"]) + vehicle = self.name2tank_str(summary["vehicle"]) + vehicle.replace(" ", "_") + + return "{:s}_{:s}_{:s}_{:d}.wotbreplay.json".format( + timestamp, player, vehicle, arena_id + ) + except Exception as err: + error("Unexpected Exception", err) + return None + + def get_map_user_strs(self) -> str: + return self.maps.keys() + + # def tank_str2name(self, tank_str: str) -> str: + # """Return tank name from short tank string in replays""" + # try: + # return self.tanks["userStr"][tank_str] + # except: + # error("Tank " + tank_str + " not found") + # return tank_str + + # def name2tank_str(self, name: str) -> str: + # """Return shorttank string from tank string in replays""" + # try: + # return self.tanks["tankStr"][name] + # except: + # debug("Tank " + name + " not found") + # return name + + def get_tanks_by_tier(self, tier: int) -> list[int]: + """Returns tank_ids by tier""" + res: list[int] = list() + try: + for tank in self.tanks: + if tank.tier == tier: + res.append(tank.tank_id) + except KeyError as err: + error("Invalid tier", err) + return res + + def get_url_clan_info(self, server: str, clan_id: int) -> str: + try: + if server is None: + return None + return ( + self.URL_WG_SERVER[server] + + self.URL_WG_CLAN_INFO + + self.WG_app_id + + "&clan_id=" + + str(clan_id) + ) + except Exception as err: + if (server is None) or (server.lower() not in WG.ACCOUNT_ID_SERVER.keys()): + error( + "No server name or invalid server name given: " + server + if (server is not None) + else "" + ) + error("Available servers: " + ", ".join(WG.ACCOUNT_ID_SERVER.keys())) + error(exception=err) + return None + + def get_url_player_tank_list(self, account_id: int) -> str: + return self.get_url_player_tanks_stats(account_id, fields="tank_id") + + def get_url_player_tanks_stats( + self, account_id: int, tank_ids: list[int] = [], fields: list[str] = [] + ) -> str: + server = self.get_server(account_id) + if server is None: + return None + if (tank_ids is not None) and (len(tank_ids) > 0): + tank_id_str = "&tank_id=" + "%2C".join([str(x) for x in tank_ids]) + else: + # emtpy tank-id list returns all the player's tanks + tank_id_str = "" + + if (fields is not None) and (len(fields) > 0): + field_str = "&fields=" + "%2C".join(fields) + else: + # return all the fields + field_str = "" + + return ( + self.URL_WG_SERVER[server] + + self.URL_WG_PLAYER_TANK_STATS + + self.WG_app_id + + "&account_id=" + + str(account_id) + + tank_id_str + + field_str + ) + + def get_url_player_stats(self, account_id, fields) -> str: + try: + server = self.get_server(account_id) + if server is None: + return None + if (fields is not None) and (len(fields) > 0): + field_str = "&fields=" + "%2C".join(fields) + else: + # return all the fields + field_str = "" + + return ( + self.URL_WG_SERVER[server] + + self.URL_WG_PLAYER_STATS + + self.WG_app_id + + "&account_id=" + + str(account_id) + + field_str + ) + except Exception as err: + if server is None: + error("Invalid account_id") + error(exception=err) + return None + + def get_url_player_achievements( + self, account_ids: list, fields: str = "max_series" + ) -> str: + try: + # assumming that all account_ids are from the same server. This has to be taken care. + server = self.get_server(account_ids[0]) + + if server is None: + return None + account_ids_str = "%2C".join(str(id) for id in account_ids) + if (fields is not None) and (len(fields) > 0): + field_str = "&fields=" + "%2C".join(fields) + else: + # return all the fields + field_str = "" + + return ( + self.URL_WG_SERVER[server] + + self.URL_WG_PLAYER_ACHIEVEMENTS + + self.WG_app_id + + "&account_id=" + + account_ids_str + + field_str + ) + except Exception as err: + if server is None: + error("Invalid account_id") + error(exception=err) + return None + + def get_url_account_id(self, nickname, server) -> int: + try: + return ( + self.URL_WG_SERVER[server] + + self.URL_WG_ACCOUNT_ID + + self.WG_app_id + + "&search=" + + urllib.parse.quote(nickname) + ) + except Exception as err: + if nickname is None or len(nickname) == 0: + error("No nickname given") + if (server is None) or (server.lower() not in WG.ACCOUNT_ID_SERVER.keys()): + error( + "No server name or invalid server name given: " + server + if (server is not None) + else "" + ) + error("Available servers: " + ", ".join(WG.ACCOUNT_ID_SERVER.keys())) + error(exception=err) + return None + + def url_get_server(self, url: str) -> str: + """Decode WG server from the URL""" + try: + for server in self.session: + if url.startswith(self.URL_WG_SERVER[server]): + return server + except Exception as err: + error(exception=err) + return "eu" # default + + def print_request_stats(self): + """Print session statics""" + if self.global_rate_limit: + verbose_std("Globar rate limit: " + self.session.get_stats_str()) + else: + for server in self.session: + verbose_std( + "Per server rate limits: " + + server + + ": " + + self.session[server].get_stats_str() + ) + + async def get_url_JSON( + self, url: str, chk_JSON_func=None, max_tries=MAX_RETRIES + ) -> dict: + """Class WG get_url_JSON() for load balancing between WG servers + that have individial rate limits""" + + if self.global_rate_limit: + session = self.session + else: + server = self.url_get_server(url) + session = self.session[server] + debug("server:" + server) + return await get_url_JSON(session, url, chk_JSON_func, max_tries) + + async def get_account_id(self, nickname: str) -> int: + """Get WG account_id for a nickname""" + try: + nick = None + server = None + nick, server = nickname.split("@") + debug(nick + " @ " + server) + server = server.lower() + if nick is None or server is None: + raise ValueError("Invalid nickname given: " + nickname) + url = self.get_url_account_id(nick, server) + + json_data = await self.get_url_JSON(url, self.chk_JSON_status) + for res in json_data["data"]: + if res["nickname"].lower() == nick.lower(): + return res["account_id"] + error("No WG account_id found: " + nickname) + + except Exception as err: + error(exception=err) + return None + + async def get_player_tank_stats( + self, account_id: int, tank_ids=[], fields=[], cache=True, cache_only=False + ) -> dict: + """Get player's stats (WR, # of battles) in a tank or all tanks (empty tank_ids[])""" + try: + stats = None + + # try cached stats first: + if cache: + stats = await self.get_cached_tank_stats(account_id, tank_ids, fields) + if stats is not None: + return stats + if cache_only: + return None + + # Cached stats not found, fetching new ones + url = self.get_url_player_tanks_stats(account_id, tank_ids, fields) + json_data = await self.get_url_JSON(url, self.chk_JSON_status) + if json_data is not None: + # debug('JSON Response received: ' + str(json_data)) + stats = json_data["data"][str(account_id)] + if cache: + await self.save_tank_stats(account_id, tank_ids, stats) + return stats + except Exception as err: + error(exception=err) + return None + + async def get_player_stats( + self, account_id: int, fields=[], cache=True, cache_only=False + ) -> dict: + """Get player's global stats""" + try: + # debug('account_id: ' + str(account_id) ) + stats = None + + # try cached stats first: + if cache: + stats = await self.get_cached_player_stats(account_id, fields) + # stats found unless StatsNotFound exception is raised + return stats + + except StatsNotFound as err: + debug("No Cached stats for account_id=" + str(account_id)) + if cache_only: + return None + # No cached stats found, need to retrieve + + try: + # Cached stats not found, fetching new ones + url = self.get_url_player_stats(account_id, fields) + json_data = await self.get_url_JSON(url, self.chk_JSON_status) + if json_data is not None: + # debug('JSON Response received: ' + str(json_data)) + stats = json_data["data"][str(account_id)] + if cache: + await self.save_player_stats(account_id, stats) + return stats + except Exception as err: + error(exception=err) + return None + + async def get_player_achievements( + self, account_ids: list, fields=[], cache=True + ) -> dict: + """Get player's achievements stats""" + try: + account_ids = set(account_ids) + stats = dict() + if len(account_ids) == 0: + debug("Zero account_ids given") + return None + + # try cached stats first: + if cache: + debug("Checking for cached stats") + account_ids_cached = set() + for account_id in account_ids: + try: + stats[ + str(account_id) + ] = await self.get_cached_player_achievements( + account_id, fields + ) + account_ids_cached.add(account_id) + except StatsNotFound as err: + # No cached stats found, need to retrieve + debug(exception=err) + account_ids = account_ids.difference(account_ids_cached) + if len(account_ids) == 0: + return stats + debug("fetching new stats") + # Cached stats not found, fetching new ones + url = self.get_url_player_achievements(list(account_ids), fields) + json_data = await self.get_url_JSON(url, self.chk_JSON_status) + if (json_data is not None) and ("data" in json_data): + # debug('JSON Response received: ' + str(json_data)) + for account_id in json_data["data"].keys(): + stats[account_id] = json_data["data"][account_id] + if cache: + await self.save_player_achievements( + int(account_id), json_data["data"][account_id] + ) + return stats + except Exception as err: + error(exception=err) + return None + + def merge_player_stats(self, stats1: dict, stats2: dict) -> dict: + try: + if stats2 is None: + return stats1 + for keyA in stats2: + if keyA not in stats1: + stats1[keyA] = stats2[keyA] + else: + for keyB in stats2[keyA]: + stats1[keyA][keyB] = stats2[keyA][keyB] + return stats1 + except KeyError as err: + error("Key not found", err) + return None + + def get_tank_data(self, tank_id: int, field: str): + if self.tanks is None: + return None + try: + return self.tanks["data"][str(tank_id)][field] + except KeyError as err: + error("Key not found", err) + return None + + def get_tank(self, tank_id: int) -> WGTank: + return self.tanks[tank_id] + + def get_tank_tier(self, tank_id: int) -> int: + tank: WGTank = self.get_tank(tank_id) + if tank.tier is not None: + return int(tank.tier) + raise ValueError(f"could not find tier for tank_id={tank_id}") + + def get_tank_name(self, tank_id: int) -> str: + tank: WGTank = self.get_tank(tank_id) + if tank.name is not None: + return tank.name + raise ValueError(f"could not get name for tank_id={tank_id}") + + # def get_tank_type(self, tank_id: int) -> str: + # tank: WGTank = self.get_tank(tank_id) + # if tank.type is not None: + # return tank.type. + # raise ValueError(f"could not get name for tank_id={tank_id}") + + # def get_tank_nation(self, tank_id: int) -> str: + # try: + # return self.get_tank_data(tank_id, "nation") + # except Exception as err: + # error(exception=err) + # return None + + # def get_tank_type_str(self, tank_id: int) -> str: + # try: + # tank_type = self.get_tank_data(tank_id, "type") + # return self.TANK_TYPE_STR[self.TANK_TYPE_ID[tank_type]] + # except Exception as err: + # error(exception=err) + # return None + + # def get_tank_nation_str(self, tank_id: int) -> str: + # try: + # nation = self.get_tank_data(tank_id, "nation") + # return self.NATION_STR[self.NATION_ID[nation]] + # except Exception as err: + # error(exception=err) + # return None + + def get_tank_type(self, tank_id: int) -> str: + tank: WGTank = self.get_tank(tank_id) + if tank.type is not None: + return str(tank.type) + raise ValueError(f"could not get type for tank_id={tank_id}") + + def get_tank_nation(self, tank_id: int) -> str: + tank: WGTank = self.get_tank(tank_id) + if tank.nation is not None: + return str(tank.nation.name) + raise ValueError(f"could not get nation for tank_id={tank_id}") + + def is_premium(self, tank_id: int) -> bool: + tank: WGTank = self.get_tank(tank_id) + return tank.is_premium + + async def save_stats(self, statsType: str, key: list, stats: list): + """Save stats to a async queue to be saved by the stat_saver -task""" + if self.statsQ is None: + return False + else: + await self.statsQ.put([statsType, key, stats, NOW()]) + return True + + async def save_player_stats(self, account_id: int, stats: dict): + try: + await self.save_stats(MODE_PLAYER_STATS, [int(account_id)], stats) + except Exception as err: + error(exception=err) + return None + + async def save_player_achievements(self, account_id: int, stats: dict): + try: + await self.save_stats(MODE_PLAYER_ACHIEVEMENTS, [int(account_id)], stats) + except Exception as err: + error(exception=err) + return None + + async def save_tank_stats(self, account_id: int, tank_ids: list, stats: dict): + try: + await self.save_stats(MODE_TANK_STATS, [int(account_id), tank_ids], stats) + except Exception as err: + error(exception=err) + return None + + async def stat_saver(self): + """Async task for saving stats into cache in background""" + + if self.statsQ is None: + error("No statsQ defined") + return None + try: + self.cache = await aiosqlite.connect(WG.CACHE_DB_FILE) + # Create cache tables table + await self.cache.execute(WG.SQL_TANK_STATS_CREATE_TBL) + await self.cache.execute(WG.SQL_PLAYER_STATS_CREATE_TBL) + await self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_CREATE_TBL) + + await self.cache.commit() + + if is_debug(): + async with self.cache.execute(WG.SQL_PLAYER_STATS_COUNT) as cursor: + debug( + "Cache contains: " + + str((await cursor.fetchone())[0]) + + " player stat records" + ) + async with self.cache.execute( + WG.SQL_PLAYER_ACHIEVEMENTS_COUNT + ) as cursor: + debug( + "Cache contains: " + + str((await cursor.fetchone())[0]) + + " player achievement records" + ) + async with self.cache.execute(WG.SQL_TANK_STATS_COUNT) as cursor: + debug( + "Cache contains: " + + str((await cursor.fetchone())[0]) + + " player tank stat records" + ) + except Exception as err: + error(exception=err) + sys.exit(1) + + while True: + try: + stats = await self.statsQ.get() + + stat_type = stats[0] + key = stats[1] + stats_data = stats[2] + update_time = stats[3] + + # debug('stat_type: ' + stat_type + ', key: ' + str(key)) + if stat_type == MODE_TANK_STATS: + await self.store_tank_stats(key, stats_data, update_time) + elif stat_type == MODE_PLAYER_STATS: + await self.store_player_stats(key, stats_data, update_time) + elif stat_type == MODE_PLAYER_ACHIEVEMENTS: + await self.store_player_achievements(key, stats_data, update_time) + else: + error( + "Function to saves stats type '" + + stat_type + + "' is not implemented yet" + ) + + except asyncio.CancelledError: + # this is an eternal loop that will wait until cancelled + return None + + except Exception as err: + error(exception=err) + self.statsQ.task_done() + return None + + async def cleanup_cache(self, grace_time=CACHE_GRACE_TIME): + """Clean old cache records""" + if self.cache is None: + debug("No active cache") + return None + for table in WG.SQL_TABLES: + async with self.cache.execute(WG.SQL_CHECK_TABLE_EXITS, (table,)) as cursor: + if (await cursor.fetchone()) is not None: + debug("Pruning cache table: " + table) + await self.cache.execute( + WG.SQL_PRUNE_CACHE.format(table, NOW() - grace_time) + ) + await self.cache.commit() + return None + + async def store_tank_stats(self, key: list, stats_data: list, update_time: int): + """Save tank stats into cache""" + assert self.cache is not None, "cache must be initiated" + + try: + account_id = key[0] + tank_ids = set(key[1]) + if stats_data is not None: + for stat in stats_data: + tank_id = stat["tank_id"] + await self.cache.execute( + WG.SQL_TANK_STATS_UPDATE, + (account_id, tank_id, update_time, json.dumps(stat)), + ) + tank_ids.discard(tank_id) + # no stats found => Add None to mark that + for tank_id in tank_ids: + await self.cache.execute( + WG.SQL_TANK_STATS_UPDATE, (account_id, tank_id, update_time, None) + ) + await self.cache.commit() + debug("Cached tank stats saved for account_id: " + str(account_id)) + return True + except Exception as err: + error(exception=err) + return False + + async def store_player_stats(self, key: list, stats_data: list, update_time: int): + """Save player stats into cache""" + assert self.cache is not None, "cache must be initiated" + + try: + account_id = key[0] + if stats_data is not None: + await self.cache.execute( + WG.SQL_PLAYER_STATS_UPDATE, + (account_id, update_time, json.dumps(stats_data)), + ) + else: + await self.cache.execute( + WG.SQL_PLAYER_STATS_UPDATE, (account_id, update_time, None) + ) + await self.cache.commit() + debug("Cached player stats saved for account_id: " + str(account_id)) + return True + except Exception as err: + error(exception=err) + return False + + async def store_player_achievements( + self, key: list, stats_data: list, update_time: int + ): + """Save player stats into cache""" + assert self.cache is not None, "cache must be initiated" + + try: + account_id = key[0] + if stats_data is not None: + await self.cache.execute( + WG.SQL_PLAYER_ACHIEVEMENTS_UPDATE, + (account_id, update_time, json.dumps(stats_data)), + ) + else: + await self.cache.execute( + WG.SQL_PLAYER_ACHIEVEMENTS_UPDATE, (account_id, update_time, None) + ) + await self.cache.commit() + debug("Cached player achievements saved for account_id: " + str(account_id)) + return True + except Exception as err: + error(exception=err) + return False + + async def get_cached_tank_stats( + self, account_id: int, tank_ids: list[int], fields: list + ): + assert self.cache is not None, "cache must be initiated" + + try: + # test for cacheDB existence + debug("Trying cached stats first") + if self.cache is None: + debug("No cache DB") + return None + + stats = [] + if len(tank_ids) > 0: + sql_query = ( + "SELECT * FROM " + + WG.SQL_TANK_STATS_TBL + + " WHERE account_id = ? AND update_time > ? AND tank_id IN (" + + ",".join([str(x) for x in tank_ids]) + + ")" + ) + else: + sql_query = ( + "SELECT * FROM " + + WG.SQL_TANK_STATS_TBL + + " WHERE account_id = ? AND update_time > ?" + ) + + async with self.cache.execute( + sql_query, [account_id, NOW() - WG.CACHE_GRACE_TIME] + ) as cursor: + tank_ids = set(tank_ids) + async for row in cursor: + # debug('account_id: ' + str(account_id) + ': 1') + if row[3] is None: + # None/null stats found in cache + # i.e. stats have been requested, but not returned from WG API + tank_ids.remove(row[1]) + continue + # debug('account_id: ' + str(account_id) + ': 2') + stats.append(json.loads(row[3])) + # debug('account_id: ' + str(account_id) + ': 3') + tank_ids.remove(row[1]) + + # return stats ONLY if ALL the requested stats were found in cache + if tank_ids == set(): + debug("Cached stats found: " + str(account_id)) + return stats + + except Exception as err: + error(exception=err) + debug("No cached stats found") + return None + + async def get_cached_player_stats(self, account_id, fields): + try: + # test for cacheDB existence + debug("Trying cached stats first") + if self.cache is None: + # debug('No cache DB') + raise StatsNotFound("No cache DB in use") + + async with self.cache.execute( + WG.SQL_PLAYER_STATS_CACHED, [account_id, NOW() - WG.CACHE_GRACE_TIME] + ) as cursor: + row = await cursor.fetchone() + # debug('account_id: ' + str(account_id) + ': 1') + if row is None: + # no cached stats found, marked with an empty array + # debug('No cached stats found') + raise StatsNotFound("No cached stats found") + + debug("Cached stats found") + if row[2] is None: + # None/null stats found in cache + # i.e. stats have been requested, but not returned from WG API + return None + else: + # Return proper stats + return json.loads(row[2]) + except StatsNotFound as err: + debug(exception=err) + raise + except Exception as err: + error("Error trying to look for cached stats", exception=err) + return None + + async def get_cached_player_achievements(self, account_id, fields): + try: + # test for cacheDB existence + debug("Trying cached stats first") + if self.cache is None: + # debug('No cache DB') + raise StatsNotFound("No cache DB in use") + + async with self.cache.execute( + WG.SQL_PLAYER_ACHIEVEMENTS_CACHED, + [account_id, NOW() - WG.CACHE_GRACE_TIME], + ) as cursor: + row = await cursor.fetchone() + # debug('account_id: ' + str(account_id) + ': 1') + if row is None: + # no cached stats found, marked with an empty array + # debug('No cached stats found') + raise StatsNotFound("No cached stats found") + + debug("Cached stats found") + if row[2] is None: + # None/null stats found in cache + # i.e. stats have been requested, but not returned from WG API + return None + else: + # Return proper stats + return json.loads(row[2]) + except StatsNotFound as err: + debug(exception=err) + raise + except Exception as err: + error("Error trying to look for cached stats", exception=err) + return None + + +# ----------------------------------------------------------- +# Class WoTinspector +# ----------------------------------------------------------- + + +class WoTinspector: + URL_WI = "https://replays.wotinspector.com" + URL_REPLAY_LIST = URL_WI + "/en/sort/ut/page/" + URL_REPLAY_DL = URL_WI + "/en/download/" + URL_REPLAY_VIEW = URL_WI + "/en/view/" + URL_REPLAY_UL = "https://api.wotinspector.com/replay/upload?" + URL_REPLAY_INFO = "https://api.wotinspector.com/replay/upload?details=full&key=" + URL_TANK_DB = "https://wotinspector.com/static/armorinspector/tank_db_blitz.js" + + REPLAY_N = 1 + DEFAULT_RATE_LIMIT: float = 10 / 60 # 10 requests / min + DEFAULT_TOKEN: str = "a5428af49bf7495986577d59b0e5bcff" + + def __init__( + self, rate_limit: float = DEFAULT_RATE_LIMIT, auth_token: Optional[str] = None + ): + headers: Optional[dict[str, str]] = None + + if auth_token is None: + auth_token = self.DEFAULT_TOKEN + headers = dict() + headers["Authorization"] = "Token " + auth_token + self.session = ThrottledClientSession( + rate_limit=rate_limit, + filters=[self.URL_REPLAY_LIST, self.URL_REPLAY_INFO], + re_filter=False, + limit_filtered=True, + headers=headers, + ) + + async def close(self) -> None: + if self.session is not None: + debug("Closing aiohttp session") + await self.session.close() + + async def get_tankopedia(self, filename="tanks.json"): + """Retrieve Tankpedia from WoTinspector.com""" + + async with self.session.get(self.URL_TANK_DB) as r: + if r.status == 200: + WI_tank_db = await r.text() + WI_tank_db = WI_tank_db.split("\n") + else: + print( + "Error: Could not get valid HTTPS response. HTTP: " + str(r.status) + ) + sys.exit(1) + tanks = {} + n = 0 + p = re.compile( + '\\s*(\\d+):\\s{"en":"([^"]+)",.*?"tier":(\\d+), "type":(\\d), "premium":(\\d).*' + ) + for line in WI_tank_db[1:-1]: + try: + m = p.match(line) + tank = {} + tank["tank_id"] = int(m.group(1)) + tank["name"] = m.group(2) + tank["tier"] = int(m.group(3)) + tank["type"] = WG.TANK_TYPE[int(m.group(4))] + tank["is_premium"] = int(m.group(5)) == 1 + tanks[str(m.group(1))] = tank + n += 1 + except Exception as err: + error(exception=err) + + tankopedia = {} + tankopedia["status"] = "ok" + tankopedia["meta"] = {"count": n} + tankopedia["data"] = tanks + + verbose_std("Tankopedia has " + str(n) + " tanks in: " + filename) + with open(filename, "w") as outfile: + outfile.write( + json.dumps( + tankopedia, ensure_ascii=False, indent=4, sort_keys=False + ) + ) + return None + + async def get_replay_JSON(self, replay_id: str): + json_resp = await get_url_JSON( + self.session, self.URL_REPLAY_INFO + replay_id, chk_JSON_func=None + ) + try: + if self.chk_JSON_replay(json_resp): + return json_resp + else: + return None + except Exception as err: + error("Unexpected Exception", err) + return None + + async def post_replay( + self, data, filename="Replay", account_id=0, title="Replay", priv=False, N=None + ): + try: + N = N if N is not None else self.REPLAY_N + self.REPLAY_N += 1 + + hash = hashlib.md5() + hash.update(data) + replay_id = hash.hexdigest() + + ## Testing if the replay has already been posted + json_resp = await self.get_replay_JSON(replay_id) + if json_resp is not None: + debug("Already uploaded: " + title, id=N) + return json_resp + + params = { + "title": title, + "private": (1 if priv else 0), + "uploaded_by": account_id, + "details": "full", + "key": replay_id, + } + + url = self.URL_REPLAY_UL + urllib.parse.urlencode( + params, quote_via=urllib.parse.quote + ) + # debug('URL: ' + url) + headers = {"Content-type": "application/x-www-form-urlencoded"} + payload = {"file": (filename, base64.b64encode(data))} + except Exception as err: + error("Unexpected Exception", exception=err, id=N) + return None + + json_resp = None + for retry in range(MAX_RETRIES): + debug( + "Posting: " + + title + + " Try #: " + + str(retry + 1) + + "/" + + str(MAX_RETRIES), + id=N, + ) + try: + async with self.session.post( + url, headers=headers, data=payload + ) as resp: + debug("HTTP response: " + str(resp.status), id=N) + if resp.status == 200: + debug("HTTP POST 200 = Success. Reading response data", id=N) + json_resp = await resp.json() + if self.chk_JSON_replay(json_resp): + debug("Response data read. Status OK", id=N) + return json_resp + debug(title + " : Receive invalid JSON", id=N) + else: + debug("Got HTTP/" + str(resp.status), id=N) + except Exception as err: + debug(exception=err, id=N) + await asyncio.sleep(SLEEP) + + debug(" Could not post replay: " + title, id=N) + return json_resp + + async def get_replay_listing(self, page: int = 0) -> aiohttp.ClientResponse: + url = self.get_url_replay_listing(page) + return await self.session.get(url) + + @classmethod + def get_url_replay_listing(cls, page: int): + return cls.URL_REPLAY_LIST + str(page) + "?vt=#filters" + + @classmethod + def get_url_replay_view(cls, replay_id): + return cls.URL_REPLAY_VIEW + replay_id + + @classmethod + def get_replay_links(cls, doc: str): + """Get replay download links from WoTinspector.com replay listing page""" + try: + soup = BeautifulSoup(doc, "lxml") + links = soup.find_all("a") + replay_links = set() + for tag in links: + link = tag.get("href", None) + if (link is not None) and link.startswith(cls.URL_REPLAY_DL): + replay_links.add(link) + debug("Adding replay link:" + link) + except Exception as err: + error(exception=err) + return replay_links + + @classmethod + def read_replay_id(cls, json_replay): + """Read replay_id from replay JSON file""" + try: + url = json_replay["data"]["view_url"] + return cls.get_replay_id(url) + except Exception as err: + error(exception=err) + return None + + @classmethod + def get_replay_id(cls, url): + return url.rsplit("/", 1)[-1] + + @classmethod + def chk_JSON_replay(cls, json_resp) -> bool: + """ "Check String for being a valid JSON file""" + try: + if ( + ("status" in json_resp) + and json_resp["status"] == "ok" + and (get_JSON_value(json_resp, key="data.summary.exp_base") is not None) + ): + debug("JSON check OK") + return True + except KeyError as err: + debug("Replay JSON check failed", exception=err) + except: + debug("Replay JSON check failed: " + str(json_resp)) + return False + + +# ----------------------------------------------------------- +# Class BlitzStars +# ----------------------------------------------------------- + + +class BlitzStars: + URL_BLITZSTARS = "https://www.blitzstars.com" + URL_PLAYER_STATS = URL_BLITZSTARS + "/api/playerstats" + URL_PLAYER_TANK_STATS = URL_BLITZSTARS + "/api/tanks" + URL_TANK_AVERAGES = URL_BLITZSTARS + "/tankaverages.json" + URL_ACTIVE_PLAYERS = URL_BLITZSTARS + "/api/playerstats/activeinlast30days" + + def __init__(self, rate_limit=30): + headers = {"Accept-Encoding": "gzip, deflate"} + self.session = ThrottledClientSession(rate_limit=rate_limit, headers=headers) + + async def close(self): + if self.session is not None: + await self.session.close() + + @classmethod + def get_url_tank_averages(cls): + return cls.URL_TANK_AVERAGES + + @classmethod + def get_url_player_stats(cls, account_id: int): + return cls.URL_PLAYER_STATS + "/" + str(account_id) + + @classmethod + def get_url_player_tank_stats(cls, account_id: int, tank_id: int): + return cls.URL_PLAYER_TANK_STATS + "/" + str(account_id) + "/" + str(tank_id) + + @classmethod + def get_url_player_tanks_stats(cls, account_id: int): + return cls.URL_PLAYER_TANK_STATS + "/" + str(account_id) + + @classmethod + def get_url_active_players(cls): + return cls.URL_ACTIVE_PLAYERS + + @classmethod + def chk_JSON_tank_stats(cls, json_resp): + """Check BlitzStars player tank stats""" + try: + if (len(json_resp) > 0) and ("tank_id" in json_resp[0]): + debug("JSON check OK") + return True + except Exception as err: + error("JSON check FAILED: " + str(json_resp)) + error(exception=err) + return False + + @classmethod + def chk_JSON_player_stats(cls, json_resp: list): + """Check BlitzStars player stats""" + try: + if (len(json_resp) > 0) and ("account_id" in json_resp[0]): + debug("JSON check OK") + return True + except Exception as err: + error("JSON check FAILED: " + str(json_resp)) + error(exception=err) + return False + + @classmethod + async def tank_stats2WG(cls, BS_tank_stats: list) -> list: + """Convert BlitzStars player tank stats to WG API format""" + try: + if BS_tank_stats is None: + return None + WG_tank_stats = [] + for stat in BS_tank_stats: + try: + tmp = {} + for field in [ + "all", + "last_battle_time", + "tank_id", + "battle_life_time", + "account_id", + ]: + tmp[field] = stat[field] + WG_tank_stats.append(tmp) + except KeyError as err: + error(exception=err) + if len(WG_tank_stats) > 0: + return WG_tank_stats + + except Exception as err: + error(exception=err) + pass + return None + + async def get_player_stats(self, account_id: int, cache=True): + """Get player stats from BlitzStars""" + try: + # debug('account_id: ' + str(account_id) ) + stats = None + + if cache: + error("CACHE NOT IMPLEMENTED YET FOR BlitzStars()") + sys.exit(1) + # stats = await self.get_cached_player_stats(account_id,fields) + # stats found unless StatsNotFound exception is raised + return stats + + except StatsNotFound as err: + # No cached stats found, need to retrieve + debug(exception=err) + pass + + try: + # Cached stats not found, fetching new ones + url = self.get_url_player_stats(account_id) + stats = await get_url_JSON(self.session, url, self.chk_JSON_player_stats) + if stats is not None: + # debug('JSON Response received: ' + str(json_data)) + if cache: + error("CACHE NOT IMPLEMENTED YET FOR BlitzStars()") + sys.exit(1) + # await self.save_stats('player_stats', [account_id], stats) + return stats + except Exception as err: + error(exception=err) + return None + + async def get_player_tank_stats(self, account_id: int, tank_id=None, cache=True): + """Get player stats for all his tanks from BlitzStars""" + try: + # debug('account_id: ' + str(account_id) ) + stats = None + + if cache: + error("CACHE NOT IMPLEMENTED YET FOR BlitzStars()") + sys.exit(1) + # stats = await self.get_cached_player_stats(account_id,fields) + # stats found unless StatsNotFound exception is raised + return stats + + except StatsNotFound as err: + # No cached stats found, need to retrieve + debug(exception=err) + pass + + try: + # Cached stats not found, fetching new ones + if tank_id is None: + # get stats for all the player's tanks + url = self.get_url_player_tanks_stats(account_id) + else: + url = self.get_url_player_tank_stats(account_id, tank_id) + stats = await get_url_JSON(self.session, url, self.chk_JSON_tank_stats) + if stats is not None: + # debug('JSON Response received: ' + str(json_data)) + if cache: + error("CACHE NOT IMPLEMENTED YET FOR BlitzStars()") + sys.exit(1) + # await self.save_stats('player_stats', [account_id], stats) + return stats + except Exception as err: + error(exception=err) + return None diff --git a/upload_wotb_replays.py b/upload_wotb_replays.py index 044e973..469fc33 100755 --- a/upload_wotb_replays.py +++ b/upload_wotb_replays.py @@ -3,341 +3,443 @@ import json, argparse, sys, os, io, asyncio, aiofiles, aioconsole import logging, re, configparser, zipfile -import blitzutils as bu -from blitzutils import WG -from blitzutils import WoTinspector +import blitzutils1 as bu +from blitzutils1 import WG +from blitzutils1 import WoTinspector logging.getLogger("asyncio").setLevel(logging.DEBUG) -FILE_CONFIG = 'blitzstats.ini' +FILE_CONFIG = "blitzstats.ini" # DB_C_REPLAYS = 'Replays' -DEBUG = False +DEBUG = False VERBOSE = False -SLEEP = 2 +SLEEP = 2 MAX_RETRIES = 3 -REPLAY_N = 0 -SKIPPED_N = 0 -ERROR_N = 0 -WIurl='https://wotinspector.com/api/replay/upload?' -WG_appID = '81381d3f45fa4aa75b78a7198eb216ad' -wg : WG | None = None -wi : WoTinspector | None = None +REPLAY_N = 0 +SKIPPED_N = 0 +ERROR_N = 0 +WIurl = "https://wotinspector.com/api/replay/upload?" +WG_appID = "81381d3f45fa4aa75b78a7198eb216ad" +wg: WG | None = None +wi: WoTinspector | None = None + async def main(argv) -> int: - global wg, wi - current_dir = os.getcwd() - # set the directory for the script - os.chdir(os.path.dirname(sys.argv[0])) - - # options defaults - OPT_WORKERS_N = 2 - WG_ID = None - WG_ACCOUNT = None # format: nick@server, where server is either 'eu', 'ru', 'na', 'asia' or 'china'. - # China is not supported since WG API stats are not available there - WG_RATE_LIMIT = 10 + global wg, wi + current_dir = os.getcwd() + # set the directory for the script + os.chdir(os.path.dirname(sys.argv[0])) - try: - ## Read config - if os.path.isfile(FILE_CONFIG): - bu.debug('Reading config file: ' + FILE_CONFIG) - config = configparser.ConfigParser() - config.read(FILE_CONFIG) - - try: - if 'WG' in config.sections(): - configWG = config['WG'] - # WG account id of the uploader: - # # Find it here: https://developers.wargaming.net/reference/all/wotb/account/list/ - WG_ID = configWG.getint('wg_id', WG_ID) - WG_ACCOUNT = configWG.get('wg_account', WG_ACCOUNT) - ## WG API Rules limit 10 request / sec. Higher rate of requests will return errors ==> extra delay - WG_RATE_LIMIT = configWG.getint('wg_rate_limit', WG_RATE_LIMIT) - except (KeyError, configparser.NoSectionError) as err: - bu.error(exception=err) - - except Exception as err: - bu.error(exception=err) + # options defaults + OPT_WORKERS_N = 2 + WG_ID = None + WG_ACCOUNT = None # format: nick@server, where server is either 'eu', 'ru', 'na', 'asia' or 'china'. + # China is not supported since WG API stats are not available there + WG_RATE_LIMIT = 10 - parser = argparse.ArgumentParser(description='Post replays(s) to WoTinspector.com and retrieve replay data as JSON') - parser.add_argument('-id', dest='accountID', type=int, default=WG_ID, help='WG account_id') - parser.add_argument('-a', '--account', dest='account', type=str, default=WG_ACCOUNT, help='Uploader\'s WG account name. Format: ACCOUNT_NAME@SERVER') - parser.add_argument('-t','--title', type=str, default=None, help='Title for replays. Use "NN" for continous numbering. Default is automatic naming') - parser.add_argument('-p', '--private', dest="private", action='store_true', default=False, help='Set replays private on WoTinspector.com') - parser.add_argument('--tankopedia', type=str, default='tanks.json', help='JSON file to read Tankopedia from. Default: "tanks.json"') - parser.add_argument('--mapfile', type=str, default='maps.json', help='JSON file to read Blitz map names from. Default: "maps.json"') - parser.add_argument('-d', '--debug', action='store_true', default=False, help='Debug mode') - parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Verbose mode') - parser.add_argument('-s', '--silent', action='store_true', default=False, help='Silent mode') - parser.add_argument('files', metavar='FILE1 [FILE2 ...]', type=str, nargs='+', help='Files to read. Use \'-\' for STDIN"') - args = parser.parse_args() + try: + ## Read config + if os.path.isfile(FILE_CONFIG): + bu.debug("Reading config file: " + FILE_CONFIG) + config = configparser.ConfigParser() + config.read(FILE_CONFIG) - bu.set_verbose(args.verbose) - bu.set_log_level(args.silent, args.verbose, args.debug) + try: + if "WG" in config.sections(): + configWG = config["WG"] + # WG account id of the uploader: + # # Find it here: https://developers.wargaming.net/reference/all/wotb/account/list/ + WG_ID = configWG.getint("wg_id", WG_ID) + WG_ACCOUNT = configWG.get("wg_account", WG_ACCOUNT) + ## WG API Rules limit 10 request / sec. Higher rate of requests will return errors ==> extra delay + WG_RATE_LIMIT = configWG.getint("wg_rate_limit", WG_RATE_LIMIT) + except (KeyError, configparser.NoSectionError) as err: + bu.error(exception=err) - wg = WG(WG_appID, args.tankopedia, args.mapfile) - wi = WoTinspector() + except Exception as err: + bu.error(exception=err) - if args.account is not None: - args.accountID = await wg.get_account_id(args.account) - bu.debug('WG account_id: ' + str(args.accountID)) + parser = argparse.ArgumentParser( + description="Post replays(s) to WoTinspector.com and retrieve replay data as JSON" + ) + parser.add_argument( + "-id", dest="accountID", type=int, default=WG_ID, help="WG account_id" + ) + parser.add_argument( + "-a", + "--account", + dest="account", + type=str, + default=WG_ACCOUNT, + help="Uploader's WG account name. Format: ACCOUNT_NAME@SERVER", + ) + parser.add_argument( + "-t", + "--title", + type=str, + default=None, + help='Title for replays. Use "NN" for continous numbering. Default is automatic naming', + ) + parser.add_argument( + "-p", + "--private", + dest="private", + action="store_true", + default=False, + help="Set replays private on WoTinspector.com", + ) + parser.add_argument( + "--tankopedia", + type=str, + default="tanks.json", + help='JSON file to read Tankopedia from. Default: "tanks.json"', + ) + parser.add_argument( + "--mapfile", + type=str, + default="maps.json", + help='JSON file to read Blitz map names from. Default: "maps.json"', + ) + parser.add_argument( + "-d", "--debug", action="store_true", default=False, help="Debug mode" + ) + parser.add_argument( + "-v", "--verbose", action="store_true", default=False, help="Verbose mode" + ) + parser.add_argument( + "-s", "--silent", action="store_true", default=False, help="Silent mode" + ) + parser.add_argument( + "files", + metavar="FILE1 [FILE2 ...]", + type=str, + nargs="+", + help="Files to read. Use '-' for STDIN\"", + ) + args = parser.parse_args() - if args.accountID is None: - args.accountID = 0 + bu.set_verbose(args.verbose) + bu.set_log_level(args.silent, args.verbose, args.debug) - try: - queue : asyncio.Queue = asyncio.Queue() - # rebase file arguments due to moving the working directory to the script location - bu.verbose_std("replays.wotinspector.com rate limits replays to 6 / minute") - args.files = bu.rebase_file_args(current_dir, args.files) + wg = WG(WG_appID, args.tankopedia, args.mapfile) + wi = WoTinspector() - tasks = [] - # Make replay Queue - tasks.append(asyncio.create_task(mkReplayQ(queue, args.files, args.title))) - # Start tasks to process the Queue - for i in range(OPT_WORKERS_N): - tasks.append(asyncio.create_task(replayWorker(queue, i, args.accountID, args.private))) - bu.debug('Task ' + str(i) + ' started') - - bu.debug('Waiting for the replay scanner to finish') - await asyncio.wait([tasks[0]]) - bu.debug('Scanner finished. Waiting for workers to finish queue') - await queue.join() - bu.debug('Cancelling workers') - for task in tasks: - task.cancel() - bu.debug('Waiting for workers to cancel') - await asyncio.gather(*tasks, return_exceptions=True) - bu.verbose_std(str(REPLAY_N) + ' replays: ' + str(REPLAY_N - SKIPPED_N - ERROR_N) + ' uploaded, ' + str(SKIPPED_N) + ' skipped, ' + str(ERROR_N) + ' errors') - - except KeyboardInterrupt: - print('Ctrl-c pressed ...') - sys.exit(1) - finally: - ## Need to close the aiohttp.session since Python destructors do not support async methods... - await wg.session.close() - await wi.close() + if args.account is not None: + args.accountID = await wg.get_account_id(args.account) + bu.debug("WG account_id: " + str(args.accountID)) - return 0 + if args.accountID is None: + args.accountID = 0 + + try: + queue: asyncio.Queue = asyncio.Queue() + # rebase file arguments due to moving the working directory to the script location + bu.verbose_std("replays.wotinspector.com rate limits replays to 6 / minute") + args.files = bu.rebase_file_args(current_dir, args.files) + + tasks = [] + # Make replay Queue + tasks.append(asyncio.create_task(mkReplayQ(queue, args.files, args.title))) + # Start tasks to process the Queue + for i in range(OPT_WORKERS_N): + tasks.append( + asyncio.create_task( + replayWorker(queue, i, args.accountID, args.private) + ) + ) + bu.debug("Task " + str(i) + " started") + + bu.debug("Waiting for the replay scanner to finish") + await asyncio.wait([tasks[0]]) + bu.debug("Scanner finished. Waiting for workers to finish queue") + await queue.join() + bu.debug("Cancelling workers") + for task in tasks: + task.cancel() + bu.debug("Waiting for workers to cancel") + await asyncio.gather(*tasks, return_exceptions=True) + bu.verbose_std( + str(REPLAY_N) + + " replays: " + + str(REPLAY_N - SKIPPED_N - ERROR_N) + + " uploaded, " + + str(SKIPPED_N) + + " skipped, " + + str(ERROR_N) + + " errors" + ) + + except KeyboardInterrupt: + print("Ctrl-c pressed ...") + sys.exit(1) + finally: + ## Need to close the aiohttp.session since Python destructors do not support async methods... + await wg.session.close() + await wi.close() + + return 0 -async def mkReplayQ(queue : asyncio.Queue, files : list, title : str): - """Create queue of replays to post""" - p_replayfile = re.compile('.*\\.wotbreplay$') - if files[0] == '-': - bu.debug('reading replay file list from STDIN') - stdin, _ = await aioconsole.get_standard_streams() - while True: - line = (await stdin.readline()).decode('utf-8').rstrip() - if not line: - break - else: - if (p_replayfile.match(line) is not None): - await queue.put(await mkQueueItem(line, title)) - else: - for fn in files: - if fn.endswith('"'): - fn = fn[:-1] - if os.path.isfile(fn) and (p_replayfile.match(fn) is not None): - await queue.put(await mkQueueItem(fn, title)) - bu.debug('File added to queue: ' + fn) - elif os.path.isdir(fn): - with os.scandir(fn) as dirEntry: - for entry in dirEntry: - try: - bu.debug('Found: ' + entry.name) - if entry.is_file() and (p_replayfile.match(entry.name) is not None): - bu.debug(entry.name) - await queue.put(await mkQueueItem(entry.path, title)) - bu.debug('File added to queue: ' + entry.path) - except Exception as err: - bu.error(str(err)) - else: - bu.error('File not found: ' + fn) - - bu.debug('Finished') - return None +async def mkReplayQ(queue: asyncio.Queue, files: list, title: str): + """Create queue of replays to post""" + p_replayfile = re.compile(".*\\.wotbreplay$") + if files[0] == "-": + bu.debug("reading replay file list from STDIN") + stdin, _ = await aioconsole.get_standard_streams() + while True: + line = (await stdin.readline()).decode("utf-8").rstrip() + if not line: + break + else: + if p_replayfile.match(line) is not None: + await queue.put(await mkQueueItem(line, title)) + else: + for fn in files: + if fn.endswith('"'): + fn = fn[:-1] + if os.path.isfile(fn) and (p_replayfile.match(fn) is not None): + await queue.put(await mkQueueItem(fn, title)) + bu.debug("File added to queue: " + fn) + elif os.path.isdir(fn): + with os.scandir(fn) as dirEntry: + for entry in dirEntry: + try: + bu.debug("Found: " + entry.name) + if entry.is_file() and ( + p_replayfile.match(entry.name) is not None + ): + bu.debug(entry.name) + await queue.put(await mkQueueItem(entry.path, title)) + bu.debug("File added to queue: " + entry.path) + except Exception as err: + bu.error(str(err)) + else: + bu.error("File not found: " + fn) + + bu.debug("Finished") + return None -async def mkQueueItem(filename : str, title : str) -> list: - """Make an item to replay queue""" - global REPLAY_N - REPLAY_N +=1 - return [filename, REPLAY_N, getTitle(filename, title, REPLAY_N)] +async def mkQueueItem(filename: str, title: str) -> list: + """Make an item to replay queue""" + global REPLAY_N + REPLAY_N += 1 + return [filename, REPLAY_N, getTitle(filename, title, REPLAY_N)] -async def replayWorker(queue: asyncio.Queue, workerID: int, account_id: int, priv = False): - """Async Worker to process the replay queue""" - global SKIPPED_N - global ERROR_N - global wi +async def replayWorker( + queue: asyncio.Queue, workerID: int, account_id: int, priv=False +): + """Async Worker to process the replay queue""" + global SKIPPED_N + global ERROR_N + global wi - assert wi is not None, "wi cannot be None" + assert wi is not None, "wi cannot be None" - while True: - item = await queue.get() - filename = item[0] - N = item[1] - title = item[2] + while True: + item = await queue.get() + filename = item[0] + N = item[1] + title = item[2] - replay_json_fn = filename + '.json' - msg_str = 'Replay[' + str(N) + ']: ' + replay_json_fn = filename + ".json" + msg_str = "Replay[" + str(N) + "]: " - #bu.debug(msg_str + replay_json_fn) - try: - if os.path.isfile(replay_json_fn): - async with aiofiles.open(replay_json_fn) as fp: - replay_json = json.loads(await fp.read()) - #if replay_json['status'] == 'ok': - if wi.chk_JSON_replay(replay_json): - bu.verbose_std(msg_str + title + ' has already been posted. Skipping.' ) - else: - os.remove(replay_json_fn) - bu.debug(msg_str + "Replay JSON not valid/complete: Deleting " + replay_json_fn, id=workerID) - SKIPPED_N += 1 - queue.task_done() - continue + # bu.debug(msg_str + replay_json_fn) + try: + if os.path.isfile(replay_json_fn): + async with aiofiles.open(replay_json_fn) as fp: + replay_json = json.loads(await fp.read()) + # if replay_json['status'] == 'ok': + if wi.chk_JSON_replay(replay_json): + bu.verbose_std( + msg_str + title + " has already been posted. Skipping." + ) + else: + os.remove(replay_json_fn) + bu.debug( + msg_str + + "Replay JSON not valid/complete: Deleting " + + replay_json_fn, + id=workerID, + ) + SKIPPED_N += 1 + queue.task_done() + continue - except asyncio.CancelledError as err: - raise err - except Exception as err: - bu.error(msg_str + 'Unexpected error: ' + str(type(err)) + ' : '+ str(err)) - try: - #bu.debug('Opening file [' + str(N) +']: ' + filename) - async with aiofiles.open(filename,'rb') as fp: - filename = os.path.basename(filename) - bu.debug(msg_str + 'File: ' + filename) - json_resp = await wi.post_replay(await fp.read(), filename, account_id, title, priv, N) - if json_resp is not None: - if (await bu.save_JSON(replay_json_fn,json_resp)): - if wi.chk_JSON_replay(json_resp): - if not bu.debug(msg_str + 'Replay saved OK: ' + filename): - bu.verbose_std(msg_str + title + ' posted') - else: - bu.warning(msg_str +'Replay file is not valid/complete: ' + filename) - ERROR_N += 1 - else: - bu.error(msg_str + 'Error saving replay: ' + filename) - ERROR_N += 1 - else: - bu.error(msg_str + 'Replay file is not valid/complete: ' + filename) - ERROR_N += 1 - except Exception as err: - bu.error(msg_str + 'Unexpected Exception: ' + str(type(err)) + ' : ' + str(err) ) - bu.debug(msg_str + 'Marking task done') - queue.task_done() + except asyncio.CancelledError as err: + raise err + except Exception as err: + bu.error(msg_str + "Unexpected error: " + str(type(err)) + " : " + str(err)) + try: + # bu.debug('Opening file [' + str(N) +']: ' + filename) + async with aiofiles.open(filename, "rb") as fp: + filename = os.path.basename(filename) + bu.debug(msg_str + "File: " + filename) + json_resp = await wi.post_replay( + await fp.read(), filename, account_id, title, priv, N + ) + if json_resp is not None: + if await bu.save_JSON(replay_json_fn, json_resp): + if wi.chk_JSON_replay(json_resp): + if not bu.debug(msg_str + "Replay saved OK: " + filename): + bu.verbose_std(msg_str + title + " posted") + else: + bu.warning( + msg_str + + "Replay file is not valid/complete: " + + filename + ) + ERROR_N += 1 + else: + bu.error(msg_str + "Error saving replay: " + filename) + ERROR_N += 1 + else: + bu.error(msg_str + "Replay file is not valid/complete: " + filename) + ERROR_N += 1 + except Exception as err: + bu.error( + msg_str + "Unexpected Exception: " + str(type(err)) + " : " + str(err) + ) + bu.debug(msg_str + "Marking task done") + queue.task_done() - return None + return None -def getTitle_old(replayfile: str, title: str, i : int) -> str: - global wg +def getTitle_old(replayfile: str, title: str, i: int) -> str: + global wg - if title is None: - try: - filename = os.path.basename(replayfile) - bu.debug(filename) - map_usrStrs = wg.get_map_user_strs() - tank_userStrs = wg.get_tank_user_strs() - - #p = re.compile('\\d{8}_\\d{4}_(.+)_(' + '|'.join(map_usrStrs) + ')(?:-\\d)?\\.wotbreplay$') - # update 6.2 changed the file name format. Bug fixed 2019-09-09 Jylpah - p = re.compile('\\d{8}_\\d{4}_.*?(' + '|'.join(tank_userStrs) + ')_(' + '|'.join(map_usrStrs) + ')(?:-\\d)?\\.wotbreplay$') - - m = p.match(filename) - if (m is not None): - if wg.tanks is not None: - tank = m.group(1) - if tank in wg.tanks['userStr']: - tank = wg.tanks['userStr'][tank] - else: - bu.error('Tank code: "' + tank + '" not found from Tankopedia (tanks.json)') - else: - tank = m.group(1) - blitz_map = m.group(2) - if blitz_map in wg.maps: - blitz_map = wg.maps[blitz_map] - else: - bu.error('Mapcode: "' + blitz_map + '" not found from map database (maps.json)') - title = tank + ' @ ' + blitz_map - else: - title = re.sub('\\.wotbreplay$', '', filename) - except Exception as err: - bu.error(err) - else: - title.replace('NN', str(i)) - return title + if title is None: + try: + filename = os.path.basename(replayfile) + bu.debug(filename) + map_usrStrs = wg.get_map_user_strs() + tank_userStrs = wg.get_tank_user_strs() + + # p = re.compile('\\d{8}_\\d{4}_(.+)_(' + '|'.join(map_usrStrs) + ')(?:-\\d)?\\.wotbreplay$') + # update 6.2 changed the file name format. Bug fixed 2019-09-09 Jylpah + p = re.compile( + "\\d{8}_\\d{4}_.*?(" + + "|".join(tank_userStrs) + + ")_(" + + "|".join(map_usrStrs) + + ")(?:-\\d)?\\.wotbreplay$" + ) + + m = p.match(filename) + if m is not None: + if wg.tanks is not None: + tank = m.group(1) + if tank in wg.tanks["userStr"]: + tank = wg.tanks["userStr"][tank] + else: + bu.error( + 'Tank code: "' + + tank + + '" not found from Tankopedia (tanks.json)' + ) + else: + tank = m.group(1) + blitz_map = m.group(2) + if blitz_map in wg.maps: + blitz_map = wg.maps[blitz_map] + else: + bu.error( + 'Mapcode: "' + + blitz_map + + '" not found from map database (maps.json)' + ) + title = tank + " @ " + blitz_map + else: + title = re.sub("\\.wotbreplay$", "", filename) + except Exception as err: + bu.error(err) + else: + title.replace("NN", str(i)) + return title -def getTitle(replayfile: str, title: str = None, i : int = 0) -> str: - global wg - assert wg is not None, "wg must not be None" +def getTitle(replayfile: str, title: str = None, i: int = 0) -> str: + global wg + assert wg is not None, "wg must not be None" - if title is None: - try: - filename = os.path.basename(replayfile) - bu.debug(filename) - tank = None - map_name = None + if title is None: + try: + filename = os.path.basename(replayfile) + bu.debug(filename) + tank = None + map_name = None + + with zipfile.ZipFile(replayfile, "r") as archive: + # bu.debug('Replay file: ' + replayfile + ' opened') + with io.TextIOWrapper(archive.open("meta.json")) as meta: + # bu.debug('Replay file\'s metadata: opened') + try: + metadata_json = json.load(meta) + # player = metadata_json['playerName'] + tank_name = wg.get_tank_name( + metadata_json["vehicleCompDescriptor"] + ) + map_name = wg.get_map(metadata_json["mapName"]) + bu.debug( + "Tank: " + tank_name + if tank_name is not None + else "NOT FOUND IN tanks.json" + " Map: " + map_name + ) + except Exception as err: + bu.error(exception=err) + if (tank_name is not None) and (map_name is not None): + title = tank_name + " @ " + map_name + else: + title = re.sub("\\.wotbreplay$", "", filename) + except Exception as err: + bu.error(err) + else: + title.replace("NN", str(i)) + bu.debug("Returning: " + title) + return title - with zipfile.ZipFile(replayfile, 'r') as archive: - # bu.debug('Replay file: ' + replayfile + ' opened') - with io.TextIOWrapper(archive.open('meta.json')) as meta: - # bu.debug('Replay file\'s metadata: opened') - try: - metadata_json = json.load(meta) - #player = metadata_json['playerName'] - tank_name = wg.get_tank_name(metadata_json['vehicleCompDescriptor']) - map_name = wg.get_map(metadata_json['mapName']) - bu.debug('Tank: ' + tank_name if tank_name is not None else "NOT FOUND IN tanks.json" + ' Map: ' + map_name) - except Exception as err: - bu.error(exception = err) - if (tank_name is not None) and (map_name is not None): - title = tank_name + ' @ ' + map_name - else: - title = re.sub('\\.wotbreplay$', '', filename) - except Exception as err: - bu.error(err) - else: - title.replace('NN', str(i)) - bu.debug('Returning: ' + title) - return title ### UNIT TESTS - pytest # class TestClass: - # def test_getTitle(self): - # #player = "jylpah" - # WG_appID = '81381d3f45fa4aa75b78a7198eb216ad' - # wg = WG(WG_appID, 'tanks.json', 'maps.json') - # tank_name = "T28 Defender" # "A68_T28D" - # map_name = "Yukon" # "idle" - # replay = './test/' + '20201030_2358__jylpah_A_1157485188135816346.wotbreplay' - # res = tank_name + ' @ ' + map_name +# def test_getTitle(self): +# #player = "jylpah" +# WG_appID = '81381d3f45fa4aa75b78a7198eb216ad' +# wg = WG(WG_appID, 'tanks.json', 'maps.json') +# tank_name = "T28 Defender" # "A68_T28D" +# map_name = "Yukon" # "idle" +# replay = './test/' + '20201030_2358__jylpah_A_1157485188135816346.wotbreplay' +# res = tank_name + ' @ ' + map_name - # assert getTitle(replay) == res +# assert getTitle(replay) == res - # @pytest.mark.asyncio - # async def test_getTitle(self): - # #player = "jylpah" - # WG_appID = '81381d3f45fa4aa75b78a7198eb216ad' - # wg = WG(WG_appID, 'tanks.json', 'maps.json') - # tank_name = "T28 Defender" # "A68_T28D" - # map_name = "Yukon" # "idle" - # replay = './test/' + '20201030_2358__jylpah_A_1157485188135816346.wotbreplay' - # res = tank_name + ' @ ' + map_name +# @pytest.mark.asyncio +# async def test_getTitle(self): +# #player = "jylpah" +# WG_appID = '81381d3f45fa4aa75b78a7198eb216ad' +# wg = WG(WG_appID, 'tanks.json', 'maps.json') +# tank_name = "T28 Defender" # "A68_T28D" +# map_name = "Yukon" # "idle" +# replay = './test/' + '20201030_2358__jylpah_A_1157485188135816346.wotbreplay' +# res = tank_name + ' @ ' + map_name - # assert getTitle(replay) == res +# assert getTitle(replay) == res ### main() ------------------------------------------- if __name__ == "__main__": - # To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp - if sys.platform.startswith("win") and sys.version_info >= (3, 8): - try: - from asyncio import WindowsSelectorEventLoopPolicy - except ImportError: - pass - else: - if not isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy): - asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) - asyncio.run(main(sys.argv[1:]), debug=False) + # To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp + if sys.platform.startswith("win") and sys.version_info >= (3, 8): + try: + from asyncio import WindowsSelectorEventLoopPolicy + except ImportError: + pass + else: + if not isinstance( + asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy + ): + asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) + asyncio.run(main(sys.argv[1:]), debug=False) From 2a6ee87673798018606699a57703004c0639e355 Mon Sep 17 00:00:00 2001 From: jylpah Date: Sun, 10 Sep 2023 20:00:23 +0300 Subject: [PATCH 3/5] Use blitzutils.WGApiTankopedia() --- analyze_wotb_replays.py | 5608 +++++++++++++++++++++------------------ 1 file changed, 3098 insertions(+), 2510 deletions(-) diff --git a/analyze_wotb_replays.py b/analyze_wotb_replays.py index 4ad7f6f..5033771 100755 --- a/analyze_wotb_replays.py +++ b/analyze_wotb_replays.py @@ -4,2777 +4,3365 @@ # Script Analyze WoT Blitz replays import sys, argparse, json, os, concurrent, inspect, aiohttp, asyncio, aiofiles, aioconsole -import motor.motor_asyncio, configparser, ssl, re, logging, time, xmltodict, collections, csv +import configparser, ssl, re, logging, time, xmltodict, collections, csv from datetime import datetime, timedelta, date -import blitzutils as bu -from blitzutils import WG -from blitzutils import WoTinspector +from motor.motor_asyncio import AsyncIOMotorDatabase, AsyncIOMotorClient +import blitzutils1 as bu +from blitzutils1 import WG +from blitzutils1 import WoTinspector logging.getLogger("asyncio").setLevel(logging.DEBUG) -WG_APP_ID = 'df25c506c7944e48fbec67e8b74fd34e' -FILE_CONFIG = 'blitzstats.ini' -DB_C_TANK_STATS = 'WG_TankStats' -DB_C_PLAYER_STATS = 'WG_PlayerStats' -DB_C_REPLAYS = 'Replays' +WG_APP_ID = "df25c506c7944e48fbec67e8b74fd34e" +FILE_CONFIG = "blitzstats.ini" +DB_C_TANK_STATS = "WG_TankStats" +DB_C_PLAYER_STATS = "WG_PlayerStats" +DB_C_REPLAYS = "Replays" -N_PLAYERS = 'Nplayers' -MISSING_STATS = 'missing_stats' +N_PLAYERS = "Nplayers" +MISSING_STATS = "missing_stats" -wg = None -wi = None +wg: WG +wi: WoTinspector REPLAY_N = 0 REPLAY_I = 0 STAT_TANK_BATTLE_MIN = 100 -BATTLE_TIME_BUCKET = 3600*24*14 +BATTLE_TIME_BUCKET = 3600 * 24 * 14 -OPT_EXPORT_REPLAY_DIR = 'export_replays' -OPT_EXPORT_CSV_FILE = 'export.csv' -OPT_EXPORT_JSON_FILE = 'export.json' +OPT_EXPORT_REPLAY_DIR = "export_replays" +OPT_EXPORT_CSV_FILE = "export.csv" +OPT_EXPORT_JSON_FILE = "export.json" + +RE_SRC_IS_DB = re.compile(r"^DB:") -RE_SRC_IS_DB = re.compile(r'^DB:') class StatFunc: -## For different player stat functions (global stats, tank tier stats, etc) -# 1st function = for forming stat_id, 2nd for DB stats query, 3rd for WG API stats query - STAT_FUNC = { - 'tank_tier': [ 'get_stat_id_tank_tier', 'get_db_tank_tier_stats', 'get_wg_tank_tier_stats', 'WR at tier' ], - 'tank': [ 'get_stat_id_tank', 'get_db_tank_stats', 'get_wg_tank_stats', 'WR in Tank' ], - 'player': [ 'get_stat_id_player', 'get_db_player_stats', 'get_wg_player_stats', 'Career WR' ], - 'tier_x': [ 'get_stat_id_player', 'get_db_tier_x_stats', 'get_wg_tier_x_stats', 'WR at tier X' ], - } - - _DEFAULT = 'player' + ## For different player stat functions (global stats, tank tier stats, etc) + # 1st function = for forming stat_id, 2nd for DB stats query, 3rd for WG API stats query + STAT_FUNC = { + "tank_tier": [ + "get_stat_id_tank_tier", + "get_db_tank_tier_stats", + "get_wg_tank_tier_stats", + "WR at tier", + ], + "tank": [ + "get_stat_id_tank", + "get_db_tank_stats", + "get_wg_tank_stats", + "WR in Tank", + ], + "player": [ + "get_stat_id_player", + "get_db_player_stats", + "get_wg_player_stats", + "Career WR", + ], + "tier_x": [ + "get_stat_id_player", + "get_db_tier_x_stats", + "get_wg_tier_x_stats", + "WR at tier X", + ], + } - _stat_func = _DEFAULT + _DEFAULT = "player" - @classmethod - def get_default(cls): - return cls._DEFAULT + _stat_func = _DEFAULT + @classmethod + def get_default(cls): + return cls._DEFAULT - @classmethod - def get_stat_funcs(cls): - return cls.STAT_FUNC.keys() + @classmethod + def get_stat_funcs(cls): + return cls.STAT_FUNC.keys() + @classmethod + def set_stat_func(cls, stat_func: str = _DEFAULT): + if stat_func in cls.STAT_FUNC.keys(): + cls._stat_func = stat_func + else: + ## ERROR + pass + return cls._stat_func - @classmethod - def set_stat_func(cls, stat_func: str = _DEFAULT): - if stat_func in cls.STAT_FUNC.keys(): - cls._stat_func = stat_func - else: - ## ERROR - pass - return cls._stat_func + @classmethod + def get_stat_func(cls): + return cls._stat_func + @classmethod + def get_stat_id_func(cls): + return cls.STAT_FUNC[cls._stat_func][0] - @classmethod - def get_stat_func(cls): - return cls._stat_func + @classmethod + def get_db_func(cls): + return cls.STAT_FUNC[cls._stat_func][1] + @classmethod + def get_wg_func(cls): + return cls.STAT_FUNC[cls._stat_func][2] - @classmethod - def get_stat_id_func(cls): - return cls.STAT_FUNC[cls._stat_func][0] - - - @classmethod - def get_db_func(cls): - return cls.STAT_FUNC[cls._stat_func][1] - - - @classmethod - def get_wg_func(cls): - return cls.STAT_FUNC[cls._stat_func][2] - - - @classmethod - def get_title(cls): - return cls.STAT_FUNC[cls._stat_func][3] + @classmethod + def get_title(cls): + return cls.STAT_FUNC[cls._stat_func][3] replay_summary_flds = [ - 'battle_result', - 'battle_type', - 'map_name', - 'battle_duration', - 'room_type', - 'protagonist', - 'player_name', - 'title', - 'mastery_badge' + "battle_result", + "battle_type", + "map_name", + "battle_duration", + "room_type", + "protagonist", + "player_name", + "title", + "mastery_badge", ] replay_details_flds = [ - 'damage_made', - 'damage_assisted_track', - 'damage_assisted', - 'damage_received', - 'damage_blocked', - 'enemies_damaged', - 'enemies_destroyed', - 'hitpoints_left', - 'time_alive', - 'distance_travelled', - 'base_capture_points', - 'base_defend_points', - 'wp_points_earned', - 'wp_points_stolen', - 'hits_received', - 'hits_bounced', - 'hits_splash', - 'hits_pen', - 'shots_made', - 'shots_hit', - 'shots_pen', - 'shots_splash', - 'enemies_spotted' - ] + "damage_made", + "damage_assisted_track", + "damage_assisted", + "damage_received", + "damage_blocked", + "enemies_damaged", + "enemies_destroyed", + "hitpoints_left", + "time_alive", + "distance_travelled", + "base_capture_points", + "base_defend_points", + "wp_points_earned", + "wp_points_stolen", + "hits_received", + "hits_bounced", + "hits_splash", + "hits_pen", + "shots_made", + "shots_hit", + "shots_pen", + "shots_splash", + "enemies_spotted", +] ## Syntax: key == stat field in https://api.wotblitz.eu/wotb/tanks/stats/ (all.KEY) ## Value array [ 'Stat Title', [ 0, data buckets ....], scaling_factor_for_bucket_values, 'print_format' ] histogram_fields = { - 'wins' : [ 'Win rate', [0, .35, .40, .45, .48, .5, .52, .55, .60, .65, .70, 1], 1, '{:2.0%}' ], - 'damage_dealt' : [ 'Avg. Dmg.', [0, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1750, 2000, 2250, 2500, 2750, 3000, 100e3], 1, '{:3.0f}' ], - 'battles' : [ 'Battles', [0, 1000, 2500, 5000, 7000, 10e3, 15e3, 25e3, 50e3, 5e7], .001, '{:.0f}k'] # battles is a mandatory stat to include - } + "wins": [ + "Win rate", + [0, 0.35, 0.40, 0.45, 0.48, 0.5, 0.52, 0.55, 0.60, 0.65, 0.70, 1], + 1, + "{:2.0%}", + ], + "damage_dealt": [ + "Avg. Dmg.", + [ + 0, + 200, + 300, + 400, + 500, + 600, + 700, + 800, + 900, + 1000, + 1100, + 1200, + 1300, + 1400, + 1500, + 1750, + 2000, + 2250, + 2500, + 2750, + 3000, + 100e3, + ], + 1, + "{:3.0f}", + ], + "battles": [ + "Battles", + [0, 1000, 2500, 5000, 7000, 10e3, 15e3, 25e3, 50e3, 5e7], + 0.001, + "{:.0f}k", + ], # battles is a mandatory stat to include +} def def_value_zero(): return 0 + def def_value_BattleCategory(): - return BattleCategory() - -class BattleCategorizationList(): - - @staticmethod - def mk_battle_modes(modes: dict): - """Make a list of battle modes as required for the _categorizations""" - mode_ids = modes.values() - id_max = max(mode_ids) - ret_modes = ['-'] * (id_max+16) - for i in range(0, id_max+16): - ret_modes[i] = 'room_type=' + str(i) - - for mode in modes: - id = modes[mode] - ret_modes[id] = mode - - return ret_modes - - _battle_modes = { - 'Any' : 0, - 'Regular' : 1, - 'Training Room' : 2, - 'Tournament' : 4, - 'Quick Tournament' : 5, # maybe? - 'Rating' : 7, - 'Mad Games' : 8, - 'Realistic Battles' : 22, - 'Uprising' : 23, - 'Gravity Mode' : 24, - 'Skirmish' : 25, - 'Burning Games' : 26 - } - - _BATTLE_MODES = mk_battle_modes.__func__(_battle_modes) - - _categorizations = { - 'total' : [ 'TOTAL', 'total' ], - 'battle_result' : [ 'Result', 'category', [ 'Loss', 'Win', 'Draw']], - 'battle_type' : [ 'Battle Type', 'category', [ 'Encounter', 'Supremacy']], - 'room_type' : [ 'Battle Mode', 'category', _BATTLE_MODES ], - 'top_tier' : [ 'Tier', 'category', [ 'Bottom tier', 'Top tier']], - 'in_platoon' : [ 'Platoon', 'category', [ 'Solo', 'Platoon']], - 'tank_tier' : [ 'Tank Tier', 'number' ], - 'is_premium' : [ 'Tank Type', 'category', [ 'Tech tree', 'Premium']], - 'tank_type' : [ 'Tank Class', 'category', WG.TANK_TYPE_STR], - 'tank_nation' : [ 'Nation', 'category', WG.NATION_STR], - 'mastery_badge' : [ 'Battle Medal', 'category', ['-', '3rd Class', '2nd Class', '1st Class', 'Mastery' ]], - 'team_result' : [ 'Team Result', 'string' ], - 'player_wins' : [ 'Player WR', 'bucket', [ 0, .35, .45, .50, .55, .65], '%' ], - 'player_battles' : [ 'Player Battles', 'bucket', [ 0, 500, 1000, 2500, 5e3, 10e3, 15e3, 25e3], 'int' ], - 'player_damage_dealt' : [ 'Player Avg Dmg', 'bucket', [ 0, 500, 1000, 1250, 1500, 1750, 2000, 2500], 'int' ], - 'damage_made' : [ 'Player Dmg made', 'bucket', [ 0, 500, 1000, 1250, 1500, 1750, 2000, 2500], 'int' ], - 'enemies_destroyed' : [ 'Player Kills', 'number'], - 'enemies_spotted' : [ 'Player Spots', 'number'], - 'hit_rate' : [ 'Player Hit Rate', 'bucket', [ 0, .25, .5, .6, .7, .8, .9, .95 ], '%' ], - 'pen_rate' : [ 'Player Pen Rate', 'bucket', [ 0, .25, .5, .6, .7, .8, .9, .95 ], '%' ], - 'alive' : [ 'Time alive (pcs)', 'bucket', [ x/100 for x in range(0,100,10) ], '%' ], - 'time_alive' : [ 'Time alive (s)', 'bucket', [ x*60 for x in range(0,8) ], 'int' ], - 'battle_duration' : [ 'Battle Duration', 'bucket', [ x*60 for x in range(0,8) ], 'int' ], - 'distance_travelled' : [ 'Distance Driven', 'bucket', [ x*250 for x in range(0,20) ], 'int' ], - 'allies_wins' : [ 'Allies WR', 'bucket', [ 0, .35, .45, .50, .55, .65], '%' ], - 'allies_battles' : [ 'Player Battles', 'bucket', [ 0, 500, 1000, 2500, 5e3, 10e3, 15e3, 25e3], 'int' ], - 'allies_damage_dealt' : [ 'Player Avg Dmg', 'bucket', [ 0, 500, 1000, 1250, 1500, 1750, 2000, 2500], 'int' ], - 'enemies_wins' : [ 'Enemies WR', 'bucket', [ 0, .35, .45, .50, .55, .65], '%' ], - 'enemies_battles' : [ 'Player Battles', 'bucket', [ 0, 500, 1000, 2500, 5e3, 10e3, 15e3, 25e3], 'int' ], - 'enemies_damage_dealt' : [ 'Player Avg Dmg', 'bucket', [ 0, 500, 1000, 1250, 1500, 1750, 2000, 2500], 'int' ], - 'player_name' : [ 'Player', 'string', 25 ], - 'protagonist' : [ 'account_id', 'number' ], - 'tank_name' : [ 'Tank', 'string', 25], - 'map_name' : [ 'Map', 'string', 20], - 'battle' : [ 'Battle', 'string', 40], - 'battle_i' : [ 'Battle #', 'number' ] - } - - _categorizations_default = [ - 'total', - 'battle_result', - 'battle_type', - 'tank_tier', - 'top_tier' - ] - - ## Player stats based categorizations - _categorizations_stats = [ group + '_' + measure for group in [ 'player', 'allies', 'enemies'] for measure in ['wins', 'battles', 'damage_dealt' ]] - - - @classmethod - def get_categorizations_all(cls, cats: list = None) -> list: - try: - if cats == None: - return list(cls._categorizations.keys()) - else: - return list(set(cls._categorizations.keys()) & set(cats)) - except Exception as err: - bu.error(exception=err) - return None - - - @classmethod - def get_categorizations_stats(cls, cats=None, stats_cats=None): - try: - if cats == None: - return cls._categorizations_stats - else: - if stats_cats == None: - return cats - player_stat_filters = set(cls._categorizations_stats) - cats = set(cls.get_categorizations_all(cats)) - if stats_cats: - cats = cats & player_stat_filters - else: - cats = cats - player_stat_filters - bu.debug(', '.join(list(cats))) - return list(cats) - except Exception as err: - bu.error(exception=err) - return None - - - @classmethod - def get_categorizations_default(cls): - return cls._categorizations_default - - - @classmethod - def set_categorizations_default(cls, cat_defaults: list): - try: - defaults = list() - for cat in cat_defaults: - if cat in cls.get_categorizations_all(): - defaults.append(cat) - else: - bu.error(cat + ' is not a defined categorization') - cls._categorizations_default = defaults - except Exception as err: - bu.error(exception=err) - - - @classmethod - def get_categorizations_extra(cls) -> list: - #return cls._categorizations_extra - return list( set(cls._categorizations.keys() ) - set(cls.get_categorizations_default() ) ) - - - @classmethod - def get_categorization_title(cls, cat: str) -> str: - try: - return cls._categorizations[cat][0] - except Exception as err: - bu.error(exception=err) - return None - - - @classmethod - def get_categorization_type(cls, cat: str) -> str: - try: - return cls._categorizations[cat][1] - except Exception as err: - bu.error(exception=err) - return None - - - @classmethod - def get_categorization_params(cls, cat: str) -> list: - try: - if len(cls._categorizations[cat]) > 2: - return cls._categorizations[cat][2:] - except Exception as err: - bu.error(exception=err) - return None - - - @classmethod - def help(cls): - for cat in cls.get_categorizations_all(): - try: - ctitle = cls.get_categorization_title(cat) - ctype = cls.get_categorization_type(cat) - cparams = cls.get_categorization_params(cat) - - if ctype == 'number': - cat_tmp = BattleNumberCategorization(cat, ctitle) - elif ctype == 'string': - cat_tmp = BattleStringCategorization(cat, ctitle, cparams) - elif ctype == 'category': - cat_tmp = BattleClassCategorization(cat, ctitle, cparams[0]) - elif ctype == 'bucket': - cat_tmp = BattleBucketCategorization(cat, ctitle, cparams[0], cparams[1]) - else: - cat_tmp = BattleTotals(ctitle) - - cat_tmp.help() - - except KeyError as err: - bu.error('BattleCategorizationList(): Key not found: Category: ' + cat, exception=err) - except Exception as err: - bu.error('BattleCategorizationList()', exception=err) - - - @classmethod - def get_categorizations(cls, args : argparse.Namespace): - extra_cats = args.extra - only_extra = args.only_extra - - if only_extra: - cats = list() - else: - cats = cls.get_categorizations_default() - - if extra_cats != None: - cats = cats + extra_cats - return cats - - - def __init__(self, cats: list): - self.urls = collections.OrderedDict() - self.url_title_max_len = 0 - - cats = list(set(cats)) # remove duplicates - # ordering - self.categorizations_list = [ cat for cat in self._categorizations if cat in cats ] - - self.categorizations = dict() - for cat in self.categorizations_list: - try: - ctitle = self.get_categorization_title(cat) - ctype = self.get_categorization_type(cat) - cparams = self.get_categorization_params(cat) - - if ctype == 'total': - self.categorizations[cat] = BattleTotals(ctitle) - elif ctype == 'number': - self.categorizations[cat] = BattleNumberCategorization(cat, ctitle) - elif ctype == 'string': - self.categorizations[cat] = BattleStringCategorization(cat, ctitle, cparams) - elif ctype == 'category': - self.categorizations[cat] = BattleClassCategorization(cat, ctitle, cparams[0]) - elif ctype == 'bucket': - self.categorizations[cat] = BattleBucketCategorization(cat, ctitle, cparams[0], cparams[1]) - - except KeyError as err: - bu.error('BattleCategorizationList(): Key not found: Category: ' + cat, exception=err) - except Exception as err: - bu.error('BattleCategorizationList()', exception=err) - - - def __iter__(self): - """Returns the Iterator object""" - return BattleCategorizationListIterator(self) - - - def len(self): - return len(self.categorizations) - - - def get_categorization(self, cat: str): - """Return a BattleCategorization for a category""" - try: - return self.categorizations[cat] - except Exception as err: - bu.error(exception=err) - return None - - - def record_result(self, result: dict): - try: - for cat in self.categorizations_list: - self.categorizations[cat].record_result(result) - except Exception as err: - bu.error(exception=err) - return None - - - def calc_results(self): - try: - for cat in self.categorizations_list: - self.categorizations[cat].calc_results() - except Exception as err: - bu.error(exception=err) - return None - - - def record_url(self, result): - try: - self.url_title_max_len = max(self.url_title_max_len, len(result['title'])) - self.urls[result['title']] = result['url'] - except Exception as err: - bu.error(exception=err) - return None - - - def print_results(self): - try: - for cat in self.categorizations_list: - self.categorizations[cat].print_results() - self.print_urls() - - except Exception as err: - bu.error(exception=err) - - - def print_urls(self): - try: - if len(self.urls) > 0: - print('\nURLs to Battle replays:') - for title in self.urls: - print(('{:' + str(3 + self.url_title_max_len) + '}').format(title) + ' : ' + self.urls[title]) - except Exception as err: - bu.error(exception=err) - - - def get_filter_categories(self, cat: str, filters: list) -> dict: - try: - return self.categorizations[str(cat)].get_filter_categories(filters) - except Exception as err: - bu.error(exception=err) - return None - - - def get_categories(self, result: dict) -> dict: - """Get categories for each categorization in the CategorizationList() for a battle result""" - try: - res = dict() - for cat in self.categorizations_list: - res[cat] = self.categorizations[cat].get_category(result) - return res - except Exception as err: - bu.error(exception=err) - return None - - - def get_results_json(self): - try: - res = dict() - for cat in self.categorizations_list: - res[cat] = self.categorizations[cat].get_results_json() - res['replay_urls'] = self.urls - return res - except Exception as err: - bu.error(exception=err) - return None - - - def get_results_list(self, urls: bool = False): - try: - res = list() - for cat in self.categorizations_list: - res.append('') - res.extend(self.categorizations[cat].get_results_list()) - if urls: - res.append('') - for url in self.urls: - res.append(url) - return res - except Exception as err: - bu.error(exception=err) - return None - - -class BattleCategorizationListIterator(): - def __init__(self, btl_cats: BattleCategorizationList) -> None: - self._btl_cats = btl_cats - self._ndx = 0 - - def __next__(self): - if self._ndx < len(self._btl_cats.categorizations_list): - cat = self._btl_cats.categorizations_list[self._ndx] - res = self._btl_cats.categorizations[cat] - self._ndx += 1 - return res - raise StopIteration - - -class BattleCategorization(): - - RESULT_CAT_HEADER_LEN = 15 - RESULT_CAT_HEADER_FRMT = '{:_<' + str(RESULT_CAT_HEADER_LEN) + 's}' - RESULT_CAT_FRMT = '{:>' + str(RESULT_CAT_HEADER_LEN) + 's}' - - def __init__(self, cat_key : str, title: str): - self.total_battles = 0 - self.category_key = cat_key - self.title = title - self.categories = collections.defaultdict(def_value_BattleCategory) - - - def set_cat_header_format(self, header_len: int, left_aligned: bool = False): - self.RESULT_CAT_HEADER_LEN = header_len - self.RESULT_CAT_HEADER_FRMT = '{:_<' + str(header_len) + 's}' - if left_aligned: - self.RESULT_CAT_FRMT = '{:<' + str(header_len) + 's}' - else: - self.RESULT_CAT_FRMT = '{:>' + str(header_len) + 's}' - - - def get_categories(self): - """Get category keys sorted""" - return sorted(self.categories.keys() , key=str.casefold) - - - ### PER Child Class - def get_category(self, result: dict) -> str: - """Get category. Must be implemented in the child classes""" - return 'NONE' - - - def get_filter_categories(self, filters: list) -> list: - try: - if type(filters) != list: - filters = [ filters ] - return [ x for x in filters if x in self.get_categories() ] - except Exception as err: - bu.error('Category: ' + str(self.category_key), exception=err) - return None - - def help(self): - """Help""" - FORMAT = '\t{:<15s}\t{}. Options:' - print(FORMAT.format(self.category_key, self.title), end=' ') - - - def record_result(self, result: dict) -> bool: - try: - cat = self.get_category(result) - self.categories[cat].record_result(result) - self.total_battles += 1 - return True - except KeyError as err: - bu.error('Category key not found: ' + cat, exception=err) - except Exception as err: - bu.error('Category key: ' + cat, exception = err) - return False - - - def calc_results(self): - try: - for cat in self.get_categories(): - self.categories[cat].calc_results(self.total_battles) - except Exception as err: - bu.error(exception = err) - - - def print_headers(self): - try: - if len(self.title) > self.RESULT_CAT_HEADER_LEN: - cat_name = self.title[:self.RESULT_CAT_HEADER_LEN] - else: - cat_name = self.title - header = self.RESULT_CAT_HEADER_FRMT.format(cat_name) - BattleCategory.print_headers(header) - return None - except Exception as err: - bu.error(exception = err) - - - def get_headers(self) -> list: - """Get BattleCategory headers as list""" - try: - return BattleCategory.get_headers(self.title) - except Exception as err: - bu.error(exception = err) - return None - - - def print_results(self): - try: - bu.print_new_line() - self.print_headers() - for cat in self.get_categories(): - if len(cat) > self.RESULT_CAT_HEADER_LEN: - cat_name = cat[:self.RESULT_CAT_HEADER_LEN] - else: - cat_name = cat - print(self.RESULT_CAT_FRMT.format(cat_name), end=' ') - self.categories[cat].print_results() - except KeyError as err: - bu.error('Key not found', err) - except Exception as err: - bu.error(exception = err) - return None - - - def get_results_json(self) -> dict: - """Get results in JSON format""" - try: - results = dict() - results['categorization'] = self.title - results['category_key'] = self.category_key - for cat in self.get_categories(): - results[cat] = self.categories[cat].get_results() - - return results - except KeyError as err: - bu.error('Key not found', err) - return None - - - def get_results_list(self) -> list: - """Get results in list format (for CSV printing)""" - try: - results = list() - results.append(self.get_headers()) - for cat in self.get_categories(): - results.append( [cat] + self.categories[cat].get_results_list() ) - return results - except KeyError as err: - bu.error('Key not found', err) - return None + return BattleCategory() + + +class BattleCategorizationList: + @staticmethod + def mk_battle_modes(modes: dict): + """Make a list of battle modes as required for the _categorizations""" + mode_ids = modes.values() + id_max = max(mode_ids) + ret_modes = ["-"] * (id_max + 16) + for i in range(0, id_max + 16): + ret_modes[i] = "room_type=" + str(i) + + for mode in modes: + id = modes[mode] + ret_modes[id] = mode + + return ret_modes + + _battle_modes = { + "Any": 0, + "Regular": 1, + "Training Room": 2, + "Tournament": 4, + "Quick Tournament": 5, # maybe? + "Rating": 7, + "Mad Games": 8, + "Realistic Battles": 22, + "Uprising": 23, + "Gravity Mode": 24, + "Skirmish": 25, + "Burning Games": 26, + } + + _BATTLE_MODES = mk_battle_modes.__func__(_battle_modes) + + _categorizations = { + "total": ["TOTAL", "total"], + "battle_result": ["Result", "category", ["Loss", "Win", "Draw"]], + "battle_type": ["Battle Type", "category", ["Encounter", "Supremacy"]], + "room_type": ["Battle Mode", "category", _BATTLE_MODES], + "top_tier": ["Tier", "category", ["Bottom tier", "Top tier"]], + "in_platoon": ["Platoon", "category", ["Solo", "Platoon"]], + "tank_tier": ["Tank Tier", "number"], + "is_premium": ["Tank Type", "category", ["Tech tree", "Premium"]], + "tank_type": ["Tank Class", "category", WG.TANK_TYPE_STR], + "tank_nation": ["Nation", "category", WG.NATION_STR], + "mastery_badge": [ + "Battle Medal", + "category", + ["-", "3rd Class", "2nd Class", "1st Class", "Mastery"], + ], + "team_result": ["Team Result", "string"], + "player_wins": ["Player WR", "bucket", [0, 0.35, 0.45, 0.50, 0.55, 0.65], "%"], + "player_battles": [ + "Player Battles", + "bucket", + [0, 500, 1000, 2500, 5e3, 10e3, 15e3, 25e3], + "int", + ], + "player_damage_dealt": [ + "Player Avg Dmg", + "bucket", + [0, 500, 1000, 1250, 1500, 1750, 2000, 2500], + "int", + ], + "damage_made": [ + "Player Dmg made", + "bucket", + [0, 500, 1000, 1250, 1500, 1750, 2000, 2500], + "int", + ], + "enemies_destroyed": ["Player Kills", "number"], + "enemies_spotted": ["Player Spots", "number"], + "hit_rate": [ + "Player Hit Rate", + "bucket", + [0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95], + "%", + ], + "pen_rate": [ + "Player Pen Rate", + "bucket", + [0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95], + "%", + ], + "alive": [ + "Time alive (pcs)", + "bucket", + [x / 100 for x in range(0, 100, 10)], + "%", + ], + "time_alive": [ + "Time alive (s)", + "bucket", + [x * 60 for x in range(0, 8)], + "int", + ], + "battle_duration": [ + "Battle Duration", + "bucket", + [x * 60 for x in range(0, 8)], + "int", + ], + "distance_travelled": [ + "Distance Driven", + "bucket", + [x * 250 for x in range(0, 20)], + "int", + ], + "allies_wins": ["Allies WR", "bucket", [0, 0.35, 0.45, 0.50, 0.55, 0.65], "%"], + "allies_battles": [ + "Player Battles", + "bucket", + [0, 500, 1000, 2500, 5e3, 10e3, 15e3, 25e3], + "int", + ], + "allies_damage_dealt": [ + "Player Avg Dmg", + "bucket", + [0, 500, 1000, 1250, 1500, 1750, 2000, 2500], + "int", + ], + "enemies_wins": [ + "Enemies WR", + "bucket", + [0, 0.35, 0.45, 0.50, 0.55, 0.65], + "%", + ], + "enemies_battles": [ + "Player Battles", + "bucket", + [0, 500, 1000, 2500, 5e3, 10e3, 15e3, 25e3], + "int", + ], + "enemies_damage_dealt": [ + "Player Avg Dmg", + "bucket", + [0, 500, 1000, 1250, 1500, 1750, 2000, 2500], + "int", + ], + "player_name": ["Player", "string", 25], + "protagonist": ["account_id", "number"], + "tank_name": ["Tank", "string", 25], + "map_name": ["Map", "string", 20], + "battle": ["Battle", "string", 40], + "battle_i": ["Battle #", "number"], + } + + _categorizations_default = [ + "total", + "battle_result", + "battle_type", + "tank_tier", + "top_tier", + ] + + ## Player stats based categorizations + _categorizations_stats = [ + group + "_" + measure + for group in ["player", "allies", "enemies"] + for measure in ["wins", "battles", "damage_dealt"] + ] + + @classmethod + def get_categorizations_all(cls, cats: list = None) -> list: + try: + if cats is None: + return list(cls._categorizations.keys()) + else: + return list(set(cls._categorizations.keys()) & set(cats)) + except Exception as err: + bu.error(exception=err) + return None + + @classmethod + def get_categorizations_stats(cls, cats=None, stats_cats=None): + try: + if cats is None: + return cls._categorizations_stats + else: + if stats_cats is None: + return cats + player_stat_filters = set(cls._categorizations_stats) + cats = set(cls.get_categorizations_all(cats)) + if stats_cats: + cats = cats & player_stat_filters + else: + cats = cats - player_stat_filters + bu.debug(", ".join(list(cats))) + return list(cats) + except Exception as err: + bu.error(exception=err) + return None + + @classmethod + def get_categorizations_default(cls): + return cls._categorizations_default + + @classmethod + def set_categorizations_default(cls, cat_defaults: list): + try: + defaults = list() + for cat in cat_defaults: + if cat in cls.get_categorizations_all(): + defaults.append(cat) + else: + bu.error(cat + " is not a defined categorization") + cls._categorizations_default = defaults + except Exception as err: + bu.error(exception=err) + + @classmethod + def get_categorizations_extra(cls) -> list: + # return cls._categorizations_extra + return list( + set(cls._categorizations.keys()) - set(cls.get_categorizations_default()) + ) + + @classmethod + def get_categorization_title(cls, cat: str) -> str: + try: + return cls._categorizations[cat][0] + except Exception as err: + bu.error(exception=err) + return None + + @classmethod + def get_categorization_type(cls, cat: str) -> str: + try: + return cls._categorizations[cat][1] + except Exception as err: + bu.error(exception=err) + return None + + @classmethod + def get_categorization_params(cls, cat: str) -> list: + try: + if len(cls._categorizations[cat]) > 2: + return cls._categorizations[cat][2:] + except Exception as err: + bu.error(exception=err) + return None + + @classmethod + def help(cls): + for cat in cls.get_categorizations_all(): + try: + ctitle = cls.get_categorization_title(cat) + ctype = cls.get_categorization_type(cat) + cparams = cls.get_categorization_params(cat) + + if ctype == "number": + cat_tmp = BattleNumberCategorization(cat, ctitle) + elif ctype == "string": + cat_tmp = BattleStringCategorization(cat, ctitle, cparams) + elif ctype == "category": + cat_tmp = BattleClassCategorization(cat, ctitle, cparams[0]) + elif ctype == "bucket": + cat_tmp = BattleBucketCategorization( + cat, ctitle, cparams[0], cparams[1] + ) + else: + cat_tmp = BattleTotals(ctitle) + + cat_tmp.help() + + except KeyError as err: + bu.error( + "BattleCategorizationList(): Key not found: Category: " + cat, + exception=err, + ) + except Exception as err: + bu.error("BattleCategorizationList()", exception=err) + + @classmethod + def get_categorizations(cls, args: argparse.Namespace): + extra_cats = args.extra + only_extra = args.only_extra + + if only_extra: + cats = list() + else: + cats = cls.get_categorizations_default() + + if extra_cats is not None: + cats = cats + extra_cats + return cats + + def __init__(self, cats: list): + self.urls = collections.OrderedDict() + self.url_title_max_len = 0 + + cats = list(set(cats)) # remove duplicates + # ordering + self.categorizations_list = [ + cat for cat in self._categorizations if cat in cats + ] + + self.categorizations = dict() + for cat in self.categorizations_list: + try: + ctitle = self.get_categorization_title(cat) + ctype = self.get_categorization_type(cat) + cparams = self.get_categorization_params(cat) + + if ctype == "total": + self.categorizations[cat] = BattleTotals(ctitle) + elif ctype == "number": + self.categorizations[cat] = BattleNumberCategorization(cat, ctitle) + elif ctype == "string": + self.categorizations[cat] = BattleStringCategorization( + cat, ctitle, cparams + ) + elif ctype == "category": + self.categorizations[cat] = BattleClassCategorization( + cat, ctitle, cparams[0] + ) + elif ctype == "bucket": + self.categorizations[cat] = BattleBucketCategorization( + cat, ctitle, cparams[0], cparams[1] + ) + + except KeyError as err: + bu.error( + "BattleCategorizationList(): Key not found: Category: " + cat, + exception=err, + ) + except Exception as err: + bu.error("BattleCategorizationList()", exception=err) + + def __iter__(self): + """Returns the Iterator object""" + return BattleCategorizationListIterator(self) + + def len(self): + return len(self.categorizations) + + def get_categorization(self, cat: str): + """Return a BattleCategorization for a category""" + try: + return self.categorizations[cat] + except Exception as err: + bu.error(exception=err) + return None + + def record_result(self, result: dict): + try: + for cat in self.categorizations_list: + self.categorizations[cat].record_result(result) + except Exception as err: + bu.error(exception=err) + return None + + def calc_results(self): + try: + for cat in self.categorizations_list: + self.categorizations[cat].calc_results() + except Exception as err: + bu.error(exception=err) + return None + + def record_url(self, result): + try: + self.url_title_max_len = max(self.url_title_max_len, len(result["title"])) + self.urls[result["title"]] = result["url"] + except Exception as err: + bu.error(exception=err) + return None + + def print_results(self): + try: + for cat in self.categorizations_list: + self.categorizations[cat].print_results() + self.print_urls() + + except Exception as err: + bu.error(exception=err) + + def print_urls(self): + try: + if len(self.urls) > 0: + print("\nURLs to Battle replays:") + for title in self.urls: + print( + ("{:" + str(3 + self.url_title_max_len) + "}").format(title) + + " : " + + self.urls[title] + ) + except Exception as err: + bu.error(exception=err) + + def get_filter_categories(self, cat: str, filters: list) -> dict: + try: + return self.categorizations[str(cat)].get_filter_categories(filters) + except Exception as err: + bu.error(exception=err) + return None + + def get_categories(self, result: dict) -> dict: + """Get categories for each categorization in the CategorizationList() for a battle result""" + try: + res = dict() + for cat in self.categorizations_list: + res[cat] = self.categorizations[cat].get_category(result) + return res + except Exception as err: + bu.error(exception=err) + return None + + def get_results_json(self): + try: + res = dict() + for cat in self.categorizations_list: + res[cat] = self.categorizations[cat].get_results_json() + res["replay_urls"] = self.urls + return res + except Exception as err: + bu.error(exception=err) + return None + + def get_results_list(self, urls: bool = False): + try: + res = list() + for cat in self.categorizations_list: + res.append("") + res.extend(self.categorizations[cat].get_results_list()) + if urls: + res.append("") + for url in self.urls: + res.append(url) + return res + except Exception as err: + bu.error(exception=err) + return None + + +class BattleCategorizationListIterator: + def __init__(self, btl_cats: BattleCategorizationList) -> None: + self._btl_cats = btl_cats + self._ndx = 0 + + def __next__(self): + if self._ndx < len(self._btl_cats.categorizations_list): + cat = self._btl_cats.categorizations_list[self._ndx] + res = self._btl_cats.categorizations[cat] + self._ndx += 1 + return res + raise StopIteration + + +class BattleCategorization: + RESULT_CAT_HEADER_LEN = 15 + RESULT_CAT_HEADER_FRMT = "{:_<" + str(RESULT_CAT_HEADER_LEN) + "s}" + RESULT_CAT_FRMT = "{:>" + str(RESULT_CAT_HEADER_LEN) + "s}" + + def __init__(self, cat_key: str, title: str): + self.total_battles = 0 + self.category_key = cat_key + self.title = title + self.categories = collections.defaultdict(def_value_BattleCategory) + + def set_cat_header_format(self, header_len: int, left_aligned: bool = False): + self.RESULT_CAT_HEADER_LEN = header_len + self.RESULT_CAT_HEADER_FRMT = "{:_<" + str(header_len) + "s}" + if left_aligned: + self.RESULT_CAT_FRMT = "{:<" + str(header_len) + "s}" + else: + self.RESULT_CAT_FRMT = "{:>" + str(header_len) + "s}" + + def get_categories(self): + """Get category keys sorted""" + return sorted(self.categories.keys(), key=str.casefold) + + ### PER Child Class + def get_category(self, result: dict) -> str: + """Get category. Must be implemented in the child classes""" + return "NONE" + + def get_filter_categories(self, filters: list) -> list: + try: + if type(filters) != list: + filters = [filters] + return [x for x in filters if x in self.get_categories()] + except Exception as err: + bu.error("Category: " + str(self.category_key), exception=err) + return None + + def help(self): + """Help""" + FORMAT = "\t{:<15s}\t{}. Options:" + print(FORMAT.format(self.category_key, self.title), end=" ") + + def record_result(self, result: dict) -> bool: + try: + cat = self.get_category(result) + self.categories[cat].record_result(result) + self.total_battles += 1 + return True + except KeyError as err: + bu.error("Category key not found: " + cat, exception=err) + except Exception as err: + bu.error("Category key: " + cat, exception=err) + return False + + def calc_results(self): + try: + for cat in self.get_categories(): + self.categories[cat].calc_results(self.total_battles) + except Exception as err: + bu.error(exception=err) + + def print_headers(self): + try: + if len(self.title) > self.RESULT_CAT_HEADER_LEN: + cat_name = self.title[: self.RESULT_CAT_HEADER_LEN] + else: + cat_name = self.title + header = self.RESULT_CAT_HEADER_FRMT.format(cat_name) + BattleCategory.print_headers(header) + return None + except Exception as err: + bu.error(exception=err) + + def get_headers(self) -> list: + """Get BattleCategory headers as list""" + try: + return BattleCategory.get_headers(self.title) + except Exception as err: + bu.error(exception=err) + return None + + def print_results(self): + try: + bu.print_new_line() + self.print_headers() + for cat in self.get_categories(): + if len(cat) > self.RESULT_CAT_HEADER_LEN: + cat_name = cat[: self.RESULT_CAT_HEADER_LEN] + else: + cat_name = cat + print(self.RESULT_CAT_FRMT.format(cat_name), end=" ") + self.categories[cat].print_results() + except KeyError as err: + bu.error("Key not found", err) + except Exception as err: + bu.error(exception=err) + return None + + def get_results_json(self) -> dict: + """Get results in JSON format""" + try: + results = dict() + results["categorization"] = self.title + results["category_key"] = self.category_key + for cat in self.get_categories(): + results[cat] = self.categories[cat].get_results() + + return results + except KeyError as err: + bu.error("Key not found", err) + return None + + def get_results_list(self) -> list: + """Get results in list format (for CSV printing)""" + try: + results = list() + results.append(self.get_headers()) + for cat in self.get_categories(): + results.append([cat] + self.categories[cat].get_results_list()) + return results + except KeyError as err: + bu.error("Key not found", err) + return None class BattleTotals(BattleCategorization): - TOTAL = 'Total' + TOTAL = "Total" - def __init__(self, title: str): - super().__init__(self.TOTAL, title) + def __init__(self, title: str): + super().__init__(self.TOTAL, title) + def get_category(self, result: dict) -> str: + return self.TOTAL - def get_category(self, result: dict) -> str: - return self.TOTAL + def get_categories(self) -> str: + return [self.TOTAL] - - def get_categories(self) -> str: - return [ self.TOTAL ] - - - def help(self): - super().help() - print('-') + def help(self): + super().help() + print("-") class BattleClassCategorization(BattleCategorization): - """Class for categorization by fixed classes with ID (int)""" - - def __init__(self, cat_key : str, title: str, classes: list): - super().__init__(cat_key, title) - self.category_labels = classes + """Class for categorization by fixed classes with ID (int)""" + def __init__(self, cat_key: str, title: str, classes: list): + super().__init__(cat_key, title) + self.category_labels = classes - def help(self): - super().help() - labels = list() - for ndx in range(0, len(self.category_labels)): - labels.append(str(ndx) + '=' + self.category_labels[ndx]) - print(', '.join(labels)) + def help(self): + super().help() + labels = list() + for ndx in range(0, len(self.category_labels)): + labels.append(str(ndx) + "=" + self.category_labels[ndx]) + print(", ".join(labels)) + def get_category(self, result: dict) -> str: + """Get category""" + cat_id = -1 + try: + cat_id = result[self.category_key] + return self.category_labels[cat_id] + except KeyError as err: + bu.error( + "Category: " + str(self.category_key) + " ID: " + str(cat_id), + exception=err, + ) + except Exception as err: + bu.error( + "Category: " + str(self.category_key) + " ID: " + str(cat_id), + exception=err, + ) + return None - def get_category(self, result: dict) -> str: - """Get category""" - cat_id = -1 - try: - cat_id = result[self.category_key] - return self.category_labels[cat_id] - except KeyError as err: - bu.error('Category: ' + str(self.category_key) + ' ID: ' + str(cat_id), exception=err) - except Exception as err: - bu.error('Category: ' + str(self.category_key) + ' ID: ' + str(cat_id), exception=err) - return None + def get_filter_categories(self, filters: list) -> list: + try: + # bu.debug(str(filters), force=True) + if type(filters) != list: + filters = [filters] + bu.debug(str(self.category_labels)) + res = list() + for ndx in filters: + if ndx in range(0, len(self.category_labels)): + res.append(self.category_labels[ndx]) + return res + # return [ self.category_labels[int(x)] for x in filters if x in range(0, len(self.get_categories())) ] + except Exception as err: + bu.error("Category: " + str(self.category_key), exception=err) + return None - - def get_filter_categories(self, filters: list) -> list: - try: - # bu.debug(str(filters), force=True) - if type(filters) != list: - filters = [ filters ] - bu.debug(str(self.category_labels)) - res = list() - for ndx in filters: - if ndx in range(0,len(self.category_labels)): - res.append(self.category_labels[ndx]) - return res - #return [ self.category_labels[int(x)] for x in filters if x in range(0, len(self.get_categories())) ] - except Exception as err: - bu.error('Category: ' + str(self.category_key), exception=err) - return None - - - def get_categories(self) -> list: - try: - cats = list() - for cat in self.category_labels: - if cat in self.categories.keys(): - cats.append(cat) - return cats - except KeyError as err: - bu.error('Key not found', exception = err) - except Exception as err: - bu.error(exception = err) - return None + def get_categories(self) -> list: + try: + cats = list() + for cat in self.category_labels: + if cat in self.categories.keys(): + cats.append(cat) + return cats + except KeyError as err: + bu.error("Key not found", exception=err) + except Exception as err: + bu.error(exception=err) + return None class BattleStringCategorization(BattleCategorization): - """Class for categorization by string""" + """Class for categorization by string""" - def __init__(self, cat_key : str, title: str, params: list): - super().__init__(cat_key, title) - if params != None and len(params) > 0: - width = params[0] - left_align = True # align long category names left by default - if len(params) > 1: - left_align = params[1] - - self.set_cat_header_format(width, left_align) + def __init__(self, cat_key: str, title: str, params: list): + super().__init__(cat_key, title) + if params is not None and len(params) > 0: + width = params[0] + left_align = True # align long category names left by default + if len(params) > 1: + left_align = params[1] + self.set_cat_header_format(width, left_align) - def get_category(self, result: dict) -> str: - """Get category""" - try: - return result[self.category_key] - except KeyError as err: - bu.error('Category key: ' + self.category_key + ' not found', exception=err) - except Exception as err: - bu.error('Category: ' + self.category_key, exception=err) - return None - + def get_category(self, result: dict) -> str: + """Get category""" + try: + return result[self.category_key] + except KeyError as err: + bu.error("Category key: " + self.category_key + " not found", exception=err) + except Exception as err: + bu.error("Category: " + self.category_key, exception=err) + return None - def get_categories(self): - """Get category keys sorted""" - try: - p_number_sort = re.compile(r'^(\d+): .+') - cats = list(self.categories.keys()) - if (p_number_sort.match(cats[0]) != None): - ndxs = sorted(range(len(cats)), key=lambda k: int(cats[k][:cats[k].find(':')]) ) - return [ cats[i] for i in ndxs ] - else: - return sorted(cats , key=str.casefold) + def get_categories(self): + """Get category keys sorted""" + try: + p_number_sort = re.compile(r"^(\d+): .+") + cats = list(self.categories.keys()) + if p_number_sort.match(cats[0]) is not None: + ndxs = sorted( + range(len(cats)), key=lambda k: int(cats[k][: cats[k].find(":")]) + ) + return [cats[i] for i in ndxs] + else: + return sorted(cats, key=str.casefold) - except Exception as err: - bu.error('Category: ' + self.category_key, exception=err) - return None + except Exception as err: + bu.error("Category: " + self.category_key, exception=err) + return None - def get_filter_categories(self, filters: list) -> list: - try: - if type(filters) != list: - filters = [ filters ] - return [ x for x in filters if x in self.get_categories() ] - # return filters - except Exception as err: - bu.error('Category: ' + str(self.category_key), exception=err) - return None + def get_filter_categories(self, filters: list) -> list: + try: + if type(filters) != list: + filters = [filters] + return [x for x in filters if x in self.get_categories()] + # return filters + except Exception as err: + bu.error("Category: " + str(self.category_key), exception=err) + return None - - def help(self): - super().help() - print(self.title + ' name') + def help(self): + super().help() + print(self.title + " name") class BattleNumberCategorization(BattleCategorization): - """Class for categorization by number""" + """Class for categorization by number""" - def get_category(self, result: dict) -> str: - """Get category as """ - try: - return str(result[self.category_key]) - except KeyError as err: - bu.error('Category: ' + str(self.category_key), exception=err) - - except Exception as err: - bu.error('Category: ' + str(self.category_key), exception=err) - return None + def get_category(self, result: dict) -> str: + """Get category as""" + try: + return str(result[self.category_key]) + except KeyError as err: + bu.error("Category: " + str(self.category_key), exception=err) + except Exception as err: + bu.error("Category: " + str(self.category_key), exception=err) + return None - def get_categories(self) -> list: - try: - return map(str, sorted( map(int, self.categories.keys()))) - except KeyError as err: - bu.error('Key not found', exception=err) - return None + def get_categories(self) -> list: + try: + return map(str, sorted(map(int, self.categories.keys()))) + except KeyError as err: + bu.error("Key not found", exception=err) + return None + def get_filter_categories(self, filters: list) -> list: + try: + if type(filters) != list: + filters = [filters] + return list(map(str, filters)) + except Exception as err: + bu.error("Category: " + str(self.category_key), exception=err) + return None - def get_filter_categories(self, filters: list) -> list: - try: - if type(filters) != list: - filters = [ filters ] - return list(map(str,filters)) - except Exception as err: - bu.error('Category: ' + str(self.category_key), exception=err) - return None - - - def help(self): - super().help() - print('INTEGER') + def help(self): + super().help() + print("INTEGER") class BattleBucketCategorization(BattleCategorization): - """Class for categorization by buckets based on value""" - - def __init__(self, cat_key : str, title: str, breaks: list, bucket_format = ''): - try: - super().__init__(cat_key, title) - self.breaks = breaks - self.bucket_format = bucket_format - self.category_labels = self.mk_category_labels(breaks, bucket_format) - except Exception as err: - bu.error('BattleBucketCategorization()', exception=err) - - - def help(self): - super().help() - labels = list() - for ndx in range(0, len(self.category_labels)): - labels.append(str(ndx) + '=' + self.category_labels[ndx]) - print(', '.join(labels)) - - - def get_category(self, result: dict) -> str: - """Get category""" - cat_id = -1 - value = None - try: - value = result[self.category_key] - cat_id = self.find_bucket(value, self.breaks) - return self.category_labels[cat_id] - except KeyError as err: - bu.error('Category: ' + str(self.category_key) + ' value: ' + str(value) + ' cat ID: ' + str(cat_id), exception=err) - except Exception as err: - bu.error('Category: ' + str(self.category_key) + ' value: ' + str(value) + ' cat ID: ' + str(cat_id), exception=err) - return None - - - def get_categories(self) -> list: - try: - cats = list() - for cat in self.category_labels: - if cat in self.categories.keys(): - cats.append(cat) - return cats - except KeyError as err: - bu.error('Key not found', exception = err) - except Exception as err: - bu.error(exception = err) - return None - - - def get_filter_categories(self, filters: list) -> list: - try: - if type(filters) != list: - filters = [ filters ] - res = list() - for ndx in filters: - if ndx in range(0,len(self.category_labels)): - res.append(self.category_labels[ndx]) - return res - except Exception as err: - bu.error('Category: ' + str(self.category_key), exception=err) - return None - - - def find_bucket(self, value, bucket_breaks: list): - """Find bucket for value""" - try: - max = len(bucket_breaks) - if max <= 1: - return 0 - ndx = int(max//2) - if value > bucket_breaks[ndx]: - return ndx + self.find_bucket(value, bucket_breaks[ndx:]) - elif value < bucket_breaks[ndx]: - return self.find_bucket(value, bucket_breaks[:ndx]) - else: - return ndx - except KeyError as err: - bu.error('Key not found ndx=' + str(ndx) + ', value=' + str(value), exception = err) - except Exception as err: - bu.error('ndx=' + str(ndx) + ', value=' + str(value), exception = err) - return -2 - - - def mk_category_labels(self, bucket_breaks: list, bucket_format: str) -> list: - try: - # add open-ended last category. list.append() modifies by reference self.breaks (not OK)!! - bucket_breaks = bucket_breaks + [ None ] - multiplier = 1 - prefix = '' - if bucket_format == '%': - frmt = '{:2.0f}' - multiplier = 100 - prefix = '%' - elif bucket_format == 'float': - frmt = '{:.2f}' - elif bucket_format == 'int': - frmt = '{:.0f}' - else: - frmt = '{:.0f}' - labels = list() - for ndx in range(0,len(bucket_breaks) - 1): - low = bucket_breaks[ndx] - high = bucket_breaks[ndx +1] - - # None makes open ended category label: '1500 - ' - if low == None: - label = ' - ' - else: - label = frmt.format(low*multiplier) + ' - ' - - if high == None: - label = label + ' ' - else: - label = label + frmt.format(high*multiplier) - - label = label + prefix - labels.append(label) - return labels - - except KeyError as err: - bu.error('KeyError: ndx=' + str(ndx) + ' buckets=' + str(bucket_breaks), exception=err) - except Exception as err: - bu.error('Unknown error', exception=err) - return None - - -class BattleCategory(): - - ## Syntax: Check how the replay JSON files look. The algorithm is counting/recording fields - _result_fields = { - 'battles' : [ 'Battles', 'Number of battles', 7, '{:7.0f}' ], - 'battles%' : [ '% Battles', 'Share of Battles', 9, '{:9.0%}' ], - 'win' : [ 'WR', 'Win rate', 6, '{:6.1%}' ], - 'damage_made' : [ 'DPB', 'Average Damage', 5, '{:5.0f}' ], - 'damage_received' : [ 'DPBr', 'Average Damage Received', 5, '{:5.0f}' ], - 'enemies_destroyed' : [ 'KPB', 'Kills / Battle', 4, '{:4.2f}' ], - 'DR' : [ 'DR', 'Damage Ratio', 5, '{:5.1f}' ], - 'KDR' : [ 'KDR', 'Kills / Death', 4, '{:4.1f}' ], - 'enemies_spotted' : [ 'Spot', 'Enemies spotted per battle', 4, '{:4.1f}' ], - 'hit_rate' : [ 'Hit rate', 'Shots hit / all shots made', 8, '{:8.1%}' ], - 'shots_made' : [ 'Shots', 'Shots made', 5, '{:5.1f}' ], - 'hits_received' : [ 'Hits R.', 'Hits reveived', 7, '{:7.1f}' ], - 'hits_bounced' : [ 'Bounced R.', 'Hits & bounced reveived', 10, '{:10.1f}' ], - 'in_platoon' : [ 'Plat%', 'Platoon rate', 6, '{:6.1%}' ], - 'pen_rate' : [ 'Pen rate', 'Shots pen / shots hit', 8, '{:8.1%}' ], - 'pen_rate_received' : [ 'Penned%', 'Received hits pen / hits received', 7, '{:7.1%}' ], - 'splash_rate' : [ 'Splash%', 'HE splash shots / shots hit', 7, '{:7.1%}' ], - 'splash_rate_received' : [ 'Splashed%', 'HE splash shots received / hits hit', 9, '{:9.1%}' ], - 'survived' : [ 'Surv%', 'Survival rate', 6, '{:6.1%}' ], - 'time_alive' : [ 'T alive', 'Time being alive in a battle in secs', 7, '{:7.0f}' ], - 'alive' : [ 'Share live', 'Percentage of time being alive in a battle', 8, '{:8.0%}' ], - 'battle_duration' : [ 'Duration', 'Battle duration', 8, '{:8.0f}' ], - 'distance_travelled': [ 'Distance', 'Distance travelled', 8, '{:8.0f}' ], - 'top_tier' : [ 'Top tier', 'Share of games as top tier', 8, '{:8.0%}' ], - 'player_wins' : [ 'Player WR', 'Average WR of the player', 9, '{:9.2%}' ], - 'allies_wins' : [ 'Allies WR', 'Average WR of allies at the tier of their tank', 9, '{:9.2%}' ], - 'enemies_wins' : [ 'Enemies WR', 'Average WR of enemies at the tier of their tank', 10, '{:10.2%}' ], - 'player_battles' : [ 'Player Btls', 'Average number battles of the player', 11, '{:11.0f}' ], - 'allies_battles' : [ 'Allies Btls', 'Average number battles of the allies', 11, '{:11.0f}' ], - 'enemies_battles' : [ 'Enemies Btls', 'Average number battles of the enemies', 12, '{:12.0f}' ], - 'player_damage_dealt' : [ 'Player Avg Dmg', 'Player Average damage', 14, '{:14.0f}' ], - 'allies_damage_dealt' : [ 'Allies Avg Dmg', 'Average damage of the allies', 14, '{:14.0f}' ], - 'enemies_damage_dealt' : [ 'Enemies Avg Dmg', 'Average damage of the enemies', 15, '{:14.0f}' ], - MISSING_STATS : [ 'No stats', 'Players without stats avail', 8, '{:8.1%}'] - } - - - # fields to display in results - _result_fields_modes = { - 'default' : [ 'battles', 'win','damage_made', 'enemies_destroyed', 'enemies_spotted', - 'top_tier', 'DR', 'survived', 'allies_wins', 'enemies_wins'], - 'team' : [ 'battles', 'win', 'in_platoon', 'player_wins', 'allies_wins','enemies_wins'], - 'team_extended' : [ 'battles', 'battles%', 'win', 'in_platoon', 'player_wins', 'allies_wins', 'enemies_wins', - 'player_damage_dealt', 'allies_damage_dealt', 'enemies_damage_dealt', - 'player_battles','allies_battles','enemies_battles' ], - 'extended' : [ 'battles', 'battles%', 'win', 'in_platoon', 'damage_made', 'enemies_destroyed', 'enemies_spotted', - 'top_tier', 'DR', 'KDR', 'hit_rate', 'pen_rate', 'survived', 'alive', - 'player_wins', 'allies_wins', 'enemies_wins', MISSING_STATS ], - 'dmg_received' : [ 'battles', 'win','damage_made', 'damage_received', 'enemies_destroyed', 'top_tier', 'DR', - 'hits_received', 'hits_bounced', 'pen_rate_received', 'splash_rate_received'], - 'all' : [ cat for cat in _result_fields.keys() ] - } - - _team_fields = [ 'wins', 'battles', 'damage_dealt' ] - - count_fields = [ - 'battles', - MISSING_STATS, - 'mastery_badge' - ] - - _result_ratios = { - 'KDR' : [ 'enemies_destroyed', 'destroyed' ], - 'DR' : [ 'damage_made', 'damage_received' ], - 'hit_rate' : [ 'shots_hit', 'shots_made' ], - 'splash_rate' : [ 'shots_splash', 'shots_hit' ], - 'splash_rate_received' : [ 'hits_splash', 'hits_received' ], - 'pen_rate' : [ 'shots_pen', 'shots_hit' ], - 'pen_rate_received' : [ 'hits_pen', 'hits_received' ], - 'dmg_block_rate' : [ 'damage_blocked', 'damage_received' ] - } - - # _extended_stats = False - - result_fields = list() - avg_fields = set() - ratio_fields = set() - - - @classmethod - def get_modes(cls) -> list: - return list(cls._result_fields_modes.keys()) - - @classmethod - def get_mode_fields(cls, mode: str): - try: - return cls._result_fields_modes[mode] - except Exception as err: - bu.error(exception=err) - return None - - - @classmethod - def set_fields(cls, mode: str = None): - try: - if mode not in cls.get_modes(): - bu.error('Mode ' + mode + ' not in defined modes: ' + ', '.join(cls.get_modes())) - sys.exit(2) - - cls.mode = mode - cls.result_fields = cls.get_mode_fields(cls.mode) - cls.result_fields_ratio = set(cls._result_ratios.keys()) & set(cls.result_fields) - cls.avg_fields = set(cls.result_fields) - set(cls._result_ratios.keys()) - set(cls.count_fields) - - for ratio in cls.result_fields_ratio: - cls.ratio_fields.add(cls._result_ratios[ratio][0]) - cls.ratio_fields.add(cls._result_ratios[ratio][1]) - except Exception as err: - bu.error('BattleCategory:' ,exception=err) - - - @classmethod - def get_result_fields(cls) -> list: - return cls.result_fields - - - @classmethod - def get_result_fields_all(cls) -> list: - return cls._result_fields.keys() - - @classmethod - def get_result_fields_ratio(cls, all: bool = False) -> list: - if all: - return cls._result_ratios.keys() - else: - return cls.result_fields_ratio - - - @classmethod - def get_avg_fields(cls) -> set: - return cls.avg_fields - - - @classmethod - def get_ratio_fields(cls) -> set: - return cls.ratio_fields - - - @classmethod - def get_field_name(cls, field: str) -> str: - try: - return cls._result_fields[field][0] - except Exception as err: - bu.error(exception=err) - - - @classmethod - def get_field_description(cls, field: str) -> str: - try: - return cls._result_fields[field][1] - except Exception as err: - bu.error(exception=err) - - - @classmethod - def get_field_width(cls, field: str): - try: - return cls._result_fields[field][2] - except Exception as err: - bu.error(exception=err) - - - @classmethod - def get_fields_team(cls) -> list: - return cls._team_fields - - - @classmethod - def calc_ratio(cls, ratio: str, result: dict): - try: - value = result[cls._result_ratios[ratio][0]] - divider = result[cls._result_ratios[ratio][1]] - if divider != 0: - if value == None: - value = 0 - return value / divider - else: - if value == 0: - return 0 - else: - return float('Inf') - except Exception as err: - bu.error('Replay _id=' + result['_id'] , exception=err) - return None - - - @classmethod - def print_headers(cls, header: str): - try: - headers = [ header ] - for field in cls.result_fields: - print_format = '{:^' + str(cls.get_field_width(field)) + 's}' - headers.append(print_format.format(cls.get_field_name(field))) - print(' '.join(headers)) - except KeyError as err: - bu.error('Key not found', exception=err) - except Exception as err: - bu.error(exception=err) - return None - - - @classmethod - def get_headers(cls, header: str = None) -> list: - try: - headers = [ header ] - for field in cls.result_fields: - headers.append(cls.get_field_name(field)) - return headers - except KeyError as err: - bu.error('Key not found', exception=err) - except Exception as err: - bu.error(exception=err) - return None - - - @classmethod - def help(cls): - try: - for field in cls.get_result_fields_all(): - print('{:24s} : {}'.format(field, cls.get_field_description(field))) - except Exception as err: - bu.error(exception=err) - return None - - - - def __init__(self): - try: - - self.battles = 0 - self.missing_stats = 0 - self.n_players = 0 - self.ratios_ready = False - self.results_ready = False - self.results = collections.defaultdict(def_value_zero) - - except KeyError as err: - bu.error('BattleCategory(): Key not found', err) - - - def record_result(self, result : dict) -> bool: - try: - for field in self.avg_fields | self.ratio_fields: - if (field in result) and (result[field] != None): - self.results[field] += result[field] - self.battles += 1 - self.n_players += result[N_PLAYERS] - self.missing_stats += result[MISSING_STATS] - return True - except KeyError as err: - bu.error('Key not found', err) - bu.error(str(result)) - except Exception as err: - bu.error('BattleCategory:' ,exception=err) - return False - - - def calc_ratios(self) -> bool: - if self.ratios_ready: - return True - try: - for field in self.result_fields_ratio: - if self.results[self._result_ratios[field][1]] != 0: - self.results[field] = self.results[self._result_ratios[field][0]] / self.results[self._result_ratios[field][1]] - else: - self.results[field] = float('Inf') - self.ratios_ready = True - return True - except KeyError as err: - bu.error('Key not found', err) - return False - - - def calc_results(self, total_battles: int = None): - if self.results_ready == True: - return True - if not self.ratios_ready: - self.calc_ratios() - try: - for field in self.avg_fields: - self.results[field] = self.results[field] / max(self.battles,1) - self.results['battles'] = self.battles - self.results['battles%'] = self.battles / total_battles - self.results[MISSING_STATS] = self.missing_stats / max(self.n_players, 1) - self.results_ready = True - return True - except KeyError as err: - bu.error('Key not found', err) - except Exception as err: - bu.error(exception=err) - return False - - - def get_results(self) -> dict: - """Return results as a dict""" - if not self.results_ready: - bu.error('Stats have not been calculated yet. call calc_results() before get_results()') - try: - results = dict() - for field in self.result_fields: - results[field] = self.results[field] - return results - except KeyError as err: - bu.error('Key not found', err) - except Exception as err: - bu.error(exception=err) - return None - - - def get_results_list(self) -> list: - """Return results as a list""" - if not self.results_ready: - bu.error('Stats have not been calculated yet. call calc_results() before get_results()') - try: - results = list() - for field in self.result_fields: - results.append(self._result_fields[field][3].format(self.results[field])) - return results - except KeyError as err: - bu.error('Key not found', err) - except Exception as err: - bu.error(exception=err) - return None - - - def print_results(self): - if not self.results_ready: - bu.error('Stats have not been calculated yet. call calc_results() before printing()') - try: - results = [] - for field in self.result_fields: - results.append(self._result_fields[field][3].format(self.results[field]) ) - print(' : '.join(results)) - return True - except KeyError as err: - bu.error('Key not found', err) - except Exception as err: - bu.error(exception=err) - return False - - -class PlayerHistogram(): - def __init__(self, field: str, name: str, fields : list, factor: float, cat_format: str ): - self.field = field - if self.field == 'wins': - self.name = StatFunc.get_title() - else: - self.name = name - self.fields = fields - self.cat_factor = factor - self.cat_format = cat_format - self.ncat = len(self.fields) - 1 - self.allies = [0] * self.ncat - self.enemies = [0] * self.ncat - self.total = [0] * self.ncat - self.results = None - - def record_ally(self, stat: float): - for cat in range(0, self.ncat): - if self.fields[cat] <= stat <= self.fields[cat+1]: - self.allies[cat] += 1 - self.total[cat] += 1 - return True - return False - - def record_enemy(self, stat: float): - for cat in range(0, self.ncat): - if self.fields[cat] <= stat <= self.fields[cat+1]: - self.enemies[cat] += 1 - self.total[cat] += 1 - return True - return False - - def get_category_name(self, cat: int): - if cat < self.ncat - 1: - return self.format_category(cat) + ' - ' + self.format_category(cat+1) - else: - return self.format_category(cat) + ' -' - - - def format_category(self, cat: int): - val = self.fields[cat] - return self.cat_format.format(float(val * self.cat_factor)) - - - def calc_results(self): - N_enemies = 0 - N_allies = 0 - res = dict() - # calculate buckets - for cat in range(0, self.ncat): - N_enemies += self.enemies[cat] - N_allies += self.allies[cat] - N_total = N_allies + N_enemies - - for cat in range(0, self.ncat): - stat = dict() - stat['allies'] = self.allies[cat] - stat['allies%'] = stat['allies'] / N_allies - stat['enemies'] = self.enemies[cat] - stat['enemies%'] = stat['enemies'] / N_enemies - stat['total'] = self.allies[cat] + self.enemies[cat] - stat['total%'] = stat['total'] / N_total - - res[self.get_category_name(cat)] = stat - - self.results = res - return res - - - def get_results(self) -> dict: - return self.results - - - def get_results_list(self) -> list: - try: - if (self.results != None): - res = list() - res.append([self.name, "Allies", "Allies %", "Enemies", "Enemies %" ,"TOTAL", "TOTAL %"]) - for cat in self.results: - stat = self.results[cat] - res.append([ cat, stat['allies'], stat['allies%'], - stat['enemies'], stat['enemies%'], - stat['total'], stat['total%'] ]) - return res - else: - bu.error('Results have not been calculated yet.') - return None - except Exception as err: - bu.error(exception=err) - return None - - - def print(self): - try: - if (self.results != None): - print("\n{:12s} | {:13s} | {:13s} | {:13s}".format(self.name, "Allies", "Enemies", "TOTAL")) - for cat in self.results: - stat = self.results[cat] - print("{:12s} | {:5d} ({:5.1%}) | {:5d} ({:5.1%}) | {:5d} ({:5.1%})".format(cat, stat['allies'], stat['allies%'], stat['enemies'], stat['enemies%'], stat['total'], stat['total%'] )) - else: - bu.error('Results have not been calculated yet.') - except Exception as err: - bu.error(exception=err) - return None + """Class for categorization by buckets based on value""" + + def __init__(self, cat_key: str, title: str, breaks: list, bucket_format=""): + try: + super().__init__(cat_key, title) + self.breaks = breaks + self.bucket_format = bucket_format + self.category_labels = self.mk_category_labels(breaks, bucket_format) + except Exception as err: + bu.error("BattleBucketCategorization()", exception=err) + + def help(self): + super().help() + labels = list() + for ndx in range(0, len(self.category_labels)): + labels.append(str(ndx) + "=" + self.category_labels[ndx]) + print(", ".join(labels)) + + def get_category(self, result: dict) -> str: + """Get category""" + cat_id = -1 + value = None + try: + value = result[self.category_key] + cat_id = self.find_bucket(value, self.breaks) + return self.category_labels[cat_id] + except KeyError as err: + bu.error( + "Category: " + + str(self.category_key) + + " value: " + + str(value) + + " cat ID: " + + str(cat_id), + exception=err, + ) + except Exception as err: + bu.error( + "Category: " + + str(self.category_key) + + " value: " + + str(value) + + " cat ID: " + + str(cat_id), + exception=err, + ) + return None + + def get_categories(self) -> list: + try: + cats = list() + for cat in self.category_labels: + if cat in self.categories.keys(): + cats.append(cat) + return cats + except KeyError as err: + bu.error("Key not found", exception=err) + except Exception as err: + bu.error(exception=err) + return None + + def get_filter_categories(self, filters: list) -> list: + try: + if type(filters) != list: + filters = [filters] + res = list() + for ndx in filters: + if ndx in range(0, len(self.category_labels)): + res.append(self.category_labels[ndx]) + return res + except Exception as err: + bu.error("Category: " + str(self.category_key), exception=err) + return None + + def find_bucket(self, value, bucket_breaks: list): + """Find bucket for value""" + try: + max = len(bucket_breaks) + if max <= 1: + return 0 + ndx = int(max // 2) + if value > bucket_breaks[ndx]: + return ndx + self.find_bucket(value, bucket_breaks[ndx:]) + elif value < bucket_breaks[ndx]: + return self.find_bucket(value, bucket_breaks[:ndx]) + else: + return ndx + except KeyError as err: + bu.error( + "Key not found ndx=" + str(ndx) + ", value=" + str(value), exception=err + ) + except Exception as err: + bu.error("ndx=" + str(ndx) + ", value=" + str(value), exception=err) + return -2 + + def mk_category_labels(self, bucket_breaks: list, bucket_format: str) -> list: + try: + # add open-ended last category. list.append() modifies by reference self.breaks (not OK)!! + bucket_breaks = bucket_breaks + [None] + multiplier = 1 + prefix = "" + if bucket_format == "%": + frmt = "{:2.0f}" + multiplier = 100 + prefix = "%" + elif bucket_format == "float": + frmt = "{:.2f}" + elif bucket_format == "int": + frmt = "{:.0f}" + else: + frmt = "{:.0f}" + labels = list() + for ndx in range(0, len(bucket_breaks) - 1): + low = bucket_breaks[ndx] + high = bucket_breaks[ndx + 1] + + # None makes open ended category label: '1500 - ' + if low is None: + label = " - " + else: + label = frmt.format(low * multiplier) + " - " + + if high is None: + label = label + " " + else: + label = label + frmt.format(high * multiplier) + + label = label + prefix + labels.append(label) + return labels + + except KeyError as err: + bu.error( + "KeyError: ndx=" + str(ndx) + " buckets=" + str(bucket_breaks), + exception=err, + ) + except Exception as err: + bu.error("Unknown error", exception=err) + return None + + +class BattleCategory: + ## Syntax: Check how the replay JSON files look. The algorithm is counting/recording fields + _result_fields = { + "battles": ["Battles", "Number of battles", 7, "{:7.0f}"], + "battles%": ["% Battles", "Share of Battles", 9, "{:9.0%}"], + "win": ["WR", "Win rate", 6, "{:6.1%}"], + "damage_made": ["DPB", "Average Damage", 5, "{:5.0f}"], + "damage_received": ["DPBr", "Average Damage Received", 5, "{:5.0f}"], + "enemies_destroyed": ["KPB", "Kills / Battle", 4, "{:4.2f}"], + "DR": ["DR", "Damage Ratio", 5, "{:5.1f}"], + "KDR": ["KDR", "Kills / Death", 4, "{:4.1f}"], + "enemies_spotted": ["Spot", "Enemies spotted per battle", 4, "{:4.1f}"], + "hit_rate": ["Hit rate", "Shots hit / all shots made", 8, "{:8.1%}"], + "shots_made": ["Shots", "Shots made", 5, "{:5.1f}"], + "hits_received": ["Hits R.", "Hits reveived", 7, "{:7.1f}"], + "hits_bounced": ["Bounced R.", "Hits & bounced reveived", 10, "{:10.1f}"], + "in_platoon": ["Plat%", "Platoon rate", 6, "{:6.1%}"], + "pen_rate": ["Pen rate", "Shots pen / shots hit", 8, "{:8.1%}"], + "pen_rate_received": [ + "Penned%", + "Received hits pen / hits received", + 7, + "{:7.1%}", + ], + "splash_rate": ["Splash%", "HE splash shots / shots hit", 7, "{:7.1%}"], + "splash_rate_received": [ + "Splashed%", + "HE splash shots received / hits hit", + 9, + "{:9.1%}", + ], + "survived": ["Surv%", "Survival rate", 6, "{:6.1%}"], + "time_alive": ["T alive", "Time being alive in a battle in secs", 7, "{:7.0f}"], + "alive": [ + "Share live", + "Percentage of time being alive in a battle", + 8, + "{:8.0%}", + ], + "battle_duration": ["Duration", "Battle duration", 8, "{:8.0f}"], + "distance_travelled": ["Distance", "Distance travelled", 8, "{:8.0f}"], + "top_tier": ["Top tier", "Share of games as top tier", 8, "{:8.0%}"], + "player_wins": ["Player WR", "Average WR of the player", 9, "{:9.2%}"], + "allies_wins": [ + "Allies WR", + "Average WR of allies at the tier of their tank", + 9, + "{:9.2%}", + ], + "enemies_wins": [ + "Enemies WR", + "Average WR of enemies at the tier of their tank", + 10, + "{:10.2%}", + ], + "player_battles": [ + "Player Btls", + "Average number battles of the player", + 11, + "{:11.0f}", + ], + "allies_battles": [ + "Allies Btls", + "Average number battles of the allies", + 11, + "{:11.0f}", + ], + "enemies_battles": [ + "Enemies Btls", + "Average number battles of the enemies", + 12, + "{:12.0f}", + ], + "player_damage_dealt": [ + "Player Avg Dmg", + "Player Average damage", + 14, + "{:14.0f}", + ], + "allies_damage_dealt": [ + "Allies Avg Dmg", + "Average damage of the allies", + 14, + "{:14.0f}", + ], + "enemies_damage_dealt": [ + "Enemies Avg Dmg", + "Average damage of the enemies", + 15, + "{:14.0f}", + ], + MISSING_STATS: ["No stats", "Players without stats avail", 8, "{:8.1%}"], + } + + # fields to display in results + _result_fields_modes = { + "default": [ + "battles", + "win", + "damage_made", + "enemies_destroyed", + "enemies_spotted", + "top_tier", + "DR", + "survived", + "allies_wins", + "enemies_wins", + ], + "team": [ + "battles", + "win", + "in_platoon", + "player_wins", + "allies_wins", + "enemies_wins", + ], + "team_extended": [ + "battles", + "battles%", + "win", + "in_platoon", + "player_wins", + "allies_wins", + "enemies_wins", + "player_damage_dealt", + "allies_damage_dealt", + "enemies_damage_dealt", + "player_battles", + "allies_battles", + "enemies_battles", + ], + "extended": [ + "battles", + "battles%", + "win", + "in_platoon", + "damage_made", + "enemies_destroyed", + "enemies_spotted", + "top_tier", + "DR", + "KDR", + "hit_rate", + "pen_rate", + "survived", + "alive", + "player_wins", + "allies_wins", + "enemies_wins", + MISSING_STATS, + ], + "dmg_received": [ + "battles", + "win", + "damage_made", + "damage_received", + "enemies_destroyed", + "top_tier", + "DR", + "hits_received", + "hits_bounced", + "pen_rate_received", + "splash_rate_received", + ], + "all": [cat for cat in _result_fields.keys()], + } + + _team_fields = ["wins", "battles", "damage_dealt"] + + count_fields = ["battles", MISSING_STATS, "mastery_badge"] + + _result_ratios = { + "KDR": ["enemies_destroyed", "destroyed"], + "DR": ["damage_made", "damage_received"], + "hit_rate": ["shots_hit", "shots_made"], + "splash_rate": ["shots_splash", "shots_hit"], + "splash_rate_received": ["hits_splash", "hits_received"], + "pen_rate": ["shots_pen", "shots_hit"], + "pen_rate_received": ["hits_pen", "hits_received"], + "dmg_block_rate": ["damage_blocked", "damage_received"], + } + + # _extended_stats = False + + result_fields = list() + avg_fields = set() + ratio_fields = set() + + @classmethod + def get_modes(cls) -> list: + return list(cls._result_fields_modes.keys()) + + @classmethod + def get_mode_fields(cls, mode: str): + try: + return cls._result_fields_modes[mode] + except Exception as err: + bu.error(exception=err) + return None + + @classmethod + def set_fields(cls, mode: str = None): + try: + if mode not in cls.get_modes(): + bu.error( + "Mode " + + mode + + " not in defined modes: " + + ", ".join(cls.get_modes()) + ) + sys.exit(2) + + cls.mode = mode + cls.result_fields = cls.get_mode_fields(cls.mode) + cls.result_fields_ratio = set(cls._result_ratios.keys()) & set( + cls.result_fields + ) + cls.avg_fields = ( + set(cls.result_fields) + - set(cls._result_ratios.keys()) + - set(cls.count_fields) + ) + + for ratio in cls.result_fields_ratio: + cls.ratio_fields.add(cls._result_ratios[ratio][0]) + cls.ratio_fields.add(cls._result_ratios[ratio][1]) + except Exception as err: + bu.error("BattleCategory:", exception=err) + + @classmethod + def get_result_fields(cls) -> list: + return cls.result_fields + + @classmethod + def get_result_fields_all(cls) -> list: + return cls._result_fields.keys() + + @classmethod + def get_result_fields_ratio(cls, all: bool = False) -> list: + if all: + return cls._result_ratios.keys() + else: + return cls.result_fields_ratio + + @classmethod + def get_avg_fields(cls) -> set: + return cls.avg_fields + + @classmethod + def get_ratio_fields(cls) -> set: + return cls.ratio_fields + + @classmethod + def get_field_name(cls, field: str) -> str: + try: + return cls._result_fields[field][0] + except Exception as err: + bu.error(exception=err) + + @classmethod + def get_field_description(cls, field: str) -> str: + try: + return cls._result_fields[field][1] + except Exception as err: + bu.error(exception=err) + + @classmethod + def get_field_width(cls, field: str): + try: + return cls._result_fields[field][2] + except Exception as err: + bu.error(exception=err) + + @classmethod + def get_fields_team(cls) -> list: + return cls._team_fields + + @classmethod + def calc_ratio(cls, ratio: str, result: dict): + try: + value = result[cls._result_ratios[ratio][0]] + divider = result[cls._result_ratios[ratio][1]] + if divider != 0: + if value is None: + value = 0 + return value / divider + else: + if value == 0: + return 0 + else: + return float("Inf") + except Exception as err: + bu.error("Replay _id=" + result["_id"], exception=err) + return None + + @classmethod + def print_headers(cls, header: str): + try: + headers = [header] + for field in cls.result_fields: + print_format = "{:^" + str(cls.get_field_width(field)) + "s}" + headers.append(print_format.format(cls.get_field_name(field))) + print(" ".join(headers)) + except KeyError as err: + bu.error("Key not found", exception=err) + except Exception as err: + bu.error(exception=err) + return None + + @classmethod + def get_headers(cls, header: str = None) -> list: + try: + headers = [header] + for field in cls.result_fields: + headers.append(cls.get_field_name(field)) + return headers + except KeyError as err: + bu.error("Key not found", exception=err) + except Exception as err: + bu.error(exception=err) + return None + + @classmethod + def help(cls): + try: + for field in cls.get_result_fields_all(): + print("{:24s} : {}".format(field, cls.get_field_description(field))) + except Exception as err: + bu.error(exception=err) + return None + + def __init__(self): + try: + self.battles = 0 + self.missing_stats = 0 + self.n_players = 0 + self.ratios_ready = False + self.results_ready = False + self.results = collections.defaultdict(def_value_zero) + + except KeyError as err: + bu.error("BattleCategory(): Key not found", err) + + def record_result(self, result: dict) -> bool: + try: + for field in self.avg_fields | self.ratio_fields: + if (field in result) and (result[field] is not None): + self.results[field] += result[field] + self.battles += 1 + self.n_players += result[N_PLAYERS] + self.missing_stats += result[MISSING_STATS] + return True + except KeyError as err: + bu.error("Key not found", err) + bu.error(str(result)) + except Exception as err: + bu.error("BattleCategory:", exception=err) + return False + + def calc_ratios(self) -> bool: + if self.ratios_ready: + return True + try: + for field in self.result_fields_ratio: + if self.results[self._result_ratios[field][1]] != 0: + self.results[field] = ( + self.results[self._result_ratios[field][0]] + / self.results[self._result_ratios[field][1]] + ) + else: + self.results[field] = float("Inf") + self.ratios_ready = True + return True + except KeyError as err: + bu.error("Key not found", err) + return False + + def calc_results(self, total_battles: int = None): + if self.results_ready == True: + return True + if not self.ratios_ready: + self.calc_ratios() + try: + for field in self.avg_fields: + self.results[field] = self.results[field] / max(self.battles, 1) + self.results["battles"] = self.battles + self.results["battles%"] = self.battles / total_battles + self.results[MISSING_STATS] = self.missing_stats / max(self.n_players, 1) + self.results_ready = True + return True + except KeyError as err: + bu.error("Key not found", err) + except Exception as err: + bu.error(exception=err) + return False + + def get_results(self) -> dict: + """Return results as a dict""" + if not self.results_ready: + bu.error( + "Stats have not been calculated yet. call calc_results() before get_results()" + ) + try: + results = dict() + for field in self.result_fields: + results[field] = self.results[field] + return results + except KeyError as err: + bu.error("Key not found", err) + except Exception as err: + bu.error(exception=err) + return None + + def get_results_list(self) -> list: + """Return results as a list""" + if not self.results_ready: + bu.error( + "Stats have not been calculated yet. call calc_results() before get_results()" + ) + try: + results = list() + for field in self.result_fields: + results.append( + self._result_fields[field][3].format(self.results[field]) + ) + return results + except KeyError as err: + bu.error("Key not found", err) + except Exception as err: + bu.error(exception=err) + return None + + def print_results(self): + if not self.results_ready: + bu.error( + "Stats have not been calculated yet. call calc_results() before printing()" + ) + try: + results = [] + for field in self.result_fields: + results.append( + self._result_fields[field][3].format(self.results[field]) + ) + print(" : ".join(results)) + return True + except KeyError as err: + bu.error("Key not found", err) + except Exception as err: + bu.error(exception=err) + return False + + +class PlayerHistogram: + def __init__( + self, field: str, name: str, fields: list, factor: float, cat_format: str + ): + self.field = field + if self.field == "wins": + self.name = StatFunc.get_title() + else: + self.name = name + self.fields = fields + self.cat_factor = factor + self.cat_format = cat_format + self.ncat = len(self.fields) - 1 + self.allies = [0] * self.ncat + self.enemies = [0] * self.ncat + self.total = [0] * self.ncat + self.results = None + + def record_ally(self, stat: float): + for cat in range(0, self.ncat): + if self.fields[cat] <= stat <= self.fields[cat + 1]: + self.allies[cat] += 1 + self.total[cat] += 1 + return True + return False + + def record_enemy(self, stat: float): + for cat in range(0, self.ncat): + if self.fields[cat] <= stat <= self.fields[cat + 1]: + self.enemies[cat] += 1 + self.total[cat] += 1 + return True + return False + + def get_category_name(self, cat: int): + if cat < self.ncat - 1: + return self.format_category(cat) + " - " + self.format_category(cat + 1) + else: + return self.format_category(cat) + " -" + + def format_category(self, cat: int): + val = self.fields[cat] + return self.cat_format.format(float(val * self.cat_factor)) + + def calc_results(self): + N_enemies = 0 + N_allies = 0 + res = dict() + # calculate buckets + for cat in range(0, self.ncat): + N_enemies += self.enemies[cat] + N_allies += self.allies[cat] + N_total = N_allies + N_enemies + + for cat in range(0, self.ncat): + stat = dict() + stat["allies"] = self.allies[cat] + stat["allies%"] = stat["allies"] / N_allies + stat["enemies"] = self.enemies[cat] + stat["enemies%"] = stat["enemies"] / N_enemies + stat["total"] = self.allies[cat] + self.enemies[cat] + stat["total%"] = stat["total"] / N_total + + res[self.get_category_name(cat)] = stat + + self.results = res + return res + + def get_results(self) -> dict: + return self.results + + def get_results_list(self) -> list: + try: + if self.results is not None: + res = list() + res.append( + [ + self.name, + "Allies", + "Allies %", + "Enemies", + "Enemies %", + "TOTAL", + "TOTAL %", + ] + ) + for cat in self.results: + stat = self.results[cat] + res.append( + [ + cat, + stat["allies"], + stat["allies%"], + stat["enemies"], + stat["enemies%"], + stat["total"], + stat["total%"], + ] + ) + return res + else: + bu.error("Results have not been calculated yet.") + return None + except Exception as err: + bu.error(exception=err) + return None + + def print(self): + try: + if self.results is not None: + print( + "\n{:12s} | {:13s} | {:13s} | {:13s}".format( + self.name, "Allies", "Enemies", "TOTAL" + ) + ) + for cat in self.results: + stat = self.results[cat] + print( + "{:12s} | {:5d} ({:5.1%}) | {:5d} ({:5.1%}) | {:5d} ({:5.1%})".format( + cat, + stat["allies"], + stat["allies%"], + stat["enemies"], + stat["enemies%"], + stat["total"], + stat["total%"], + ) + ) + else: + bu.error("Results have not been calculated yet.") + except Exception as err: + bu.error(exception=err) + return None class ErrorCatchingArgumentParser(argparse.ArgumentParser): - def exit(self, status=0, message=None): - if status: - if message != None: - raise UserWarning(message) - else: - raise UserWarning() + def exit(self, status=0, message=None): + if status: + if message is not None: + raise UserWarning(message) + else: + raise UserWarning() ## main() ------------------------------------------------------------- + async def main(argv): - global wg, wi, WG_APP_ID, OPT_MODE_DEFAULT, OPT_EXPORT_CSV_FILE, OPT_EXPORT_JSON_FILE - # set the directory for the script - current_dir = os.getcwd() - os.chdir(os.path.dirname(sys.argv[0])) + global wg, wi, WG_APP_ID, OPT_MODE_DEFAULT, OPT_EXPORT_CSV_FILE, OPT_EXPORT_JSON_FILE + # set the directory for the script + current_dir = os.getcwd() + os.chdir(os.path.dirname(sys.argv[0])) - ## Default options: - OPT_MODE_DEFAULT = 'default' - OPT_MODE_HELP = 'help' - OPT_MODES = BattleCategory.get_modes() + [OPT_MODE_HELP] - OPT_DB = False - OPT_HIST = False - OPT_STAT_FUNC = StatFunc.get_default() - OPT_WORKERS_N = 10 - # OPT_EXPORT_CSV_FILE = 'export.csv' - # OPT_EXPORT_JSON_FILE = 'export.json' + ## Default options: + OPT_MODE_DEFAULT = "default" + OPT_MODE_HELP = "help" + OPT_MODES = BattleCategory.get_modes() + [OPT_MODE_HELP] + OPT_DB = False + OPT_HIST = False + OPT_STAT_FUNC = StatFunc.get_default() + OPT_WORKERS_N = 10 + # OPT_EXPORT_CSV_FILE = 'export.csv' + # OPT_EXPORT_JSON_FILE = 'export.json' - #WG_ACCOUNT = None # format: nick@server, where server is either 'eu', 'ru', 'na', 'asia' or 'china'. - # China is not supported since WG API stats are not available there - #WG_ID = None # WG account_id in integer format. - # WG_ACCOUNT will be used to retrieve the account_id, but it can be set directly too - WG_RATE_LIMIT = 10 ## WG standard. Do not edit unless you have your - ## own server app ID, it will REDUCE the performance - - ## VERY unlikely you have a DB set up - DB_SERVER = 'localhost' - DB_PORT = 27017 - DB_TLS = False - DB_CERT_REQ = False - DB_AUTH = 'admin' - DB_NAME = 'BlitzStats' - DB_USER = 'mongouser' - DB_PASSWD = 'PASSWORD' - DB_CERT = None - DB_CA = None - - try: - ## Read config - if os.path.isfile(FILE_CONFIG): - config = configparser.ConfigParser() - config.read(FILE_CONFIG) + # WG_ACCOUNT = None # format: nick@server, where server is either 'eu', 'ru', 'na', 'asia' or 'china'. + # China is not supported since WG API stats are not available there + # WG_ID = None # WG account_id in integer format. + # WG_ACCOUNT will be used to retrieve the account_id, but it can be set directly too + WG_RATE_LIMIT = 10 ## WG standard. Do not edit unless you have your + ## own server app ID, it will REDUCE the performance - try: - if 'ANALYZER' in config.sections(): - configAnalyzer = config['ANALYZER'] - OPT_MODE_DEFAULT = configAnalyzer.getboolean('mode', OPT_MODE_DEFAULT) - OPT_HIST = configAnalyzer.getboolean('histograms', OPT_HIST) - OPT_STAT_FUNC = configAnalyzer.get('stat_func', fallback=OPT_STAT_FUNC) - OPT_WORKERS_N = configAnalyzer.getint('workers', OPT_WORKERS_N) - OPT_EXPORT_CSV_FILE = configAnalyzer.get('csv_file', OPT_EXPORT_CSV_FILE) - OPT_EXPORT_JSON_FILE= configAnalyzer.get('json_file', OPT_EXPORT_JSON_FILE) - res_categorizations = configAnalyzer.get('categorizations', None) - if res_categorizations != None: - res_categorizations.replace(' ','') - BattleCategorizationList.set_categorizations_default(res_categorizations.split(',')) - histogram_fields_str = configAnalyzer.get('histogram_buckets', None) - if histogram_fields_str != None: - set_histogram_buckets(json.loads(histogram_fields_str)) + ## VERY unlikely you have a DB set up + DB_SERVER = "localhost" + DB_PORT = 27017 + DB_TLS = False + DB_CERT_REQ = False + DB_AUTH = "admin" + DB_NAME = "BlitzStats" + DB_USER = "mongouser" + DB_PASSWD = "PASSWORD" + DB_CERT = None + DB_CA = None - except (KeyError, configparser.NoSectionError) as err: - bu.error(exception=err) + try: + ## Read config + if os.path.isfile(FILE_CONFIG): + config = configparser.ConfigParser() + config.read(FILE_CONFIG) - try: - if 'WG' in config.sections(): - configWG = config['WG'] - # WG account id of the uploader: - # # Find it here: https://developers.wargaming.net/reference/all/wotb/account/list/ - #WG_ID = configWG.getint('wg_id', WG_ID) - #WG_ACCOUNT = configWG.get('wg_account', WG_ACCOUNT) - WG_APP_ID = configWG.get('wg_app_id', WG_APP_ID) - WG_RATE_LIMIT = configWG.getint('wg_rate_limit', WG_RATE_LIMIT) - except (KeyError, configparser.NoSectionError) as err: - bu.error(exception=err) + try: + if "ANALYZER" in config.sections(): + configAnalyzer = config["ANALYZER"] + OPT_MODE_DEFAULT = configAnalyzer.getboolean( + "mode", OPT_MODE_DEFAULT + ) + OPT_HIST = configAnalyzer.getboolean("histograms", OPT_HIST) + OPT_STAT_FUNC = configAnalyzer.get( + "stat_func", fallback=OPT_STAT_FUNC + ) + OPT_WORKERS_N = configAnalyzer.getint("workers", OPT_WORKERS_N) + OPT_EXPORT_CSV_FILE = configAnalyzer.get( + "csv_file", OPT_EXPORT_CSV_FILE + ) + OPT_EXPORT_JSON_FILE = configAnalyzer.get( + "json_file", OPT_EXPORT_JSON_FILE + ) + res_categorizations = configAnalyzer.get("categorizations", None) + if res_categorizations is not None: + res_categorizations.replace(" ", "") + BattleCategorizationList.set_categorizations_default( + res_categorizations.split(",") + ) + histogram_fields_str = configAnalyzer.get("histogram_buckets", None) + if histogram_fields_str is not None: + set_histogram_buckets(json.loads(histogram_fields_str)) - try: - if 'DATABASE' in config.sections(): - configDB = config['DATABASE'] - OPT_DB = configDB.getboolean('opt_DB', OPT_DB) - DB_SERVER = configDB.get('db_server', DB_SERVER) - DB_PORT = configDB.getint('db_port', DB_PORT) - DB_TLS = configDB.getboolean('db_tls', DB_TLS) - DB_CERT_REQ = configDB.getboolean('db_tls_req', DB_CERT_REQ) - DB_AUTH = configDB.get('db_auth', DB_AUTH) - DB_NAME = configDB.get('db_name', DB_NAME) - DB_USER = configDB.get('db_user', DB_USER) - DB_PASSWD = configDB.get('db_password', DB_PASSWD) - DB_CERT = configDB.get('db_tls_cert_file', DB_CERT) - DB_CA = configDB.get('db_tls_ca_file', DB_CA) - except (KeyError, configparser.NoSectionError) as err: - bu.error(exception=err) + except (KeyError, configparser.NoSectionError) as err: + bu.error(exception=err) - parser = ErrorCatchingArgumentParser(description='Analyze Blitz replay JSON files from WoTinspector.com. Use \'upload_wotb_replays.py\' to upload the replay files first.') + try: + if "WG" in config.sections(): + configWG = config["WG"] + # WG account id of the uploader: + # # Find it here: https://developers.wargaming.net/reference/all/wotb/account/list/ + # WG_ID = configWG.getint('wg_id', WG_ID) + # WG_ACCOUNT = configWG.get('wg_account', WG_ACCOUNT) + WG_APP_ID = configWG.get("wg_app_id", WG_APP_ID) + WG_RATE_LIMIT = configWG.getint("wg_rate_limit", WG_RATE_LIMIT) + except (KeyError, configparser.NoSectionError) as err: + bu.error(exception=err) - parser.add_argument('--mode', default=OPT_MODE_DEFAULT, choices=OPT_MODES, help='Select stats to print out (columns). Options: ' + ', '.join(OPT_MODES)) - parser.add_argument('--extra', choices=BattleCategorizationList.get_categorizations_all(), default=None, nargs='*', help='Print extra categories: ' + ', '.join( cat + '=' + BattleCategorizationList.get_categorization_title(cat) for cat in BattleCategorizationList.get_categorizations_all())) - parser.add_argument('--only_extra', action='store_true', default=False, help='Print only the extra categories') - parser.add_argument('--hist', action='store_true', default=OPT_HIST, help='Print player histograms: ' + ', '.join( histogram_fields[k][0] for k in histogram_fields)) - # parser.add_argument('--output', default='plain', choices=['plain', 'db'], help='Select output mode: plain text or database') - parser.add_argument('-id', dest='account_id', type=int, default=None, help='WG account_id to analyze. Replays without the account_id will be ignored.') - parser.add_argument('-a', '--account', type=str, default=None, help='WG account name to analyze. Format: ACCOUNT_NAME@SERVER') - parser.add_argument('--stat_func', default=OPT_STAT_FUNC, choices=StatFunc.get_stat_funcs(), help='Select how to calculate for ally/enemy performance: tank-tier stats, global player stats') - parser.add_argument('-u', '--url', action='store_true', default=False, help='Print replay URLs') - parser.add_argument('--tankfile', type=str, default='tanks.json', help='JSON file to read Tankopedia from. Default is "tanks.json"') - parser.add_argument('--mapfile', type=str, default='maps.json', help='JSON file to read Blitz map names from. Default is "maps.json"') - parser.add_argument('--json', action='store_true', default=False, help='Export data in JSON format') - parser.add_argument('--csv', action='store_true', default=False, help='Export data in CSV format') - parser.add_argument('--replays', action='store_true', default=False, help='Export replay info in JSON format') - parser.add_argument('-o','--outfile', type=str, default='-', metavar="OUTPUT", help='File to write results. Default STDOUT') - parser.add_argument('--db', action='store_true', default=OPT_DB, help='Use DB - You are unlikely to have it') - parser.add_argument('--filters', type=str, default=None, help='Filter replays based on categories. Filters given in JSON format.\nUse array "[]" for multiple filters/values. see --mode help.\nExample: : [ {"tank_tier" : [8,9,10] }, { "player_wins" : 5 }]') - parser.add_argument('--filters_db', type=str, default=None, help='[only for DB setup] Filter replays in DB based on categories. Filters given in MongoDB JSON format. See --mode help') - parser.add_argument('--min', type=int, default=None, help='Only select replays from players with minimum number of replays in the dataset') - parser.add_argument('-d', '--debug', action='store_true', default=False, help='Debug mode') - parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Verbose mode') - parser.add_argument('-s', '--silent', action='store_true', default=False, help='Silent mode') - parser.add_argument('-l', '--log', action='store_true', default=False, help='Enable file logging') - parser.add_argument('files', metavar='FILE1 [FILE2 ...]', type=str, nargs='*', help='Files/dirs to read. Use \'-\' for STDIN, "db:" for database') - ## parser.add_argument('--help_filters', action='store_true', default=False, help='Extended help for --filters') + try: + if "DATABASE" in config.sections(): + configDB = config["DATABASE"] + OPT_DB = configDB.getboolean("opt_DB", OPT_DB) + DB_SERVER = configDB.get("db_server", DB_SERVER) + DB_PORT = configDB.getint("db_port", DB_PORT) + DB_TLS = configDB.getboolean("db_tls", DB_TLS) + DB_CERT_REQ = configDB.getboolean("db_tls_req", DB_CERT_REQ) + DB_AUTH = configDB.get("db_auth", DB_AUTH) + DB_NAME = configDB.get("db_name", DB_NAME) + DB_USER = configDB.get("db_user", DB_USER) + DB_PASSWD = configDB.get("db_password", DB_PASSWD) + DB_CERT = configDB.get("db_tls_cert_file", DB_CERT) + DB_CA = configDB.get("db_tls_ca_file", DB_CA) + except (KeyError, configparser.NoSectionError) as err: + bu.error(exception=err) - try: - args = parser.parse_args() - except Exception as err: - bu.error('Invalid arguments', exception=err) - sys.exit(0) + parser = ErrorCatchingArgumentParser( + description="Analyze Blitz replay JSON files from WoTinspector.com. Use 'upload_wotb_replays.py' to upload the replay files first." + ) - # res_categories = BattleCategorization.get_categorizations(OPT_CATEGORIZATIONS, args) + parser.add_argument( + "--mode", + default=OPT_MODE_DEFAULT, + choices=OPT_MODES, + help="Select stats to print out (columns). Options: " + + ", ".join(OPT_MODES), + ) + parser.add_argument( + "--extra", + choices=BattleCategorizationList.get_categorizations_all(), + default=None, + nargs="*", + help="Print extra categories: " + + ", ".join( + cat + "=" + BattleCategorizationList.get_categorization_title(cat) + for cat in BattleCategorizationList.get_categorizations_all() + ), + ) + parser.add_argument( + "--only_extra", + action="store_true", + default=False, + help="Print only the extra categories", + ) + parser.add_argument( + "--hist", + action="store_true", + default=OPT_HIST, + help="Print player histograms: " + + ", ".join(histogram_fields[k][0] for k in histogram_fields), + ) + # parser.add_argument('--output', default='plain', choices=['plain', 'db'], help='Select output mode: plain text or database') + parser.add_argument( + "-id", + dest="account_id", + type=int, + default=None, + help="WG account_id to analyze. Replays without the account_id will be ignored.", + ) + parser.add_argument( + "-a", + "--account", + type=str, + default=None, + help="WG account name to analyze. Format: ACCOUNT_NAME@SERVER", + ) + parser.add_argument( + "--stat_func", + default=OPT_STAT_FUNC, + choices=StatFunc.get_stat_funcs(), + help="Select how to calculate for ally/enemy performance: tank-tier stats, global player stats", + ) + parser.add_argument( + "-u", "--url", action="store_true", default=False, help="Print replay URLs" + ) + parser.add_argument( + "--tankfile", + type=str, + default="tanks.json", + help='JSON file to read Tankopedia from. Default is "tanks.json"', + ) + parser.add_argument( + "--mapfile", + type=str, + default="maps.json", + help='JSON file to read Blitz map names from. Default is "maps.json"', + ) + parser.add_argument( + "--json", + action="store_true", + default=False, + help="Export data in JSON format", + ) + parser.add_argument( + "--csv", + action="store_true", + default=False, + help="Export data in CSV format", + ) + parser.add_argument( + "--replays", + action="store_true", + default=False, + help="Export replay info in JSON format", + ) + parser.add_argument( + "-o", + "--outfile", + type=str, + default="-", + metavar="OUTPUT", + help="File to write results. Default STDOUT", + ) + parser.add_argument( + "--db", + action="store_true", + default=OPT_DB, + help="Use DB - You are unlikely to have it", + ) + parser.add_argument( + "--filters", + type=str, + default=None, + help='Filter replays based on categories. Filters given in JSON format.\nUse array "[]" for multiple filters/values. see --mode help.\nExample: : [ {"tank_tier" : [8,9,10] }, { "player_wins" : 5 }]', + ) + parser.add_argument( + "--filters_db", + type=str, + default=None, + help="[only for DB setup] Filter replays in DB based on categories. Filters given in MongoDB JSON format. See --mode help", + ) + parser.add_argument( + "--min", + type=int, + default=None, + help="Only select replays from players with minimum number of replays in the dataset", + ) + parser.add_argument( + "-d", "--debug", action="store_true", default=False, help="Debug mode" + ) + parser.add_argument( + "-v", "--verbose", action="store_true", default=False, help="Verbose mode" + ) + parser.add_argument( + "-s", "--silent", action="store_true", default=False, help="Silent mode" + ) + parser.add_argument( + "-l", + "--log", + action="store_true", + default=False, + help="Enable file logging", + ) + parser.add_argument( + "files", + metavar="FILE1 [FILE2 ...]", + type=str, + nargs="*", + help="Files/dirs to read. Use '-' for STDIN, \"db:\" for database", + ) + ## parser.add_argument('--help_filters', action='store_true', default=False, help='Extended help for --filters') - bu.set_log_level(args.silent, args.verbose, args.debug) - # bu.set_progress_step(250) # Set the frequency of the progress dots. + try: + args = parser.parse_args() + except Exception as err: + bu.error("Invalid arguments", exception=err) + sys.exit(0) - #### Connect to MongoDB. You are unlikely to have this set up... - bu.debug('DB_SERVER: ' + DB_SERVER) - bu.debug('DB_PORT: ' + str(DB_PORT)) - bu.debug('DB_TLS: ' + "True" if DB_TLS else "False") - bu.debug('DB_AUTH: ' + DB_AUTH) - bu.debug('DB_NAME: ' + DB_NAME) - - client = None - db = None - if args.db: - try: - client = motor.motor_asyncio.AsyncIOMotorClient(DB_SERVER,DB_PORT, authSource=DB_AUTH, username=DB_USER, password=DB_PASSWD, tls=DB_TLS, tlsAllowInvalidCertificates=DB_CERT_REQ, tlsCertificateKeyFile=DB_CERT, tlsCAFile=DB_CA) - db = client[DB_NAME] - bu.debug('Database connection initiated') - except Exception as err: - bu.error("Could no initiate DB connection: Disabling DB", err) - args.db = False - else: - bu.debug('No DB in use') + # res_categories = BattleCategorization.get_categorizations(OPT_CATEGORIZATIONS, args) - # TBD - if args.mode == OPT_MODE_HELP: - await help_extended(db, parser) - sys.exit(0) - elif len(args.files) == 0: - raise UserWarning('No FILES argument given: No replays to analyse') + bu.set_log_level(args.silent, args.verbose, args.debug) + # bu.set_progress_step(250) # Set the frequency of the progress dots. - if args.log: - await bu.set_file_logging('analyze_wotb_replays', add_timestamp=True) + #### Connect to MongoDB. You are unlikely to have this set up... + bu.debug("DB_SERVER: " + DB_SERVER) + bu.debug("DB_PORT: " + str(DB_PORT)) + bu.debug("DB_TLS: " + "True" if DB_TLS else "False") + bu.debug("DB_AUTH: " + DB_AUTH) + bu.debug("DB_NAME: " + DB_NAME) - StatFunc.set_stat_func(args.stat_func) - # rebase file arguments due to moving the working directory to the script location - args.files = bu.rebase_file_args(current_dir, args.files) + client = None + db = None + if args.db: + try: + client = AsyncIOMotorClient( + DB_SERVER, + DB_PORT, + authSource=DB_AUTH, + username=DB_USER, + password=DB_PASSWD, + tls=DB_TLS, + tlsAllowInvalidCertificates=DB_CERT_REQ, + tlsCertificateKeyFile=DB_CERT, + tlsCAFile=DB_CA, + ) + db = client[DB_NAME] + bu.debug("Database connection initiated") + except Exception as err: + bu.error("Could no initiate DB connection: Disabling DB", err) + args.db = False + else: + bu.debug("No DB in use") - wg = WG(WG_APP_ID, args.tankfile, args.mapfile, stats_cache=True, rate_limit=WG_RATE_LIMIT) - wi = WoTinspector(rate_limit=10) + # TBD + if args.mode == OPT_MODE_HELP: + await help_extended(db, parser) + sys.exit(0) + elif len(args.files) == 0: + raise UserWarning("No FILES argument given: No replays to analyse") - if args.account != None: - args.account_id = await wg.get_account_id(args.account) - bu.debug('WG account_id: ' + str(args.account_id)) + if args.log: + await bu.set_file_logging("analyze_wotb_replays", add_timestamp=True) - # try: - BattleCategory.set_fields(args.mode) - replayQ = asyncio.Queue(maxsize=1000) - reader_tasks = [] - # Make replay Queue + StatFunc.set_stat_func(args.stat_func) + # rebase file arguments due to moving the working directory to the script location + args.files = bu.rebase_file_args(current_dir, args.files) + except Exception as err: + bu.error(err) + sys.exit(1) - scanner_task = asyncio.create_task(mk_replayQ(replayQ, args, db)) - bu.set_counter('Reading replays', 10) - # Start tasks to process the Queue - for i in range(OPT_WORKERS_N): - reader_tasks.append(asyncio.create_task(replay_reader(replayQ, i, args))) - bu.debug('ReplayReader ' + str(i) + ' started') + try: + wg = WG( + WG_app_id=WG_APP_ID, + tankopedia_fn=args.tankfile, + maps_fn=args.mapfile, + stats_cache=True, + rate_limit=WG_RATE_LIMIT, + ) + wi = WoTinspector(rate_limit=10) - bu.debug('Waiting for the replay scanner to finish') - await asyncio.wait([scanner_task]) - bu.finish_progress_bar() - # bu.debug('Scanner finished. Waiting for replay readers to finish the queue') - await replayQ.join() - await asyncio.sleep(0.1) - bu.debug('Replays read. Cancelling Readers and analyzing results') - for task in reader_tasks: - task.cancel() - await asyncio.sleep(0.1) - results = [] - players = set() - replays = dict() - for res in await asyncio.gather(*reader_tasks): - results.extend(res[0]) - players.update(res[1]) - replays.update(res[2]) - - if len(players) == 0: - raise Exception("No players found to fetch stats for. No replays found?") + if args.account is not None: + args.account_id = await wg.get_account_id(args.account) + bu.debug("WG account_id: " + str(args.account_id)) - if args.min != None: - results = filter_min_replays_by_player(results, args.min) - - # Filter based on non-stats filters - results = filter_results(results, args.filters, stats_filters=False) - players = get_players(results) - - (player_stats, stat_id_map) = await process_player_stats(players, OPT_WORKERS_N, args, db) - - # Filter based on stats filters - results = filter_results(results, args.filters, stats_filters=True) - bu.debug(str(len(replays)) + ' replays to process') - replays = filter_replays(replays, results) - - bu.debug('Number of player stats: ' + str(len(player_stats))) - teamresults = calc_team_stats(results, player_stats, stat_id_map, args) + # try: + BattleCategory.set_fields(args.mode) + replayQ = asyncio.Queue(maxsize=1000) + reader_tasks = [] + # Make replay Queue - blt_cat_list = process_battle_results(teamresults, args) - bu.verbose_std('WR = ' + StatFunc.get_title()) - blt_cat_list.print_results() + scanner_task = asyncio.create_task(mk_replayQ(replayQ, args, db)) + bu.set_counter("Reading replays", 10) + # Start tasks to process the Queue + for i in range(OPT_WORKERS_N): + reader_tasks.append(asyncio.create_task(replay_reader(replayQ, i, args))) + bu.debug("ReplayReader " + str(i) + " started") - histograms = None - if args.hist: - histograms = process_player_dist(results, player_stats, stat_id_map) - print_player_dist(histograms) - - if args.json: - await export_json(args, blt_cat_list, histograms) - - if args.csv: - await export_csv(args, blt_cat_list, histograms) - - if args.replays: - await export_replays(replays) + bu.debug("Waiting for the replay scanner to finish") + await asyncio.wait([scanner_task]) + bu.finish_progress_bar() + # bu.debug('Scanner finished. Waiting for replay readers to finish the queue') + await replayQ.join() + await asyncio.sleep(0.1) + bu.debug("Replays read. Cancelling Readers and analyzing results") + for task in reader_tasks: + task.cancel() + await asyncio.sleep(0.1) + results = [] + players = set() + replays = dict() + for res in await asyncio.gather(*reader_tasks): + results.extend(res[0]) + players.update(res[1]) + replays.update(res[2]) - bu.debug('Finished. Cleaning up..................') - # except Exception as err: - # bu.error(exception=err) - except UserWarning as err: - bu.warning(str(err)) - except asyncio.CancelledError: - pass - except Exception as err: - bu.error(exception=err) - finally: - ## Need to close the aiohttp.session since Python destructors do not support async methods... - if wg != None: - await wg.close() - if wi != None: - await wi.close() - return None + if len(players) == 0: + raise Exception("No players found to fetch stats for. No replays found?") + + if args.min is not None: + results = filter_min_replays_by_player(results, args.min) + + # Filter based on non-stats filters + results = filter_results(results, args.filters, stats_filters=False) + players = get_players(results) + + (player_stats, stat_id_map) = await process_player_stats( + players, OPT_WORKERS_N, args, db + ) + + # Filter based on stats filters + results = filter_results(results, args.filters, stats_filters=True) + bu.debug(str(len(replays)) + " replays to process") + replays = filter_replays(replays, results) + + bu.debug("Number of player stats: " + str(len(player_stats))) + teamresults = calc_team_stats(results, player_stats, stat_id_map, args) + + blt_cat_list = process_battle_results(teamresults, args) + bu.verbose_std("WR = " + StatFunc.get_title()) + blt_cat_list.print_results() + + histograms = None + if args.hist: + histograms = process_player_dist(results, player_stats, stat_id_map) + print_player_dist(histograms) + + if args.json: + await export_json(args, blt_cat_list, histograms) + + if args.csv: + await export_csv(args, blt_cat_list, histograms) + + if args.replays: + await export_replays(replays) + + bu.debug("Finished. Cleaning up..................") + # except Exception as err: + # bu.error(exception=err) + except UserWarning as err: + bu.warning(str(err)) + except asyncio.CancelledError: + pass + except Exception as err: + bu.error(exception=err) + finally: + ## Need to close the aiohttp.session since Python destructors do not support async methods... + if wg is not None: + await wg.close() + if wi is not None: + await wi.close() + return None -async def export_csv(args : argparse.Namespace, blt_cat_list: BattleCategorizationList, histograms: dict = None): - """Export results into a CSV file""" - try: - rows = blt_cat_list.get_results_list() - if args.hist: - rows.append('') - rows.append(['Player Histograms']) - for histogram in histograms.values(): - rows.append('') - rows.extend(histogram.get_results_list()) - if args.outfile == '-': - export_file = OPT_EXPORT_CSV_FILE - else: - export_file = args.outfile - with open(export_file, 'w') as csvfile: - # creating a csv writer object - csvwriter = csv.writer(csvfile) - csvwriter.writerows(rows) - bu.verbose_std('Results exported to: ' + export_file) - except Exception as err: - bu.error(exception=err) +async def export_csv( + args: argparse.Namespace, + blt_cat_list: BattleCategorizationList, + histograms: dict = None, +): + """Export results into a CSV file""" + try: + rows = blt_cat_list.get_results_list() + if args.hist: + rows.append("") + rows.append(["Player Histograms"]) + for histogram in histograms.values(): + rows.append("") + rows.extend(histogram.get_results_list()) + if args.outfile == "-": + export_file = OPT_EXPORT_CSV_FILE + else: + export_file = args.outfile + with open(export_file, "w") as csvfile: + # creating a csv writer object + csvwriter = csv.writer(csvfile) + csvwriter.writerows(rows) + bu.verbose_std("Results exported to: " + export_file) + except Exception as err: + bu.error(exception=err) -async def export_json(args : argparse.Namespace, blt_cat_list: BattleCategorizationList, histograms: dict = None): - """Export results into a JSON file""" - try: - res = blt_cat_list.get_results_json() - if histograms != None: - res_hist = dict() - for histogram in histograms.keys(): - res_hist[histogram] = histograms[histogram].get_results() - res['histograms'] = res_hist - if args.outfile == '-': - export_file = OPT_EXPORT_JSON_FILE - else: - export_file = args.outfile - await bu.save_JSON(export_file, res, pretty=False) ## Excel does not understand pretty JSON - bu.verbose_std('Results exported to: ' + export_file) - except Exception as err: - bu.error(exception=err) +async def export_json( + args: argparse.Namespace, + blt_cat_list: BattleCategorizationList, + histograms: dict = None, +): + """Export results into a JSON file""" + try: + res = blt_cat_list.get_results_json() + if histograms is not None: + res_hist = dict() + for histogram in histograms.keys(): + res_hist[histogram] = histograms[histogram].get_results() + res["histograms"] = res_hist + if args.outfile == "-": + export_file = OPT_EXPORT_JSON_FILE + else: + export_file = args.outfile + await bu.save_JSON( + export_file, res, pretty=False + ) ## Excel does not understand pretty JSON + bu.verbose_std("Results exported to: " + export_file) + except Exception as err: + bu.error(exception=err) -async def help_extended(db : motor.motor_asyncio.AsyncIOMotorDatabase = None, parser: argparse.ArgumentParser = None): - """Help for --filters""" - if parser != None: - parser.print_help() - # Result category options +async def help_extended( + db: AsyncIOMotorDatabase = None, + parser: argparse.ArgumentParser = None, +): + """Help for --filters""" + if parser is not None: + parser.print_help() + # Result category options - # Results fields - print('\n-------------------------------------------------------------------') - print('| Result fields |') - print('-------------------------------------------------------------------\n') - - BattleCategory.help() + # Results fields + print("\n-------------------------------------------------------------------") + print("| Result fields |") + print("-------------------------------------------------------------------\n") - print('\n-------------------------------------------------------------------') - print('| Filters |') - print('-------------------------------------------------------------------\n') - print("\t--filters '{ \"categorization\" : category }'") - print('\tThese filters let you filter only battles matching the filter. The filter has been proper JSON dict.') - print("\tExample: --filter '{ \"room_type\" :1 }'") - print('') - BattleCategorizationList.help() - print('\n-------------------------------------------------------------------') - print('| Rounding errors |') - print('-------------------------------------------------------------------\n') - print('Yes, the numbers are rounded UP because reasons.') - print('If interested why please read this: ') - print('\thttps://realpython.com/python-rounding/#pythons-built-in-round-function') - print('No, I am not planning to fix these for now.') + BattleCategory.help() - # Filter usage - print('\n-------------------------------------------------------------------') - print('| DB Filter usage |') - print('-------------------------------------------------------------------\n') - print('You are unlikely to have DB setup required for these filters. This is "Jylpah\'s special"') - print('') - print("Syntax: --filters_db '{ \"replay.param\": VALUE, \"replay.param2\": { \"$MONGO_OPERATOR\" : VALUE }, ... }'") - print("\tExample: --filters_db '{ \"data.summary.protagonist\": ACCOUNT_ID, \"data.summary.battle_start_timestamp\": { \"$gt\" : 1602853200 } }'") - if db != None: - dbc = db[DB_C_REPLAYS] - try: - res = await dbc.find_one() - summary = res['data']['summary'] - details = summary['details'][0] - print('\n--filters KEYS:') - for s in summary: - print("\tdata.summary." + s) - for d in details: - print("\tdata.summary.details.[]." + d) - except Exception as err: - bu.error(exception=err) + print("\n-------------------------------------------------------------------") + print("| Filters |") + print("-------------------------------------------------------------------\n") + print("\t--filters '{ \"categorization\" : category }'") + print( + "\tThese filters let you filter only battles matching the filter. The filter has been proper JSON dict." + ) + print("\tExample: --filter '{ \"room_type\" :1 }'") + print("") + BattleCategorizationList.help() + print("\n-------------------------------------------------------------------") + print("| Rounding errors |") + print("-------------------------------------------------------------------\n") + print("Yes, the numbers are rounded UP because reasons.") + print("If interested why please read this: ") + print("\thttps://realpython.com/python-rounding/#pythons-built-in-round-function") + print("No, I am not planning to fix these for now.") + + # Filter usage + print("\n-------------------------------------------------------------------") + print("| DB Filter usage |") + print("-------------------------------------------------------------------\n") + print( + 'You are unlikely to have DB setup required for these filters. This is "Jylpah\'s special"' + ) + print("") + print( + 'Syntax: --filters_db \'{ "replay.param": VALUE, "replay.param2": { "$MONGO_OPERATOR" : VALUE }, ... }\'' + ) + print( + '\tExample: --filters_db \'{ "data.summary.protagonist": ACCOUNT_ID, "data.summary.battle_start_timestamp": { "$gt" : 1602853200 } }\'' + ) + if db is not None: + dbc = db[DB_C_REPLAYS] + try: + res = await dbc.find_one() + summary = res["data"]["summary"] + details = summary["details"][0] + print("\n--filters KEYS:") + for s in summary: + print("\tdata.summary." + s) + for d in details: + print("\tdata.summary.details.[]." + d) + except Exception as err: + bu.error(exception=err) async def export_replays(replays: dict): - """Export replays""" - try: - replay_dir = OPT_EXPORT_REPLAY_DIR + '_' + bu.get_date_str() - bu.verbose_std('Exporting replays to ' + replay_dir) - if not os.path.isdir(replay_dir): - os.mkdir(replay_dir) + """Export replays""" + global wg + try: + replay_dir = OPT_EXPORT_REPLAY_DIR + "_" + bu.get_date_str() + bu.verbose_std("Exporting replays to " + replay_dir) + if not os.path.isdir(replay_dir): + os.mkdir(replay_dir) - i = 0 - for replay in replays.values(): - filename = wg.get_replay_filename(replay) - async with aiofiles.open(os.path.join(replay_dir, filename), 'w', encoding="utf8") as f: - await f.write(json.dumps(replay, indent=4)) - i += 1 - bu.verbose_std( str(i) + ' replays exported') - except Exception as err: - bu.error(exception=err) + i = 0 + for replay in replays.values(): + filename = wg.get_replay_filename(replay) + async with aiofiles.open( + os.path.join(replay_dir, filename), "w", encoding="utf8" + ) as f: + await f.write(json.dumps(replay, indent=4)) + i += 1 + bu.verbose_std(str(i) + " replays exported") + except Exception as err: + bu.error(exception=err) ## move the class PlayerHistogram? def set_histogram_buckets(json: dict): - global histogram_fields - try: - for field in json.keys(): - histogram_fields[field][1] = json[field] - except (KeyError, Exception) as err: - bu.error(exception=err) - return histogram_fields + global histogram_fields + try: + for field in json.keys(): + histogram_fields[field][1] = json[field] + except (KeyError, Exception) as err: + bu.error(exception=err) + return histogram_fields def get_players(results: list) -> set: - try: - players = set() - for res in results: - players.add(res['player']) - players.update(res['allies']) - players.update(res['enemies']) - return players - except Exception as err: - bu.error(exception=err) - return None + try: + players = set() + for res in results: + players.add(res["player"]) + players.update(res["allies"]) + players.update(res["enemies"]) + return players + except Exception as err: + bu.error(exception=err) + return None def filter_min_replays_by_player(results: list, min_replays: int) -> list: - try: - replay_counter = collections.defaultdict(def_value_zero) - for res in results: - replay_counter[res['protagonist']] +=1 - - players_enough_replays = set() - for player in replay_counter.keys(): - if replay_counter[player] >= min_replays: - players_enough_replays.add(player) - - res_ret = list() - for res in results: - if res['protagonist'] in players_enough_replays: - res_ret.append(res) - bu.verbose_std('Replays after filtering: ' + str(len(res_ret))) - return res_ret + try: + replay_counter = collections.defaultdict(def_value_zero) + for res in results: + replay_counter[res["protagonist"]] += 1 - except Exception as err: - bu.error(exception=err) - return None + players_enough_replays = set() + for player in replay_counter.keys(): + if replay_counter[player] >= min_replays: + players_enough_replays.add(player) + + res_ret = list() + for res in results: + if res["protagonist"] in players_enough_replays: + res_ret.append(res) + bu.verbose_std("Replays after filtering: " + str(len(res_ret))) + return res_ret + + except Exception as err: + bu.error(exception=err) + return None def process_player_dist(results: list, player_stats: dict, stat_id_map: dict) -> dict: - """Process player distribution""" - hist_stats = dict() - try: - for field in histogram_fields: - hist_stats[field] = PlayerHistogram(field, histogram_fields[field][0], histogram_fields[field][1], histogram_fields[field][2], histogram_fields[field][3] ) + """Process player distribution""" + hist_stats = dict() + try: + for field in histogram_fields: + hist_stats[field] = PlayerHistogram( + field, + histogram_fields[field][0], + histogram_fields[field][1], + histogram_fields[field][2], + histogram_fields[field][3], + ) - for result in results: - for player in result['allies'] | result['enemies']: # union of Sets - player_remapped = stat_id_map[player] - if player_remapped in player_stats: - if player in result['allies']: - for stat_field in hist_stats: - hist_stats[stat_field].record_ally(player_stats[player_remapped][stat_field]) - else: - for stat_field in hist_stats: - hist_stats[stat_field].record_enemy(player_stats[player_remapped][stat_field]) + for result in results: + for player in result["allies"] | result["enemies"]: # union of Sets + player_remapped = stat_id_map[player] + if player_remapped in player_stats: + if player in result["allies"]: + for stat_field in hist_stats: + hist_stats[stat_field].record_ally( + player_stats[player_remapped][stat_field] + ) + else: + for stat_field in hist_stats: + hist_stats[stat_field].record_enemy( + player_stats[player_remapped][stat_field] + ) - for stat_field in hist_stats: - hist_stats[stat_field].calc_results() - - return hist_stats - except Exception as err: - bu.error(exception=err) + for stat_field in hist_stats: + hist_stats[stat_field].calc_results() - return None + return hist_stats + except Exception as err: + bu.error(exception=err) + + return None def print_player_dist(histograms: dict): - # Print results - print('\nPlayer Histograms______', end='', flush=True) - for hist in histograms.values(): - print('') - hist.print() + # Print results + print("\nPlayer Histograms______", end="", flush=True) + for hist in histograms.values(): + print("") + hist.print() -def process_battle_results(results: dict, args : argparse.Namespace): - """Process replay battle results""" - try: - url = args.url - cats = BattleCategorizationList.get_categorizations(args) - blt_cat_list= None - - # remove if? - if len(results) > 0: - blt_cat_list = BattleCategorizationList(cats) - for result in results: - blt_cat_list.record_result(result) - if url: - blt_cat_list.record_url(result) +def process_battle_results(results: dict, args: argparse.Namespace): + """Process replay battle results""" + try: + url = args.url + cats = BattleCategorizationList.get_categorizations(args) + blt_cat_list = None - blt_cat_list.calc_results() - else: - warning_txt = 'No replays to process ' - if args.filters != None: - warning_txt = warning_txt + 'after filtering' - bu.warning(warning_txt) + # remove if? + if len(results) > 0: + blt_cat_list = BattleCategorizationList(cats) + for result in results: + blt_cat_list.record_result(result) + if url: + blt_cat_list.record_url(result) - except Exception as err: - bu.error(exception=err) - return blt_cat_list + blt_cat_list.calc_results() + else: + warning_txt = "No replays to process " + if args.filters is not None: + warning_txt = warning_txt + "after filtering" + bu.warning(warning_txt) + + except Exception as err: + bu.error(exception=err) + return blt_cat_list -def filter_results(results: list, filters_json : str, stats_filters=None) -> bool: - """Filter replays based on battle category filters""" - try: - if filters_json == None: - return results - filters = json.loads(filters_json) - bu.debug(str(filters)) - if type(filters) != dict: - bu.error('invalid --filter arguments: ' + str(filter)) - sys.exit(1) - - filter_cats = BattleCategorizationList.get_categorizations_stats(filters.keys(), stats_filters) - filter_cats = BattleCategorizationList(filter_cats) - if filter_cats.len() == 0: - bu.debug('Nothing to filter') - return results +def filter_results(results: list, filters_json: str, stats_filters=None) -> bool: + """Filter replays based on battle category filters""" + try: + if filters_json is None: + return results + filters = json.loads(filters_json) + bu.debug(str(filters)) + if type(filters) != dict: + bu.error("invalid --filter arguments: " + str(filter)) + sys.exit(1) - bu.verbose('Filters applied:') - for category in filter_cats: - cat = category.category_key - categorization = filter_cats.get_categorization(cat) - bu.debug('Category: ' + cat + ' filters: ' + str(filters)) - filters[cat] = categorization.get_filter_categories(filters[cat]) - bu.verbose(' ' + cat + ' = ' + ','.join(filters[cat])) - bu.verbose('') + filter_cats = BattleCategorizationList.get_categorizations_stats( + filters.keys(), stats_filters + ) + filter_cats = BattleCategorizationList(filter_cats) + if filter_cats.len() == 0: + bu.debug("Nothing to filter") + return results - res = list() - for result in results: - try: - cats = filter_cats.get_categories(result) - bu.debug(str(cats)) - ret = False - for cat in cats: - if cats[cat] not in filters[cat]: - ret = False - break - ret = True - if ret: - res.append(result) - except Exception as err: - bu.error(exception=err) - if len(res) == 0: - raise UserWarning('No replays left after filtering') - bu.verbose_std('Replays after filtering: ' + str(len(res))) - return res - except UserWarning: - raise - except Exception as err: - bu.error(exception=err) - return None + bu.verbose("Filters applied:") + for category in filter_cats: + cat = category.category_key + categorization = filter_cats.get_categorization(cat) + bu.debug("Category: " + cat + " filters: " + str(filters)) + filters[cat] = categorization.get_filter_categories(filters[cat]) + bu.verbose(" " + cat + " = " + ",".join(filters[cat])) + bu.verbose("") + + res = list() + for result in results: + try: + cats = filter_cats.get_categories(result) + bu.debug(str(cats)) + ret = False + for cat in cats: + if cats[cat] not in filters[cat]: + ret = False + break + ret = True + if ret: + res.append(result) + except Exception as err: + bu.error(exception=err) + if len(res) == 0: + raise UserWarning("No replays left after filtering") + bu.verbose_std("Replays after filtering: " + str(len(res))) + return res + except UserWarning: + raise + except Exception as err: + bu.error(exception=err) + return None def filter_replays(replays_in: dict, results: list) -> list: - """Filter source replays based on (filtered) results""" - replays = dict() - for res in results: - try: - # bu.debug('res[_id] : ' + res['_id'], force=True) - _id = res['_id'] - replays[_id] = replays_in[_id] - except Exception as err: - bu.error(exception=err) - return replays - - + """Filter source replays based on (filtered) results""" + replays = dict() + for res in results: + try: + # bu.debug('res[_id] : ' + res['_id'], force=True) + _id = res["_id"] + replays[_id] = replays_in[_id] + except Exception as err: + bu.error(exception=err) + return replays -async def process_player_stats(players, N_workers: int, args : argparse.Namespace, db : motor.motor_asyncio.AsyncIOMotorDatabase) -> dict: - """Start stat_workers to retrieve and store players' stats""" - ## Create queue of player-tank pairs to find stats for - try: - statsQ = asyncio.Queue() - bu.debug('Create player stats queue: ' + str(len(players)) + ' players') - stat_id_map = dict() - stat_ids = set() - stat_id_map_func = globals()[StatFunc.get_stat_id_func()] +async def process_player_stats( + players, + N_workers: int, + args: argparse.Namespace, + db: AsyncIOMotorDatabase, +) -> dict: + """Start stat_workers to retrieve and store players' stats""" + ## Create queue of player-tank pairs to find stats for + try: + statsQ = asyncio.Queue() + bu.debug("Create player stats queue: " + str(len(players)) + " players") + stat_id_map = dict() + stat_ids = set() - for player in players: - stat_id_map[player] = stat_id_map_func(player) - stat_ids.add(stat_id_map[player]) + stat_id_map_func = globals()[StatFunc.get_stat_id_func()] - bu.set_progress_bar('Fetching player stats', len(stat_ids), 25, True) - # create statsQ - for stat_id in stat_ids: - await statsQ.put(stat_id) + for player in players: + stat_id_map[player] = stat_id_map_func(player) + stat_ids.add(stat_id_map[player]) - # Process player WR / battle stats - stats_tasks = [] - bu.debug('Starting player stats workers') - for i in range(N_workers): - bu.debug("Starting worker " + str(i)) - stats_tasks.append(asyncio.create_task(stat_worker(statsQ, i, args, db))) - - bu.debug('Waiting stats workers to finish') - await statsQ.join() - bu.debug('Cancelling stats workers') - for task in stats_tasks: - task.cancel() - player_stats = dict() - stat_id_remap = dict() + bu.set_progress_bar("Fetching player stats", len(stat_ids), 25, True) + # create statsQ + for stat_id in stat_ids: + await statsQ.put(stat_id) - bu.debug('Gathering stats worker outputs') - for (stats, id_remap) in await asyncio.gather(*stats_tasks): - player_stats = {**player_stats, **stats} - stat_id_remap = {**stat_id_remap, **id_remap} + # Process player WR / battle stats + stats_tasks = [] + bu.debug("Starting player stats workers") + for i in range(N_workers): + bu.debug("Starting worker " + str(i)) + stats_tasks.append(asyncio.create_task(stat_worker(statsQ, i, args, db))) - bu.finish_progress_bar() - - ## DO REMAPPING - stat_id_map = remap_stat_id(stat_id_map, stat_id_remap) + bu.debug("Waiting stats workers to finish") + await statsQ.join() + bu.debug("Cancelling stats workers") + for task in stats_tasks: + task.cancel() + player_stats = dict() + stat_id_remap = dict() - bu.debug('Returning player_stats') - return (player_stats, stat_id_map) - except Exception as err: - bu.error(exception=err) - return None + bu.debug("Gathering stats worker outputs") + for stats, id_remap in await asyncio.gather(*stats_tasks): + player_stats = {**player_stats, **stats} + stat_id_remap = {**stat_id_remap, **id_remap} + + bu.finish_progress_bar() + + ## DO REMAPPING + stat_id_map = remap_stat_id(stat_id_map, stat_id_remap) + + bu.debug("Returning player_stats") + return (player_stats, stat_id_map) + except Exception as err: + bu.error(exception=err) + return None def remap_stat_id(stat_id_map: dict, stat_id_remap: dict): - bu.debug('Remapping stat_ids: stat_id_map: ' + str(len(stat_id_map)) + ' remap needed: ' + str(len(stat_id_remap))) - for stat_id in stat_id_map: - # bu.debug(stat_id) - if stat_id_map[stat_id] in stat_id_remap.keys(): - stat_id_map[stat_id] = stat_id_remap[stat_id_map[stat_id]] - return stat_id_map + bu.debug( + "Remapping stat_ids: stat_id_map: " + + str(len(stat_id_map)) + + " remap needed: " + + str(len(stat_id_remap)) + ) + for stat_id in stat_id_map: + # bu.debug(stat_id) + if stat_id_map[stat_id] in stat_id_remap.keys(): + stat_id_map[stat_id] = stat_id_remap[stat_id_map[stat_id]] + return stat_id_map -def calc_team_stats(results: list, player_stats : dict, stat_id_map : dict, args : argparse.Namespace) -> list: - """Calculate team stats""" - - return_list = [] +def calc_team_stats( + results: list, player_stats: dict, stat_id_map: dict, args: argparse.Namespace +) -> list: + """Calculate team stats""" - ## Bug here?? - #stat_types = player_stats[list(player_stats.keys())[0]].keys() - #stat_types = list() - stat_types = BattleCategory.get_fields_team() - - for result in results: - try: - missing_stats = 0 - n_players = len(result['allies']) + len(result['enemies']) - n_allies = collections.defaultdict(def_value_zero) - allies_stats = collections.defaultdict(def_value_zero) - n_enemies = collections.defaultdict(def_value_zero) - enemies_stats = collections.defaultdict(def_value_zero) - - #bu.debug('Processing Allies') - for ally in result['allies']: - # Player itself is not in 'allies': see read_replay_JSON() - ally_mapped = stat_id_map[ally] - if ally_mapped not in player_stats.keys(): - missing_stats += 1 - continue - for stat in stat_types: - if player_stats[ally_mapped][stat] != None: - allies_stats[stat] += player_stats[ally_mapped][stat] - n_allies[stat] += 1 - - #bu.debug('Processing Enemies') - for enemy in result['enemies']: - enemy_mapped = stat_id_map[enemy] - if enemy_mapped not in player_stats: - missing_stats += 1 - continue - for stat in stat_types: - if player_stats[enemy_mapped][stat] != None: - enemies_stats[stat] += player_stats[enemy_mapped][stat] - n_enemies[stat] += 1 - - #bu.debug('Processing player's own stats') - player_mapped = stat_id_map[result['player']] - if player_mapped not in player_stats: - missing_stats += 1 - else: - for stat in stat_types: - if player_stats[player_mapped][stat] != None: - result['player_' + stat] = player_stats[player_mapped][stat] + return_list = [] - for stat in stat_types: - if n_allies[stat] > 0: - result['allies_' + stat] = allies_stats[stat] / n_allies[stat] - else: - bu.debug('No allies stats for: ' + str(result)) - - if n_enemies[stat] > 0: - result['enemies_' + stat] = enemies_stats[stat] / n_enemies[stat] - else: - bu.debug('No enemies stats for: ' + str(result)) + ## Bug here?? + # stat_types = player_stats[list(player_stats.keys())[0]].keys() + # stat_types = list() + stat_types = BattleCategory.get_fields_team() - # Steamroller stats - result['team_result'] = str(result['allies_survived']) + '-' + str(result['enemies_survived']) - - result[N_PLAYERS] = n_players - result[MISSING_STATS] = missing_stats - return_list.append(result) - except KeyError as err: - bu.error('Key not found', err) - except Exception as err: - bu.error(exception=err) - - return return_list + for result in results: + try: + missing_stats = 0 + n_players = len(result["allies"]) + len(result["enemies"]) + n_allies = collections.defaultdict(def_value_zero) + allies_stats = collections.defaultdict(def_value_zero) + n_enemies = collections.defaultdict(def_value_zero) + enemies_stats = collections.defaultdict(def_value_zero) + + # bu.debug('Processing Allies') + for ally in result["allies"]: + # Player itself is not in 'allies': see read_replay_JSON() + ally_mapped = stat_id_map[ally] + if ally_mapped not in player_stats.keys(): + missing_stats += 1 + continue + for stat in stat_types: + if player_stats[ally_mapped][stat] is not None: + allies_stats[stat] += player_stats[ally_mapped][stat] + n_allies[stat] += 1 + + # bu.debug('Processing Enemies') + for enemy in result["enemies"]: + enemy_mapped = stat_id_map[enemy] + if enemy_mapped not in player_stats: + missing_stats += 1 + continue + for stat in stat_types: + if player_stats[enemy_mapped][stat] is not None: + enemies_stats[stat] += player_stats[enemy_mapped][stat] + n_enemies[stat] += 1 + + # bu.debug('Processing player's own stats') + player_mapped = stat_id_map[result["player"]] + if player_mapped not in player_stats: + missing_stats += 1 + else: + for stat in stat_types: + if player_stats[player_mapped][stat] is not None: + result["player_" + stat] = player_stats[player_mapped][stat] + + for stat in stat_types: + if n_allies[stat] > 0: + result["allies_" + stat] = allies_stats[stat] / n_allies[stat] + else: + bu.debug("No allies stats for: " + str(result)) + + if n_enemies[stat] > 0: + result["enemies_" + stat] = enemies_stats[stat] / n_enemies[stat] + else: + bu.debug("No enemies stats for: " + str(result)) + + # Steamroller stats + result["team_result"] = ( + str(result["allies_survived"]) + "-" + str(result["enemies_survived"]) + ) + + result[N_PLAYERS] = n_players + result[MISSING_STATS] = missing_stats + return_list.append(result) + except KeyError as err: + bu.error("Key not found", err) + except Exception as err: + bu.error(exception=err) + + return return_list -async def stat_worker(queue : asyncio.Queue, workerID: int, args : argparse.Namespace, db : motor.motor_asyncio.AsyncIOMotorDatabase) -> list: - """Worker thread to find stats for player / tank pairs""" - # global wg - stats = dict() - stat_id_remap = dict() +async def stat_worker( + queue: asyncio.Queue, + workerID: int, + args: argparse.Namespace, + db: AsyncIOMotorDatabase, +) -> list: + """Worker thread to find stats for player / tank pairs""" + global wg + stats = dict() + stat_id_remap = dict() - stat_db_func = globals()[StatFunc.get_db_func()] - stat_wg_func = globals()[StatFunc.get_wg_func()] + stat_db_func = globals()[StatFunc.get_db_func()] + stat_wg_func = globals()[StatFunc.get_wg_func()] - bu.debug("workedID: " + str(workerID) + ' started', id=workerID) - try: - while True: - stat_id = await queue.get() - try: - bu.debug('Stat_id: ' + stat_id, id=workerID) - bu.print_progress() - stats_tmp = None - - # Try cache first - pruned_stat_id = prune_stat_id(stat_id) - if (pruned_stat_id not in stats): - stats_tmp = await stat_wg_func(pruned_stat_id, cache_only = True) - else: - stat_id_remap[stat_id] = pruned_stat_id - continue + bu.debug("workedID: " + str(workerID) + " started", id=workerID) + try: + while True: + stat_id = await queue.get() + try: + bu.debug("Stat_id: " + stat_id, id=workerID) + bu.print_progress() + stats_tmp = None - if (stats_tmp != None): - stats[pruned_stat_id] = stats_tmp - stat_id_remap[stat_id] = pruned_stat_id - continue - - # try DB - stats[stat_id] = await stat_db_func(db, stat_id) - bu.debug('get_db_' + args.stat_func + '_stats returned: account_id=' + str(get_account_id_f_stat_id(stat_id)) + ': ' + str(stats[stat_id]), workerID) + # Try cache first + pruned_stat_id = prune_stat_id(stat_id) + if pruned_stat_id not in stats: + stats_tmp = await stat_wg_func(pruned_stat_id, cache_only=True) + else: + stat_id_remap[stat_id] = pruned_stat_id + continue - # no DB stats found, trying WG AP - if (stats[stat_id] == None): - stats[pruned_stat_id] = await stat_wg_func(pruned_stat_id) - stat_id_remap[stat_id] = pruned_stat_id - del stats[stat_id] - bu.debug('get_wg_' + args.stat_func + '_stats returned: account_id=' + str(get_account_id_f_stat_id(pruned_stat_id)) + ': ' + str(stats[pruned_stat_id]), workerID) + if stats_tmp is not None: + stats[pruned_stat_id] = stats_tmp + stat_id_remap[stat_id] = pruned_stat_id + continue - except KeyError as err: - bu.error('Key not found', err, id=workerID) - except Exception as err: - bu.error('Unexpected error', err, id=workerID) - finally: - queue.task_done() + # try DB + stats[stat_id] = await stat_db_func(db, stat_id) + bu.debug( + "get_db_" + + args.stat_func + + "_stats returned: account_id=" + + str(get_account_id_f_stat_id(stat_id)) + + ": " + + str(stats[stat_id]), + workerID, + ) + + # no DB stats found, trying WG AP + if stats[stat_id] is None: + stats[pruned_stat_id] = await stat_wg_func(pruned_stat_id) + stat_id_remap[stat_id] = pruned_stat_id + del stats[stat_id] + bu.debug( + "get_wg_" + + args.stat_func + + "_stats returned: account_id=" + + str(get_account_id_f_stat_id(pruned_stat_id)) + + ": " + + str(stats[pruned_stat_id]), + workerID, + ) + + except KeyError as err: + bu.error("Key not found", err, id=workerID) + except Exception as err: + bu.error("Unexpected error", err, id=workerID) + finally: + queue.task_done() + + except (asyncio.CancelledError, concurrent.futures.CancelledError): + bu.debug("Stats queue is empty", id=workerID) + except Exception as err: + bu.error(exception=err, id=workerID) + finally: + # remove empty stats + try: + keys_2_del = [] + for key in stats: + if stats[key] is None: + keys_2_del.append(key) + for key in keys_2_del: + del stats[key] + except KeyError as err: + bu.error("Error in pruning empty stats", err, id=workerID) + # bu.debug('Returning stats & exiting') + return (stats, stat_id_remap) - except (asyncio.CancelledError, concurrent.futures.CancelledError): - bu.debug('Stats queue is empty', id=workerID) - except Exception as err: - bu.error(exception=err, id=workerID) - finally: - # remove empty stats - try: - keys_2_del = [] - for key in stats: - if stats[key] == None: - keys_2_del.append(key) - for key in keys_2_del: - del stats[key] - except KeyError as err: - bu.error('Error in pruning empty stats', err, id=workerID) - # bu.debug('Returning stats & exiting') - return (stats, stat_id_remap) - ## player stat functions: tank -async def get_wg_tank_stats(stat_id_str: str, cache_only = False) -> dict: - """Get player stats from WG. Returns WR per tier of tank_id""" - try: - (account_id, tank_id) = str2ints(stat_id_str) - - # 'battles' must always be there - hist_stats = [ 'all.' + x for x in histogram_fields.keys() ] - hist_stats.append('tank_id') +async def get_wg_tank_stats(stat_id_str: str, cache_only=False) -> dict: + """Get player stats from WG. Returns WR per tier of tank_id""" + global wg + try: + (account_id, tank_id) = str2ints(stat_id_str) - player_stats = await wg.get_player_tank_stats(account_id, [ tank_id ], hist_stats, cache_only = cache_only) - #bu.debug('account_id: ' + str(account_id) + ' ' + str(player_stats)) + # 'battles' must always be there + hist_stats = ["all." + x for x in histogram_fields.keys()] + hist_stats.append("tank_id") - return tank_stats_helper(player_stats) + player_stats = await wg.get_player_tank_stats( + account_id, [tank_id], hist_stats, cache_only=cache_only + ) + # bu.debug('account_id: ' + str(account_id) + ' ' + str(player_stats)) - except KeyError as err: - bu.error('account_id: ' + str(account_id) + ' tank_id:' + str(tank_id) +' : Key not found', err) - except Exception as err: - bu.error(exception=err) - return None + return tank_stats_helper(player_stats) + + except KeyError as err: + bu.error( + "account_id: " + + str(account_id) + + " tank_id:" + + str(tank_id) + + " : Key not found", + err, + ) + except Exception as err: + bu.error(exception=err) + return None -async def get_db_tank_stats(db : motor.motor_asyncio.AsyncIOMotorDatabase, stat_id_str: str) -> dict: - """Get player stats from MongoDB (you are very unlikely to have it, unless you have set it up)""" - if db == None: - return None - try: - dbc = db[DB_C_TANK_STATS] - ( account_id, tank_id, battletime ) = str2ints(stat_id_str) - time_buffer = 2*7*24*3600 +async def get_db_tank_stats(db: AsyncIOMotorDatabase, stat_id_str: str) -> dict: + """Get player stats from MongoDB (you are very unlikely to have it, unless you have set it up)""" + if db is None: + return None + try: + dbc = db[DB_C_TANK_STATS] + (account_id, tank_id, battletime) = str2ints(stat_id_str) + time_buffer = 2 * 7 * 24 * 3600 - hist_stats = [ 'all.' + x for x in histogram_fields.keys() ] - hist_stats.append('tank_id') - hist_stats.append('account_id') - hist_stats.append('last_battle_time') - project = dict() - for stat in hist_stats: - project[stat] = True - project['_id'] = False - bu.debug('find(): tank_id={} account_id={} last_battle_time>={}'.format(tank_id, account_id, battletime - time_buffer)) - cursor = dbc.find({ '$and': [ { 'tank_id' : tank_id }, { 'account_id': account_id }, { 'last_battle_time': { '$gte': battletime - time_buffer }} ] }, projection=project).sort('last_battle_time',-1).limit(1) + hist_stats = ["all." + x for x in histogram_fields.keys()] + hist_stats.append("tank_id") + hist_stats.append("account_id") + hist_stats.append("last_battle_time") + project = dict() + for stat in hist_stats: + project[stat] = True + project["_id"] = False + bu.debug( + "find(): tank_id={} account_id={} last_battle_time>={}".format( + tank_id, account_id, battletime - time_buffer + ) + ) + cursor = ( + dbc.find( + { + "$and": [ + {"tank_id": tank_id}, + {"account_id": account_id}, + {"last_battle_time": {"$gte": battletime - time_buffer}}, + ] + }, + projection=project, + ) + .sort("last_battle_time", -1) + .limit(1) + ) - # pipeline = [ { '$match': { '$and': [ { 'account_id': account_id }, { 'last_battle_time': { '$lte': battletime + time_buffer }}, { 'tank_id' : tank_id } ]}}, - # { '$sort': { 'last_battle_time': -1 }}, - # { '$group': { '_id': '$tank_id', 'doc': { '$first': '$$ROOT' }}}, - # { '$replaceRoot': { 'newRoot': '$doc' }}, - # { '$project': { '_id': 0 }} ] + # pipeline = [ { '$match': { '$and': [ { 'account_id': account_id }, { 'last_battle_time': { '$lte': battletime + time_buffer }}, { 'tank_id' : tank_id } ]}}, + # { '$sort': { 'last_battle_time': -1 }}, + # { '$group': { '_id': '$tank_id', 'doc': { '$first': '$$ROOT' }}}, + # { '$replaceRoot': { 'newRoot': '$doc' }}, + # { '$project': { '_id': 0 }} ] - # cursor = dbc.aggregate(pipeline, allowDiskUse=True) - #cursor = dbc.aggregate(pipeline) + # cursor = dbc.aggregate(pipeline, allowDiskUse=True) + # cursor = dbc.aggregate(pipeline) - stats = await cursor.to_list(1) - # store cache - # await wg.save_tank_stats(account_id, [tank_id], stats) - - return tank_stats_helper(stats) - - except Exception as err: - bu.error('account_id: ' + str(account_id) + ' Error', err) - return None + stats = await cursor.to_list(1) + # store cache + # await wg.save_tank_stats(account_id, [tank_id], stats) + + return tank_stats_helper(stats) + + except Exception as err: + bu.error("account_id: " + str(account_id) + " Error", err) + return None ## player stat functions: tank_tier -async def get_wg_tank_tier_stats(stat_id_str: str, cache_only = False) -> dict: - """Get player stats from WG. Returns WR per tier of tank_id""" - try: - (account_id, tier) = str2ints(stat_id_str) - tier_tanks = wg.get_tanks_by_tier(tier) +async def get_wg_tank_tier_stats(stat_id_str: str, cache_only: bool = False) -> dict: + """Get player stats from WG. Returns WR per tier of tank_id""" + global wg + try: + (account_id, tier) = str2ints(stat_id_str) + tier_tanks = wg.get_tanks_by_tier(tier) - # 'battles' must always be there - hist_stats = [ 'all.' + x for x in histogram_fields.keys() ] - hist_stats.append('tank_id') + # 'battles' must always be there + hist_stats = ["all." + x for x in histogram_fields.keys()] + hist_stats.append("tank_id") - player_stats = await wg.get_player_tank_stats(account_id, tier_tanks, hist_stats, cache_only = cache_only) - #bu.debug('account_id: ' + str(account_id) + ' ' + str(player_stats)) + player_stats = await wg.get_player_tank_stats( + account_id, tier_tanks, hist_stats, cache_only=cache_only + ) + # bu.debug('account_id: ' + str(account_id) + ' ' + str(player_stats)) - return tank_stats_helper(player_stats) + return tank_stats_helper(player_stats) - except KeyError as err: - bu.error('account_id: ' + str(account_id) + ' Key not found', err) - except Exception as err: - bu.error(exception=err) - return None + except KeyError as err: + bu.error("account_id: " + str(account_id) + " Key not found", err) + except Exception as err: + bu.error(exception=err) + return None ## player stat functions: tier_x -async def get_wg_tier_x_stats(stat_id_str: str, cache_only = False) -> dict: - """Get player stats from WG. Returns WR per tier of tank_id""" - try: - account_id = int(stat_id_str) - tier_tanks = wg.get_tanks_by_tier(10) +async def get_wg_tier_x_stats(stat_id_str: str, cache_only=False) -> dict: + """Get player stats from WG. Returns WR per tier of tank_id""" + global wg + try: + account_id = int(stat_id_str) + tier_tanks = wg.get_tanks_by_tier(10) - # 'battles' must always be there - hist_stats = [ 'all.' + x for x in histogram_fields.keys() ] - hist_stats.append('tank_id') + # 'battles' must always be there + hist_stats = ["all." + x for x in histogram_fields.keys()] + hist_stats.append("tank_id") - player_stats = await wg.get_player_tank_stats(account_id, tier_tanks, hist_stats, cache_only = cache_only) - #bu.debug('account_id: ' + str(account_id) + ' ' + str(player_stats)) + player_stats = await wg.get_player_tank_stats( + account_id, tier_tanks, hist_stats, cache_only=cache_only + ) + # bu.debug('account_id: ' + str(account_id) + ' ' + str(player_stats)) - return tank_stats_helper(player_stats) + return tank_stats_helper(player_stats) - except KeyError as err: - bu.error('account_id: ' + str(account_id) + ' Key not found', err) - except Exception as err: - bu.error(exception=err) - return None - - -async def get_db_tank_tier_stats(db : motor.motor_asyncio.AsyncIOMotorDatabase, stat_id_str: str) -> dict: - """Get player stats from MongoDB (you are very unlikely to have it, unless you have set it up)""" - if db == None: - return None - try: - dbc = db[DB_C_TANK_STATS] - ( account_id, tier, battletime ) = str2ints(stat_id_str) - tier_tanks = wg.get_tanks_by_tier(tier) - time_buffer = 2*7*24*3600 - - pipeline = [ { '$match': { '$and': [ { 'account_id': account_id }, { 'last_battle_time': { '$lte': battletime + time_buffer }}, { 'tank_id' : {'$in': tier_tanks} } ]}}, - { '$sort': { 'last_battle_time': -1 }}, - { '$group': { '_id': '$tank_id', 'doc': { '$first': '$$ROOT' }}}, - { '$replaceRoot': { 'newRoot': '$doc' }}, - { '$project': { '_id': 0 }} ] - - # cursor = dbc.aggregate(pipeline, allowDiskUse=True) - cursor = dbc.aggregate(pipeline) - - stats = await cursor.to_list(1000) - # await wg.save_tank_stats(account_id, tier_tanks, stats) - return tank_stats_helper(stats) - - except Exception as err: - bu.error('account_id: ' + str(account_id) + ' Error', err) - return None + except KeyError as err: + bu.error("account_id: " + str(account_id) + " Key not found", err) + except Exception as err: + bu.error(exception=err) + return None -async def get_db_tier_x_stats(db : motor.motor_asyncio.AsyncIOMotorDatabase, stat_id_str: str) -> dict: - """Get player stats from MongoDB (you are very unlikely to have it, unless you have set it up)""" - if db == None: - return None - try: - ( account_id, battletime ) = str2ints(stat_id_str) - return await get_db_tank_tier_stats(db,get_stat_id(account_id, 10, battletime) ) - except Exception as err: - bu.error('account_id: ' + str(account_id) + ' Error', err) - return None - +async def get_db_tank_tier_stats(db: AsyncIOMotorDatabase, stat_id_str: str) -> dict: + """Get player stats from MongoDB (you are very unlikely to have it, unless you have set it up)""" + global wg + if db is None: + return None + try: + dbc = db[DB_C_TANK_STATS] + (account_id, tier, battletime) = str2ints(stat_id_str) + tier_tanks = wg.get_tanks_by_tier(tier) + time_buffer = 2 * 7 * 24 * 3600 + + pipeline = [ + { + "$match": { + "$and": [ + {"account_id": account_id}, + {"last_battle_time": {"$lte": battletime + time_buffer}}, + {"tank_id": {"$in": tier_tanks}}, + ] + } + }, + {"$sort": {"last_battle_time": -1}}, + {"$group": {"_id": "$tank_id", "doc": {"$first": "$$ROOT"}}}, + {"$replaceRoot": {"newRoot": "$doc"}}, + {"$project": {"_id": 0}}, + ] + + # cursor = dbc.aggregate(pipeline, allowDiskUse=True) + cursor = dbc.aggregate(pipeline) + + stats = await cursor.to_list(1000) + # await wg.save_tank_stats(account_id, tier_tanks, stats) + return tank_stats_helper(stats) + + except Exception as err: + bu.error("account_id: " + str(account_id) + " Error", err) + return None + + +async def get_db_tier_x_stats(db: AsyncIOMotorDatabase, stat_id_str: str) -> dict: + """Get player stats from MongoDB (you are very unlikely to have it, unless you have set it up)""" + if db is None: + return None + try: + (account_id, battletime) = str2ints(stat_id_str) + return await get_db_tank_tier_stats(db, get_stat_id(account_id, 10, battletime)) + except Exception as err: + bu.error("account_id: " + str(account_id) + " Error", err) + return None + ## player stat functions: player -async def get_db_player_stats(db : motor.motor_asyncio.AsyncIOMotorDatabase, stat_id_str: str) -> dict: - """Get player stats from MongoDB (you are very unlikely to have it, unless you have set it up)""" - if db == None: - return None - try: - dbc = db[DB_C_TANK_STATS] ## not collecting player stats yet 2019-12-23 - ( account_id, battletime ) = str2ints(stat_id_str) - time_buffer = 2*7*24*3600 - - pipeline = [ { '$match': { '$and': [ { 'account_id': account_id }, { 'last_battle_time': { '$lte': battletime + time_buffer }} ]}}, - { '$sort': { 'last_battle_time': -1 }}, - { '$group': { '_id': { 'tank_id':'$tank_id'}, 'doc': { '$first': '$$ROOT' }}}, - { '$replaceRoot': { 'newRoot': '$doc' }}, - { '$project': { '_id': 0 }} ] +async def get_db_player_stats(db: AsyncIOMotorDatabase, stat_id_str: str) -> dict: + """Get player stats from MongoDB (you are very unlikely to have it, unless you have set it up)""" + if db is None: + return None + try: + dbc = db[DB_C_TANK_STATS] ## not collecting player stats yet 2019-12-23 + (account_id, battletime) = str2ints(stat_id_str) + time_buffer = 2 * 7 * 24 * 3600 - # cursor = dbc.aggregate(pipeline, allowDiskUse=True) - cursor = dbc.aggregate(pipeline) + pipeline = [ + { + "$match": { + "$and": [ + {"account_id": account_id}, + {"last_battle_time": {"$lte": battletime + time_buffer}}, + ] + } + }, + {"$sort": {"last_battle_time": -1}}, + {"$group": {"_id": {"tank_id": "$tank_id"}, "doc": {"$first": "$$ROOT"}}}, + {"$replaceRoot": {"newRoot": "$doc"}}, + {"$project": {"_id": 0}}, + ] - stats = await cursor.to_list(1000) - # await wg.save_tank_stats(account_id, [], stats) - return tank_stats_helper(stats) - - except Exception as err: - bu.error('account_id: ' + str(account_id) + ' Error', err) - return None + # cursor = dbc.aggregate(pipeline, allowDiskUse=True) + cursor = dbc.aggregate(pipeline) + + stats = await cursor.to_list(1000) + # await wg.save_tank_stats(account_id, [], stats) + return tank_stats_helper(stats) + + except Exception as err: + bu.error("account_id: " + str(account_id) + " Error", err) + return None -async def get_wg_player_stats(stat_id_str: str, cache_only = False) -> dict: - """Get player stats from WG. Returns WR per tier of tank_id""" - try: - account_id = int(stat_id_str) - - # 'battles' must always be there - hist_stats = [ 'statistics.all.' + x for x in histogram_fields.keys() ] - hist_stats.append('account_id') +async def get_wg_player_stats(stat_id_str: str, cache_only=False) -> dict: + """Get player stats from WG. Returns WR per tier of tank_id""" + try: + account_id = int(stat_id_str) - player_stats = await wg.get_player_stats(account_id, hist_stats, cache_only = cache_only) - #bu.debug('account_id: ' + str(account_id) + ' ' + str(player_stats)) + # 'battles' must always be there + hist_stats = ["statistics.all." + x for x in histogram_fields.keys()] + hist_stats.append("account_id") - return await player_stats_helper(player_stats) + player_stats = await wg.get_player_stats( + account_id, hist_stats, cache_only=cache_only + ) + # bu.debug('account_id: ' + str(account_id) + ' ' + str(player_stats)) - except KeyError as err: - bu.error('account_id: ' + str(account_id) + ' Key not found', err) - except Exception as err: - bu.error(exception=err) - return None + return await player_stats_helper(player_stats) + + except KeyError as err: + bu.error("account_id: " + str(account_id) + " Key not found", err) + except Exception as err: + bu.error(exception=err) + return None def tank_stats_helper(stat_list: list): - """Helpher func got get_db_tank_tier_stats() and get_wg_tank_tier_stats()""" - try: - if stat_list == None: - return None - stats = collections.defaultdict(def_value_zero) - hist_fields = histogram_fields.keys() - if 'battles' not in hist_fields: - bu.error('\'battles\' must be defined in \'histogram_fields\'') - return None + """Helpher func got get_db_tank_tier_stats() and get_wg_tank_tier_stats()""" + try: + if stat_list is None: + return None + stats = collections.defaultdict(def_value_zero) + hist_fields = histogram_fields.keys() + if "battles" not in hist_fields: + bu.error("'battles' must be defined in 'histogram_fields'") + return None - for tank_stat in stat_list: - tank_stat = tank_stat['all'] - for field in hist_fields: - stats[field] += max(0, tank_stat[field]) + for tank_stat in stat_list: + tank_stat = tank_stat["all"] + for field in hist_fields: + stats[field] += max(0, tank_stat[field]) - if stats['battles'] == 0: - return None + if stats["battles"] == 0: + return None - for field in hist_fields: - if field == 'battles': continue - stats[field] = stats[field] / stats['battles'] - return stats + for field in hist_fields: + if field == "battles": + continue + stats[field] = stats[field] / stats["battles"] + return stats - except Exception as err: - bu.error(exception=err) - return None + except Exception as err: + bu.error(exception=err) + return None async def player_stats_helper(player_stats: dict): - """Extract relevant stats for player histogram from raw player stats""" - try: - if player_stats == None: - return None - stats = dict() - hist_fields = histogram_fields.keys() - if 'battles' not in hist_fields: - bu.error('\'battles\' must be defined in \'histogram_fields\'') - return None - - for field in hist_fields: - stats[field] = max(0, player_stats['statistics']['all'][field]) + """Extract relevant stats for player histogram from raw player stats""" + try: + if player_stats is None: + return None + stats = dict() + hist_fields = histogram_fields.keys() + if "battles" not in hist_fields: + bu.error("'battles' must be defined in 'histogram_fields'") + return None - if stats['battles'] == 0: - return None + for field in hist_fields: + stats[field] = max(0, player_stats["statistics"]["all"][field]) - for field in hist_fields: - if field == 'battles': continue - stats[field] = stats[field] / stats['battles'] - return stats + if stats["battles"] == 0: + return None - except Exception as err: - bu.error(exception=err) - return None + for field in hist_fields: + if field == "battles": + continue + stats[field] = stats[field] / stats["battles"] + return stats + + except Exception as err: + bu.error(exception=err) + return None -async def mk_replayQ(queue : asyncio.Queue, args : argparse.Namespace, db : motor.motor_asyncio.AsyncIOMotorDatabase = None): - """Create queue of replays to post""" - p_replayfile = re.compile('.*\\.wotbreplay\\.json$') - files = args.files - Nreplays = 0 - if files[0] == 'db:': - if db == None: - bu.error('No database connection opened') - sys.exit(1) - try: - dbc = db[DB_C_REPLAYS] - cursor = None - if args.filters_db != None: - bu.debug(str(args.filters_db)) - filters = json.loads(args.filters_db) - bu.debug(json.dumps(filters, indent=2)) - cursor = dbc.find(filters) - else: - # select all - cursor = dbc.find(dict()) - bu.debug('Reading replays...') - async for replay_json in cursor: - _id = replay_json['_id'] - #del(replay_json['_id']) - await queue.put(await mk_readerQ_item(replay_json, _id=_id)) - Nreplays += 1 - bu.debug('All the matching replays have been read from the DB') - except Exception as err: - bu.error(exception=err) +async def mk_replayQ( + queue: asyncio.Queue, + args: argparse.Namespace, + db: AsyncIOMotorDatabase = None, +): + """Create queue of replays to post""" + p_replayfile = re.compile(".*\\.wotbreplay\\.json$") + files = args.files + Nreplays = 0 + if files[0] == "db:": + if db is None: + bu.error("No database connection opened") + sys.exit(1) + try: + dbc = db[DB_C_REPLAYS] + cursor = None + if args.filters_db is not None: + bu.debug(str(args.filters_db)) + filters = json.loads(args.filters_db) + bu.debug(json.dumps(filters, indent=2)) + cursor = dbc.find(filters) + else: + # select all + cursor = dbc.find(dict()) + bu.debug("Reading replays...") + async for replay_json in cursor: + _id = replay_json["_id"] + # del(replay_json['_id']) + await queue.put(await mk_readerQ_item(replay_json, _id=_id)) + Nreplays += 1 + bu.debug("All the matching replays have been read from the DB") + except Exception as err: + bu.error(exception=err) - elif files[0] == '-': - bu.debug('reading replay file list from STDIN') - stdin, _ = await aioconsole.get_standard_streams() - while True: - try: - line = (await stdin.readline()).decode('utf-8').rstrip() - if not line: break - - if (p_replayfile.match(line) != None): - replay_json = await bu.open_JSON(line, wi.chk_JSON_replay) - await queue.put(await mk_readerQ_item(replay_json, filename=line)) - except Exception as err: - bu.error(exception=err) - else: - for fn in files: - try: - # bu.debug('Filename: ' + fn) - if fn.endswith('"'): - fn = fn[:-1] - if os.path.isfile(fn) and (p_replayfile.match(fn) != None): - replay_json = await bu.open_JSON(fn, wi.chk_JSON_replay) - await queue.put(await mk_readerQ_item(replay_json, filename=fn)) - bu.debug('File added to queue: ' + fn) - Nreplays += 1 - elif os.path.isdir(fn): - with os.scandir(fn) as dirEntry: - for entry in dirEntry: - if entry.is_file() and (p_replayfile.match(entry.name) != None): - bu.debug(entry.name) - replay_json = await bu.open_JSON(entry.path, wi.chk_JSON_replay) - await queue.put(await mk_readerQ_item(replay_json, filename=entry.name)) - bu.debug('File added to queue: ' + entry.path) - Nreplays += 1 - except Exception as err: - bu.error(exception=err) - return Nreplays + elif files[0] == "-": + bu.debug("reading replay file list from STDIN") + stdin, _ = await aioconsole.get_standard_streams() + while True: + try: + line = (await stdin.readline()).decode("utf-8").rstrip() + if not line: + break + + if p_replayfile.match(line) is not None: + replay_json = await bu.open_JSON(line, wi.chk_JSON_replay) + await queue.put(await mk_readerQ_item(replay_json, filename=line)) + except Exception as err: + bu.error(exception=err) + else: + for fn in files: + try: + # bu.debug('Filename: ' + fn) + if fn.endswith('"'): + fn = fn[:-1] + if os.path.isfile(fn) and (p_replayfile.match(fn) is not None): + replay_json = await bu.open_JSON(fn, wi.chk_JSON_replay) + await queue.put(await mk_readerQ_item(replay_json, filename=fn)) + bu.debug("File added to queue: " + fn) + Nreplays += 1 + elif os.path.isdir(fn): + with os.scandir(fn) as dirEntry: + for entry in dirEntry: + if entry.is_file() and ( + p_replayfile.match(entry.name) is not None + ): + bu.debug(entry.name) + replay_json = await bu.open_JSON( + entry.path, wi.chk_JSON_replay + ) + await queue.put( + await mk_readerQ_item( + replay_json, filename=entry.name + ) + ) + bu.debug("File added to queue: " + entry.path) + Nreplays += 1 + except Exception as err: + bu.error(exception=err) + return Nreplays -async def mk_readerQ_item(replay_json, filename : str = None, _id: str = None) -> list: - """Make an item to replay queue""" - global REPLAY_N - REPLAY_N +=1 - try: - if wi.chk_JSON_replay(replay_json): - if not '_id' in replay_json: - if _id == None: - _id = wi.read_replay_id(replay_json) - replay_json['_id'] = _id - else: - replay_json = None # mark error - except Exception as err: - bu.error(exception=err) - - if filename != None: - return [replay_json, REPLAY_N, os.path.basename(filename) ] - elif _id != None: - return [replay_json, REPLAY_N, 'DB: ' + _id ] - else: - return None +async def mk_readerQ_item(replay_json, filename: str = None, _id: str = None) -> list: + """Make an item to replay queue""" + global REPLAY_N + REPLAY_N += 1 + try: + if wi.chk_JSON_replay(replay_json): + if not "_id" in replay_json: + if _id is None: + _id = wi.read_replay_id(replay_json) + replay_json["_id"] = _id + else: + replay_json = None # mark error + except Exception as err: + bu.error(exception=err) + + if filename is not None: + return [replay_json, REPLAY_N, os.path.basename(filename)] + elif _id is not None: + return [replay_json, REPLAY_N, "DB: " + _id] + else: + return None -async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Namespace): - """Async Worker to process the replay queue""" - #global SKIPPED_N - results = [] - playerstanks = set() - replays = dict() - try: - while True: - item = await queue.get() - replay_json = item[0] - replayID = item[1] - replay_file = item[2] +async def replay_reader(queue: asyncio.Queue, readerID: int, args: argparse.Namespace): + """Async Worker to process the replay queue""" + # global SKIPPED_N + global wi + results = [] + playerstanks = set() + replays = dict() + try: + while True: + item = await queue.get() + replay_json = item[0] + replayID = item[1] + replay_file = item[2] - try: - # msg_str = 'Replay[' + str(replayID) + ']: ' - if replay_json == None: - bu.warning('Invalid replay. Skipping: ' + (replay_file if replay_file != None else '') ) - queue.task_done() - continue - - ## Read the replay JSON - bu.debug('reading replay', readerID) - result = await read_replay_JSON(replay_json, args) - bu.print_progress() - if result == None: - bu.warning('Invalid replay ' + (replay_file if replay_file != None else '') ) - queue.task_done() - continue - - playerstanks.update(set(result['allies'])) - playerstanks.update(set(result['enemies'])) - playerstanks.update(set([result['player']])) - _id = wi.read_replay_id(replay_json) - replays[_id] = replay_json - results.append(result) + try: + # msg_str = 'Replay[' + str(replayID) + ']: ' + if replay_json is None: + bu.warning( + "Invalid replay. Skipping: " + + (replay_file if replay_file is not None else "") + ) + queue.task_done() + continue - bu.debug('Marking replay [' + str(replayID) + '] done', id=readerID) - - except Exception as err: - bu.error(exception=err) - queue.task_done() - except (asyncio.CancelledError, concurrent.futures.CancelledError): - bu.debug( str(len(results)) + ' replays, ' + str(len(playerstanks)) + ' player/tanks', readerID) - return results, playerstanks, replays - return None + ## Read the replay JSON + bu.debug("reading replay", readerID) + result = await read_replay_JSON(replay_json, args) + bu.print_progress() + if result is None: + bu.warning( + "Invalid replay " + + (replay_file if replay_file is not None else "") + ) + queue.task_done() + continue + + playerstanks.update(set(result["allies"])) + playerstanks.update(set(result["enemies"])) + playerstanks.update(set([result["player"]])) + _id = wi.read_replay_id(replay_json) + replays[_id] = replay_json + results.append(result) + + bu.debug("Marking replay [" + str(replayID) + "] done", id=readerID) + + except Exception as err: + bu.error(exception=err) + queue.task_done() + except (asyncio.CancelledError, concurrent.futures.CancelledError): + bu.debug( + str(len(results)) + " replays, " + str(len(playerstanks)) + " player/tanks", + readerID, + ) + return results, playerstanks, replays + return None -async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict: - """Parse replay JSON dict""" - global REPLAY_I - account_id = args.account_id - url = args.url - #db = args.db - result = dict() - try: - # bu.debug(str(replay_json)) - if not wi.chk_JSON_replay(replay_json): - bu.debug('Invalid replay') - return None - - result['_id'] = replay_json['_id'] - replay_summary = replay_json['data']['summary'] - result['battle_start_timestamp'] = int(replay_summary['battle_start_timestamp']) - # TBD... - protagonist = int(replay_summary['protagonist']) - - # For filtering replays per submitter - result['protagonist'] = protagonist +async def read_replay_JSON(replay_json: dict, args: argparse.Namespace) -> dict: + """Parse replay JSON dict""" + global REPLAY_I, wg + account_id = args.account_id + url = args.url + # db = args.db + result = dict() + try: + # bu.debug(str(replay_json)) + if not wi.chk_JSON_replay(replay_json): + bu.debug("Invalid replay") + return None - if account_id == None: - account_id = protagonist - elif protagonist != account_id: - if account_id in replay_summary['enemies']: - # switch the teams... - if replay_summary['battle_result'] != 2: - if replay_summary['battle_result'] == 0: - replay_summary['battle_result'] = 1 - else: - replay_summary['battle_result'] = 0 - tmp = replay_summary['enemies'] - replay_summary['enemies'] = replay_summary['allies'] - replay_summary['allies'] = tmp - elif account_id in replay_summary['allies']: - pass - else: - # looking for an account_id, but account_id not found in the teams => skipping the replay - return None - if url: - result['url'] = replay_json['data']['view_url'] - - for key in replay_summary_flds: - result[key] = replay_summary[key] - - except Exception as err: - bu.error(exception=err) - return None - try: - #bu.debug('Part 2') - result['allies'] = set() - result['enemies'] = set() - result['allies_survived'] = 0 # for steamroller stats - result['enemies_survived'] = 0 # for steamroller stats - btl_duration = 0 - btl_tier = 0 - protagonist_tank = None - for player in replay_summary['details']: - btl_duration = max(btl_duration, player['time_alive']) - player_tank_tier = wg.get_tank_data(player['vehicle_descr'], 'tier') - btl_tier = max(btl_tier, player_tank_tier) + result["_id"] = replay_json["_id"] + replay_summary = replay_json["data"]["summary"] + result["battle_start_timestamp"] = int(replay_summary["battle_start_timestamp"]) + # TBD... + protagonist = int(replay_summary["protagonist"]) - if player['dbid'] == account_id: - # player itself is not put in results['allies'] - tmp = dict() - tmp['account_id'] = account_id - tank_id = player['vehicle_descr'] - tmp['tank_id'] = tank_id - tmp['tank_tier'] = player_tank_tier - tmp['tank_name'] = wg.get_tank_name(tmp['tank_id']) - tmp['tank_type'] = wg.get_tank_type_id(tmp['tank_id']) - tmp['tank_nation'] = wg.get_tank_nation_id(tmp['tank_id']) - tmp['is_premium'] = int(wg.is_premium(tmp['tank_id'])) - platoon_ndx = player['squad_index'] - tmp['squad_index'] = platoon_ndx - tmp['in_platoon'] = 0 if platoon_ndx == None else 1 - for key in replay_details_flds: - tmp[key] = player[key] - if player['hitpoints_left'] == 0: - tmp['survived'] = 0 - tmp['destroyed'] = 1 - else: - tmp['survived'] = 1 - tmp['destroyed'] = 0 - result['allies_survived'] += 1 - for key in tmp.keys(): - result[key] = tmp[key] + # For filtering replays per submitter + result["protagonist"] = protagonist - else: - tmp_account_id = player['dbid'] - tmp_tank_id = player['vehicle_descr'] - tmp_battletime = result['battle_start_timestamp'] - if player['death_reason'] == -1: - survived = True - else: - survived = False - - if player['dbid'] in replay_summary['allies']: - result['allies'].add(get_stat_id(tmp_account_id, tmp_tank_id, tmp_battletime)) - if survived: - result['allies_survived'] += 1 - else: - result['enemies'].add(get_stat_id(tmp_account_id, tmp_tank_id, tmp_battletime)) - if survived: - result['enemies_survived'] += 1 + if account_id is None: + account_id = protagonist + elif protagonist != account_id: + if account_id in replay_summary["enemies"]: + # switch the teams... + if replay_summary["battle_result"] != 2: + if replay_summary["battle_result"] == 0: + replay_summary["battle_result"] = 1 + else: + replay_summary["battle_result"] = 0 + tmp = replay_summary["enemies"] + replay_summary["enemies"] = replay_summary["allies"] + replay_summary["allies"] = tmp + elif account_id in replay_summary["allies"]: + pass + else: + # looking for an account_id, but account_id not found in the teams => skipping the replay + return None + if url: + result["url"] = replay_json["data"]["view_url"] - result['battle_duration'] = btl_duration - ## Rather use 'player' than incomprehensible 'protagonist'... - result['player'] = get_stat_id(account_id, tank_id, result['battle_start_timestamp']) - bu.debug('Player stats_id: ' + result['player']) - # remove platoon buddy from stats - if result['squad_index'] != None: - for player in replay_summary['details']: - bu.debug(str(player)) - if (player['squad_index'] == result['squad_index']) and (player['dbid'] in replay_summary['allies']) and (player['dbid'] != account_id): - # platoon buddy found - tmp_account_id = player['dbid'] - tmp_tank_id = player['vehicle_descr'] - #tmp_tank_tier = str(wg.get_tank_tier(tmp_tank_id)) - tmp_battletime = result['battle_start_timestamp'] - # platoon buddy removed from stats - result['allies'].remove(get_stat_id(tmp_account_id, tmp_tank_id, tmp_battletime)) - break - for ratio in BattleCategory.get_result_fields_ratio(all=True): - result[ratio] = BattleCategory.calc_ratio(ratio, result) + for key in replay_summary_flds: + result[key] = replay_summary[key] - result['alive'] = min(result['time_alive'] / result['battle_duration'], 1) - result['battle_tier'] = btl_tier - result['top_tier'] = 1 if (result['tank_tier'] == btl_tier) else 0 - result['win'] = 1 if result['battle_result'] == 1 else 0 - REPLAY_I += 1 - result['battle_i'] = REPLAY_I - result['battle'] = str(REPLAY_I) + ': ' + replay_summary['vehicle'] + ' @' + replay_summary['map_name'] - bu.debug(str(result)) - return result - except KeyError as err: - bu.warning('Invalid replay. Key not found', err) - except Exception as err: - bu.error(exception=err) - return None + except Exception as err: + bu.error(exception=err) + return None + try: + # bu.debug('Part 2') + result["allies"] = set() + result["enemies"] = set() + result["allies_survived"] = 0 # for steamroller stats + result["enemies_survived"] = 0 # for steamroller stats + btl_duration = 0 + btl_tier = 0 + protagonist_tank = None + for player in replay_summary["details"]: + tank_id : int = player["vehicle_descr"] + btl_duration = max(btl_duration, player["time_alive"]) + player_tank_tier = wg.get_tank_tier(tank_id) + btl_tier = max(btl_tier, player_tank_tier) + + if player["dbid"] == account_id: + # player itself is not put in results['allies'] + tmp = dict() + tmp["account_id"] = account_id + tmp["tank_id"] = tank_id + tmp["tank_tier"] = player_tank_tier + tmp["tank_name"] = wg.get_tank_name(tank_id) + tmp["tank_type"] = wg.get_tank_type(tank_id) + tmp["tank_nation"] = wg.get_tank_nation(tank_id) + tmp["is_premium"] = int(wg.is_premium(tank_id)) + platoon_ndx = player["squad_index"] + tmp["squad_index"] = platoon_ndx + tmp["in_platoon"] = 0 if platoon_ndx is None else 1 + for key in replay_details_flds: + tmp[key] = player[key] + if player["hitpoints_left"] == 0: + tmp["survived"] = 0 + tmp["destroyed"] = 1 + else: + tmp["survived"] = 1 + tmp["destroyed"] = 0 + result["allies_survived"] += 1 + for key in tmp.keys(): + result[key] = tmp[key] + + else: + tmp_account_id = player["dbid"] + tmp_tank_id = player["vehicle_descr"] + tmp_battletime = result["battle_start_timestamp"] + if player["death_reason"] == -1: + survived = True + else: + survived = False + + if player["dbid"] in replay_summary["allies"]: + result["allies"].add( + get_stat_id(tmp_account_id, tmp_tank_id, tmp_battletime) + ) + if survived: + result["allies_survived"] += 1 + else: + result["enemies"].add( + get_stat_id(tmp_account_id, tmp_tank_id, tmp_battletime) + ) + if survived: + result["enemies_survived"] += 1 + + result["battle_duration"] = btl_duration + ## Rather use 'player' than incomprehensible 'protagonist'... + result["player"] = get_stat_id( + account_id, tank_id, result["battle_start_timestamp"] + ) + bu.debug("Player stats_id: " + result["player"]) + # remove platoon buddy from stats + if result["squad_index"] is not None: + for player in replay_summary["details"]: + bu.debug(str(player)) + if ( + (player["squad_index"] == result["squad_index"]) + and (player["dbid"] in replay_summary["allies"]) + and (player["dbid"] != account_id) + ): + # platoon buddy found + tmp_account_id = player["dbid"] + tmp_tank_id = player["vehicle_descr"] + # tmp_tank_tier = str(wg.get_tank_tier(tmp_tank_id)) + tmp_battletime = result["battle_start_timestamp"] + # platoon buddy removed from stats + result["allies"].remove( + get_stat_id(tmp_account_id, tmp_tank_id, tmp_battletime) + ) + break + for ratio in BattleCategory.get_result_fields_ratio(all=True): + result[ratio] = BattleCategory.calc_ratio(ratio, result) + + result["alive"] = min(result["time_alive"] / result["battle_duration"], 1) + result["battle_tier"] = btl_tier + result["top_tier"] = 1 if (result["tank_tier"] == btl_tier) else 0 + result["win"] = 1 if result["battle_result"] == 1 else 0 + REPLAY_I += 1 + result["battle_i"] = REPLAY_I + result["battle"] = ( + str(REPLAY_I) + + ": " + + replay_summary["vehicle"] + + " @" + + replay_summary["map_name"] + ) + bu.debug(str(result)) + return result + except KeyError as err: + bu.warning("Invalid replay. Key not found", err) + except Exception as err: + bu.error(exception=err) + return None def str2ints(stat_id_str: str) -> list: - """Convert stat_id string e.g. 'account_id:tank_id' to list of ints""" - return [ int(x) for x in stat_id_str.split(':') ] + """Convert stat_id string e.g. 'account_id:tank_id' to list of ints""" + return [int(x) for x in stat_id_str.split(":")] -def prune_stat_id(stat_id_str: str) -> str: - stat_id = stat_id_str.split(':') - del stat_id[-1] - return ':'.join(stat_id) +def prune_stat_id(stat_id_str: str) -> str: + stat_id = stat_id_str.split(":") + del stat_id[-1] + return ":".join(stat_id) def get_stat_id(account_id: int, tank_id: int, battle_time: int) -> str: - return ':'.join(map(str, [account_id, tank_id, battle_time ])) + return ":".join(map(str, [account_id, tank_id, battle_time])) def get_stat_id_tank_tier(stat_id_str: str) -> str: - """Return stat_id = account_id:tank_tier:battletime""" - try: - stat_id = str2ints(stat_id_str) - account_id = stat_id[0] - tank_tier = wg.get_tank_tier(stat_id[1]) - battle_time = (stat_id[2] // BATTLE_TIME_BUCKET) * BATTLE_TIME_BUCKET - return get_stat_id(account_id, tank_tier, battle_time) - except Exception as err: - bu.error('Stats_id: ' + stat_id_str, exception=err) - return None + """Return stat_id = account_id:tank_tier:battletime""" + global wg + try: + stat_id = str2ints(stat_id_str) + account_id = stat_id[0] + tank_tier = wg.get_tank_tier(stat_id[1]) + battle_time = (stat_id[2] // BATTLE_TIME_BUCKET) * BATTLE_TIME_BUCKET + return get_stat_id(account_id, tank_tier, battle_time) + except Exception as err: + bu.error("Stats_id: " + stat_id_str, exception=err) + return None def get_stat_id_tank(stat_id_str: str) -> str: - """Return stat_id = account_id:tank_id:battletime""" - try: - stat_id = str2ints(stat_id_str) - account_id = stat_id[0] - tank_id = stat_id[1] - battle_time = (stat_id[2] // BATTLE_TIME_BUCKET) * BATTLE_TIME_BUCKET - return get_stat_id(account_id, tank_id, battle_time) - except Exception as err: - bu.error('Stats_id: ' + stat_id_str, exception=err) - return None + """Return stat_id = account_id:tank_id:battletime""" + try: + stat_id = str2ints(stat_id_str) + account_id = stat_id[0] + tank_id = stat_id[1] + battle_time = (stat_id[2] // BATTLE_TIME_BUCKET) * BATTLE_TIME_BUCKET + return get_stat_id(account_id, tank_id, battle_time) + except Exception as err: + bu.error("Stats_id: " + stat_id_str, exception=err) + return None + def get_stat_id_player(stat_id_str: str) -> str: - """Return stat_id = account_id:battletime""" - try: - stat_id = str2ints(stat_id_str) - battle_time = (stat_id[2] // BATTLE_TIME_BUCKET) * BATTLE_TIME_BUCKET - return ':'.join(map(str, [ stat_id[0], battle_time ])) - except Exception as err: - bu.error('Stats_id: ' + stat_id_str, exception=err) - return None + """Return stat_id = account_id:battletime""" + try: + stat_id = str2ints(stat_id_str) + battle_time = (stat_id[2] // BATTLE_TIME_BUCKET) * BATTLE_TIME_BUCKET + return ":".join(map(str, [stat_id[0], battle_time])) + except Exception as err: + bu.error("Stats_id: " + stat_id_str, exception=err) + return None def get_account_id_f_stat_id(stat_id_str: str) -> int: - """get account_id from a stat_id""" - try: - stat_id = str2ints(stat_id_str) - return stat_id[0] - except Exception as err: - bu.error('Stats_id: ' + stat_id_str, exception=err) - return None + """get account_id from a stat_id""" + try: + stat_id = str2ints(stat_id_str) + return stat_id[0] + except Exception as err: + bu.error("Stats_id: " + stat_id_str, exception=err) + return None ### main() if __name__ == "__main__": - # To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp - if sys.platform.startswith("win") and sys.version_info >= (3, 8): - try: - from asyncio import WindowsSelectorEventLoopPolicy - except ImportError: - pass - else: - if not isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy): - asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) - asyncio.run(main(sys.argv[1:]), debug=False) + # To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp + if sys.platform.startswith("win") and sys.version_info >= (3, 8): + try: + from asyncio import WindowsSelectorEventLoopPolicy + except ImportError: + pass + else: + if not isinstance( + asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy + ): + asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) + asyncio.run(main(sys.argv[1:]), debug=False) From 555aa1ae3eb64f41d99f05dcad0750377db19c70 Mon Sep 17 00:00:00 2001 From: jylpah Date: Sun, 10 Sep 2023 20:01:07 +0300 Subject: [PATCH 4/5] Update 10.2, new tankopedia format --- tanks.json | 7380 +--------------------------------------------------- 1 file changed, 1 insertion(+), 7379 deletions(-) diff --git a/tanks.json b/tanks.json index 1964f1d..dde8f60 100644 --- a/tanks.json +++ b/tanks.json @@ -1,7379 +1 @@ -{ - "status": "ok", - "meta": { - "count": 592 - }, - "data": { - "1": { - "is_premium": false, - "name": "T-34", - "nation": "ussr", - "tank_id": 1, - "tier": 5, - "type": "mediumTank" - }, - "17": { - "is_premium": false, - "name": "Pz.Kpfw. IV Ausf. G", - "nation": "germany", - "tank_id": 17, - "tier": 5, - "type": "mediumTank" - }, - "33": { - "is_premium": true, - "name": "T14", - "nation": "usa", - "tank_id": 33, - "tier": 5, - "type": "heavyTank" - }, - "49": { - "is_premium": true, - "name": "Type 59", - "nation": "china", - "tank_id": 49, - "tier": 8, - "type": "mediumTank" - }, - "81": { - "is_premium": false, - "name": "Vickers Medium Mk. I", - "nation": "uk", - "tank_id": 81, - "tier": 1, - "type": "mediumTank" - }, - "113": { - "is_premium": true, - "name": "Vindicator Ultramarines", - "nation": "other", - "tank_id": 113, - "tier": 7, - "type": "AT-SPG" - }, - "257": { - "is_premium": false, - "name": "SU-85", - "nation": "ussr", - "tank_id": 257, - "tier": 5, - "type": "AT-SPG" - }, - "289": { - "is_premium": false, - "name": "M3 Stuart", - "nation": "usa", - "tank_id": 289, - "tier": 2, - "type": "lightTank" - }, - "321": { - "is_premium": false, - "name": "D2", - "nation": "france", - "tank_id": 321, - "tier": 3, - "type": "heavyTank" - }, - "337": { - "is_premium": true, - "name": "Vickers Medium Mk. II", - "nation": "uk", - "tank_id": 337, - "tier": 2, - "type": "mediumTank" - }, - "353": { - "is_premium": false, - "name": "Chi-Ni", - "nation": "japan", - "tank_id": 353, - "tier": 2, - "type": "mediumTank" - }, - "385": { - "is_premium": false, - "name": "Progetto M40 mod. 65", - "nation": "european", - "tank_id": 385, - "tier": 10, - "type": "mediumTank" - }, - "513": { - "is_premium": false, - "name": "IS", - "nation": "ussr", - "tank_id": 513, - "tier": 7, - "type": "heavyTank" - }, - "529": { - "is_premium": false, - "name": "Tiger I", - "nation": "germany", - "tank_id": 529, - "tier": 7, - "type": "heavyTank" - }, - "545": { - "is_premium": false, - "name": "T1 Cunningham", - "nation": "usa", - "tank_id": 545, - "tier": 1, - "type": "lightTank" - }, - "577": { - "is_premium": false, - "name": "Renault FT", - "nation": "france", - "tank_id": 577, - "tier": 1, - "type": "lightTank" - }, - "593": { - "is_premium": false, - "name": "Sherman Firefly", - "nation": "uk", - "tank_id": 593, - "tier": 6, - "type": "mediumTank" - }, - "609": { - "is_premium": false, - "name": "Renault Otsu", - "nation": "japan", - "tank_id": 609, - "tier": 1, - "type": "lightTank" - }, - "625": { - "is_premium": true, - "name": "Stridsvagn 74A2", - "nation": "other", - "tank_id": 625, - "tier": 6, - "type": "mediumTank" - }, - "641": { - "is_premium": false, - "name": "Prototipo Standard B", - "nation": "european", - "tank_id": 641, - "tier": 9, - "type": "mediumTank" - }, - "769": { - "is_premium": false, - "name": "BT-7", - "nation": "ussr", - "tank_id": 769, - "tier": 3, - "type": "lightTank" - }, - "785": { - "is_premium": false, - "name": "Pz.Kpfw. 35 (t)", - "nation": "germany", - "tank_id": 785, - "tier": 2, - "type": "lightTank" - }, - "801": { - "is_premium": false, - "name": "M6", - "nation": "usa", - "tank_id": 801, - "tier": 6, - "type": "heavyTank" - }, - "817": { - "is_premium": true, - "name": "WZ-111", - "nation": "china", - "tank_id": 817, - "tier": 8, - "type": "heavyTank" - }, - "849": { - "is_premium": false, - "name": "Matilda", - "nation": "uk", - "tank_id": 849, - "tier": 4, - "type": "heavyTank" - }, - "865": { - "is_premium": false, - "name": "Type 95 Ha-Go", - "nation": "japan", - "tank_id": 865, - "tier": 1, - "type": "lightTank" - }, - "881": { - "is_premium": true, - "name": "Edelweiss", - "nation": "other", - "tank_id": 881, - "tier": 7, - "type": "mediumTank" - }, - "897": { - "is_premium": false, - "name": "P.44 Pantera", - "nation": "european", - "tank_id": 897, - "tier": 8, - "type": "mediumTank" - }, - "1025": { - "is_premium": false, - "name": "BT-2", - "nation": "ussr", - "tank_id": 1025, - "tier": 2, - "type": "lightTank" - }, - "1041": { - "is_premium": false, - "name": "StuG III Ausf. G", - "nation": "germany", - "tank_id": 1041, - "tier": 5, - "type": "AT-SPG" - }, - "1057": { - "is_premium": false, - "name": "M4 Sherman", - "nation": "usa", - "tank_id": 1057, - "tier": 5, - "type": "mediumTank" - }, - "1073": { - "is_premium": false, - "name": "T-34-1", - "nation": "china", - "tank_id": 1073, - "tier": 7, - "type": "mediumTank" - }, - "1089": { - "is_premium": false, - "name": "B1", - "nation": "france", - "tank_id": 1089, - "tier": 4, - "type": "heavyTank" - }, - "1105": { - "is_premium": false, - "name": "Cromwell", - "nation": "uk", - "tank_id": 1105, - "tier": 6, - "type": "mediumTank" - }, - "1121": { - "is_premium": false, - "name": "Type 5 Chi-Ri", - "nation": "japan", - "tank_id": 1121, - "tier": 7, - "type": "mediumTank" - }, - "1137": { - "is_premium": true, - "name": "Predator Ultramarines", - "nation": "other", - "tank_id": 1137, - "tier": 7, - "type": "heavyTank" - }, - "1153": { - "is_premium": false, - "name": "P.43 ter", - "nation": "european", - "tank_id": 1153, - "tier": 7, - "type": "mediumTank" - }, - "1297": { - "is_premium": false, - "name": "Panther I", - "nation": "germany", - "tank_id": 1297, - "tier": 7, - "type": "mediumTank" - }, - "1313": { - "is_premium": false, - "name": "M4A3E8 Sherman", - "nation": "usa", - "tank_id": 1313, - "tier": 6, - "type": "mediumTank" - }, - "1329": { - "is_premium": false, - "name": "Renault NC-31", - "nation": "china", - "tank_id": 1329, - "tier": 1, - "type": "lightTank" - }, - "1361": { - "is_premium": true, - "name": "Churchill Mk. VI", - "nation": "uk", - "tank_id": 1361, - "tier": 6, - "type": "heavyTank" - }, - "1377": { - "is_premium": false, - "name": "Type 3 Chi-Nu", - "nation": "japan", - "tank_id": 1377, - "tier": 5, - "type": "mediumTank" - }, - "1393": { - "is_premium": true, - "name": "Nameless", - "nation": "other", - "tank_id": 1393, - "tier": 7, - "type": "heavyTank" - }, - "1409": { - "is_premium": false, - "name": "P.43 bis", - "nation": "european", - "tank_id": 1409, - "tier": 6, - "type": "mediumTank" - }, - "1537": { - "is_premium": true, - "name": "T-28 mod. 1940", - "nation": "ussr", - "tank_id": 1537, - "tier": 4, - "type": "mediumTank" - }, - "1553": { - "is_premium": false, - "name": "Jagdpanzer IV", - "nation": "germany", - "tank_id": 1553, - "tier": 6, - "type": "AT-SPG" - }, - "1569": { - "is_premium": false, - "name": "T20", - "nation": "usa", - "tank_id": 1569, - "tier": 7, - "type": "mediumTank" - }, - "1585": { - "is_premium": false, - "name": "T-34-2", - "nation": "china", - "tank_id": 1585, - "tier": 8, - "type": "mediumTank" - }, - "1601": { - "is_premium": true, - "name": "D1", - "nation": "france", - "tank_id": 1601, - "tier": 2, - "type": "lightTank" - }, - "1617": { - "is_premium": true, - "name": "Sherman V", - "nation": "uk", - "tank_id": 1617, - "tier": 5, - "type": "mediumTank" - }, - "1633": { - "is_premium": false, - "name": "Type 1 Chi-He", - "nation": "japan", - "tank_id": 1633, - "tier": 4, - "type": "mediumTank" - }, - "1649": { - "is_premium": true, - "name": "Helsing", - "nation": "other", - "tank_id": 1649, - "tier": 7, - "type": "AT-SPG" - }, - "1665": { - "is_premium": false, - "name": "Lago", - "nation": "european", - "tank_id": 1665, - "tier": 4, - "type": "mediumTank" - }, - "1809": { - "is_premium": false, - "name": "Hetzer", - "nation": "germany", - "tank_id": 1809, - "tier": 4, - "type": "AT-SPG" - }, - "1825": { - "is_premium": false, - "name": "M2 Light Tank", - "nation": "usa", - "tank_id": 1825, - "tier": 1, - "type": "lightTank" - }, - "1841": { - "is_premium": false, - "name": "WZ-120", - "nation": "china", - "tank_id": 1841, - "tier": 9, - "type": "mediumTank" - }, - "1857": { - "is_premium": false, - "name": "Bat.-Châtillon 25 t AP", - "nation": "france", - "tank_id": 1857, - "tier": 9, - "type": "lightTank" - }, - "1889": { - "is_premium": false, - "name": "Type 4 Chi-To", - "nation": "japan", - "tank_id": 1889, - "tier": 6, - "type": "mediumTank" - }, - "1905": { - "is_premium": true, - "name": "O-47", - "nation": "other", - "tank_id": 1905, - "tier": 8, - "type": "mediumTank" - }, - "1921": { - "is_premium": false, - "name": "Strv m/42", - "nation": "european", - "tank_id": 1921, - "tier": 5, - "type": "mediumTank" - }, - "2049": { - "is_premium": false, - "name": "A-20", - "nation": "ussr", - "tank_id": 2049, - "tier": 4, - "type": "lightTank" - }, - "2065": { - "is_premium": false, - "name": "Pz.Kpfw. II", - "nation": "germany", - "tank_id": 2065, - "tier": 1, - "type": "lightTank" - }, - "2097": { - "is_premium": false, - "name": "WZ-111 model 1-4", - "nation": "china", - "tank_id": 2097, - "tier": 9, - "type": "heavyTank" - }, - "2129": { - "is_premium": false, - "name": "Crusader", - "nation": "uk", - "tank_id": 2129, - "tier": 5, - "type": "lightTank" - }, - "2145": { - "is_premium": false, - "name": "Type 97 Chi-Ha", - "nation": "japan", - "tank_id": 2145, - "tier": 3, - "type": "mediumTank" - }, - "2161": { - "is_premium": true, - "name": "WZ 135G FT Blaze", - "nation": "other", - "tank_id": 2161, - "tier": 7, - "type": "AT-SPG" - }, - "2177": { - "is_premium": false, - "name": "14TP", - "nation": "european", - "tank_id": 2177, - "tier": 3, - "type": "lightTank" - }, - "2305": { - "is_premium": false, - "name": "SU-152", - "nation": "ussr", - "tank_id": 2305, - "tier": 7, - "type": "AT-SPG" - }, - "2321": { - "is_premium": false, - "name": "VK 36.01 (H)", - "nation": "germany", - "tank_id": 2321, - "tier": 6, - "type": "heavyTank" - }, - "2353": { - "is_premium": false, - "name": "Vickers Mk. E Type B", - "nation": "china", - "tank_id": 2353, - "tier": 1, - "type": "lightTank" - }, - "2369": { - "is_premium": true, - "name": "FCM 36 Pak 40", - "nation": "france", - "tank_id": 2369, - "tier": 3, - "type": "AT-SPG" - }, - "2385": { - "is_premium": true, - "name": "Vickers Medium Mk. III", - "nation": "uk", - "tank_id": 2385, - "tier": 3, - "type": "mediumTank" - }, - "2401": { - "is_premium": true, - "name": "Type 98 Ke-Ni", - "nation": "japan", - "tank_id": 2401, - "tier": 3, - "type": "lightTank" - }, - "2433": { - "is_premium": false, - "name": "10TP", - "nation": "european", - "tank_id": 2433, - "tier": 2, - "type": "lightTank" - }, - "2561": { - "is_premium": false, - "name": "T-34-85", - "nation": "ussr", - "tank_id": 2561, - "tier": 6, - "type": "mediumTank" - }, - "2577": { - "is_premium": true, - "name": "VK 30.01 (H)", - "nation": "germany", - "tank_id": 2577, - "tier": 5, - "type": "heavyTank" - }, - "2593": { - "is_premium": false, - "name": "T30", - "nation": "usa", - "tank_id": 2593, - "tier": 9, - "type": "AT-SPG" - }, - "2609": { - "is_premium": true, - "name": "Type 64", - "nation": "china", - "tank_id": 2609, - "tier": 6, - "type": "lightTank" - }, - "2625": { - "is_premium": false, - "name": "ARL 44", - "nation": "france", - "tank_id": 2625, - "tier": 6, - "type": "heavyTank" - }, - "2657": { - "is_premium": false, - "name": "STA-1", - "nation": "japan", - "tank_id": 2657, - "tier": 8, - "type": "mediumTank" - }, - "2689": { - "is_premium": false, - "name": "Vickers Mk. F", - "nation": "european", - "tank_id": 2689, - "tier": 1, - "type": "lightTank" - }, - "2817": { - "is_premium": false, - "name": "KV-1S", - "nation": "ussr", - "tank_id": 2817, - "tier": 6, - "type": "heavyTank" - }, - "2849": { - "is_premium": true, - "name": "T34", - "nation": "usa", - "tank_id": 2849, - "tier": 8, - "type": "heavyTank" - }, - "2865": { - "is_premium": false, - "name": "WZ-110", - "nation": "china", - "tank_id": 2865, - "tier": 8, - "type": "heavyTank" - }, - "2881": { - "is_premium": true, - "name": "AMX 40", - "nation": "france", - "tank_id": 2881, - "tier": 4, - "type": "mediumTank" - }, - "2897": { - "is_premium": false, - "name": "Churchill I", - "nation": "uk", - "tank_id": 2897, - "tier": 5, - "type": "heavyTank" - }, - "2913": { - "is_premium": true, - "name": "Type 5 Ke-Ho", - "nation": "japan", - "tank_id": 2913, - "tier": 4, - "type": "lightTank" - }, - "2945": { - "is_premium": true, - "name": "Progetto M35 mod. 46", - "nation": "european", - "tank_id": 2945, - "tier": 8, - "type": "mediumTank" - }, - "3073": { - "is_premium": true, - "name": "T-46", - "nation": "ussr", - "tank_id": 3073, - "tier": 3, - "type": "lightTank" - }, - "3089": { - "is_premium": false, - "name": "Leichttraktor", - "nation": "germany", - "tank_id": 3089, - "tier": 1, - "type": "lightTank" - }, - "3105": { - "is_premium": true, - "name": "M3 Lee", - "nation": "usa", - "tank_id": 3105, - "tier": 4, - "type": "mediumTank" - }, - "3121": { - "is_premium": false, - "name": "M5A1 Stuart", - "nation": "china", - "tank_id": 3121, - "tier": 4, - "type": "lightTank" - }, - "3137": { - "is_premium": false, - "name": "AMX 50 100", - "nation": "france", - "tank_id": 3137, - "tier": 8, - "type": "heavyTank" - }, - "3153": { - "is_premium": false, - "name": "Black Prince", - "nation": "uk", - "tank_id": 3153, - "tier": 7, - "type": "heavyTank" - }, - "3201": { - "is_premium": true, - "name": "50TP prototyp", - "nation": "european", - "tank_id": 3201, - "tier": 9, - "type": "heavyTank" - }, - "3329": { - "is_premium": false, - "name": "MS-1 mod. 1", - "nation": "ussr", - "tank_id": 3329, - "tier": 1, - "type": "lightTank" - }, - "3345": { - "is_premium": true, - "name": "Pz.Kpfw. 38 (t)", - "nation": "germany", - "tank_id": 3345, - "tier": 3, - "type": "lightTank" - }, - "3361": { - "is_premium": false, - "name": "T1 Heavy Tank", - "nation": "usa", - "tank_id": 3361, - "tier": 5, - "type": "heavyTank" - }, - "3377": { - "is_premium": false, - "name": "WZ-131", - "nation": "china", - "tank_id": 3377, - "tier": 7, - "type": "lightTank" - }, - "3425": { - "is_premium": false, - "name": "Type 61", - "nation": "japan", - "tank_id": 3425, - "tier": 9, - "type": "mediumTank" - }, - "3457": { - "is_premium": false, - "name": "Emil I", - "nation": "european", - "tank_id": 3457, - "tier": 8, - "type": "heavyTank" - }, - "3585": { - "is_premium": false, - "name": "SU-100", - "nation": "ussr", - "tank_id": 3585, - "tier": 6, - "type": "AT-SPG" - }, - "3601": { - "is_premium": true, - "name": "Panzerjäger I", - "nation": "germany", - "tank_id": 3601, - "tier": 2, - "type": "AT-SPG" - }, - "3633": { - "is_premium": false, - "name": "IS-2", - "nation": "china", - "tank_id": 3633, - "tier": 7, - "type": "heavyTank" - }, - "3649": { - "is_premium": false, - "name": "Bat.-Châtillon 25 t", - "nation": "france", - "tank_id": 3649, - "tier": 10, - "type": "lightTank" - }, - "3681": { - "is_premium": false, - "name": "STB-1", - "nation": "japan", - "tank_id": 3681, - "tier": 10, - "type": "mediumTank" - }, - "3697": { - "is_premium": true, - "name": "Lupus", - "nation": "other", - "tank_id": 3697, - "tier": 7, - "type": "heavyTank" - }, - "3713": { - "is_premium": false, - "name": "Strv 74", - "nation": "european", - "tank_id": 3713, - "tier": 6, - "type": "mediumTank" - }, - "3857": { - "is_premium": false, - "name": "Jagdpanther", - "nation": "germany", - "tank_id": 3857, - "tier": 7, - "type": "AT-SPG" - }, - "3873": { - "is_premium": false, - "name": "T29", - "nation": "usa", - "tank_id": 3873, - "tier": 7, - "type": "heavyTank" - }, - "3889": { - "is_premium": false, - "name": "WZ-132", - "nation": "china", - "tank_id": 3889, - "tier": 8, - "type": "lightTank" - }, - "3905": { - "is_premium": false, - "name": "AMX 50 120", - "nation": "france", - "tank_id": 3905, - "tier": 9, - "type": "heavyTank" - }, - "3921": { - "is_premium": false, - "name": "Caernarvon", - "nation": "uk", - "tank_id": 3921, - "tier": 8, - "type": "heavyTank" - }, - "3937": { - "is_premium": false, - "name": "Ho-Ri Type III", - "nation": "japan", - "tank_id": 3937, - "tier": 10, - "type": "AT-SPG" - }, - "3953": { - "is_premium": true, - "name": "T 55A", - "nation": "other", - "tank_id": 3953, - "tier": 9, - "type": "mediumTank" - }, - "3969": { - "is_premium": false, - "name": "Leo", - "nation": "european", - "tank_id": 3969, - "tier": 7, - "type": "mediumTank" - }, - "4113": { - "is_premium": false, - "name": "VK 30.02 (D)", - "nation": "germany", - "tank_id": 4113, - "tier": 7, - "type": "mediumTank" - }, - "4145": { - "is_premium": false, - "name": "WZ-121", - "nation": "china", - "tank_id": 4145, - "tier": 10, - "type": "mediumTank" - }, - "4193": { - "is_premium": false, - "name": "Ho-Ri Type II", - "nation": "japan", - "tank_id": 4193, - "tier": 9, - "type": "AT-SPG" - }, - "4209": { - "is_premium": false, - "name": "WarDuck", - "nation": "other", - "tank_id": 4209, - "tier": 1, - "type": "mediumTank" - }, - "4225": { - "is_premium": false, - "name": "Emil II", - "nation": "european", - "tank_id": 4225, - "tier": 9, - "type": "heavyTank" - }, - "4353": { - "is_premium": false, - "name": "T-44", - "nation": "ussr", - "tank_id": 4353, - "tier": 8, - "type": "mediumTank" - }, - "4369": { - "is_premium": false, - "name": "Pz.Kpfw. III", - "nation": "germany", - "tank_id": 4369, - "tier": 3, - "type": "mediumTank" - }, - "4385": { - "is_premium": false, - "name": "T32", - "nation": "usa", - "tank_id": 4385, - "tier": 8, - "type": "heavyTank" - }, - "4401": { - "is_premium": false, - "name": "Type 2597 Chi-Ha", - "nation": "china", - "tank_id": 4401, - "tier": 3, - "type": "mediumTank" - }, - "4417": { - "is_premium": true, - "name": "AMX M4 mle. 54", - "nation": "france", - "tank_id": 4417, - "tier": 10, - "type": "heavyTank" - }, - "4433": { - "is_premium": false, - "name": "Conqueror", - "nation": "uk", - "tank_id": 4433, - "tier": 9, - "type": "heavyTank" - }, - "4449": { - "is_premium": true, - "name": "IS-2 Pravda SP", - "nation": "japan", - "tank_id": 4449, - "tier": 7, - "type": "heavyTank" - }, - "4465": { - "is_premium": true, - "name": "Hafen", - "nation": "other", - "tank_id": 4465, - "tier": 7, - "type": "mediumTank" - }, - "4481": { - "is_premium": false, - "name": "Kranvagn", - "nation": "european", - "tank_id": 4481, - "tier": 10, - "type": "heavyTank" - }, - "4609": { - "is_premium": false, - "name": "T-26", - "nation": "ussr", - "tank_id": 4609, - "tier": 1, - "type": "lightTank" - }, - "4657": { - "is_premium": false, - "name": "Type T-34", - "nation": "china", - "tank_id": 4657, - "tier": 5, - "type": "mediumTank" - }, - "4689": { - "is_premium": false, - "name": "Churchill VII", - "nation": "uk", - "tank_id": 4689, - "tier": 6, - "type": "heavyTank" - }, - "4705": { - "is_premium": true, - "name": "Firefly Saunders SP", - "nation": "japan", - "tank_id": 4705, - "tier": 6, - "type": "mediumTank" - }, - "4721": { - "is_premium": true, - "name": "Gravedigger", - "nation": "other", - "tank_id": 4721, - "tier": 7, - "type": "heavyTank" - }, - "4737": { - "is_premium": true, - "name": "EMIL 1951", - "nation": "european", - "tank_id": 4737, - "tier": 8, - "type": "heavyTank" - }, - "4881": { - "is_premium": true, - "name": "Pz.Kpfw. III Ausf. A", - "nation": "germany", - "tank_id": 4881, - "tier": 3, - "type": "lightTank" - }, - "4897": { - "is_premium": true, - "name": "M2 Medium Tank", - "nation": "usa", - "tank_id": 4897, - "tier": 3, - "type": "mediumTank" - }, - "4913": { - "is_premium": false, - "name": "59-16", - "nation": "china", - "tank_id": 4913, - "tier": 6, - "type": "lightTank" - }, - "4929": { - "is_premium": false, - "name": "AMX 13 90", - "nation": "france", - "tank_id": 4929, - "tier": 8, - "type": "lightTank" - }, - "4945": { - "is_premium": true, - "name": "Valentine Mk. IX", - "nation": "uk", - "tank_id": 4945, - "tier": 4, - "type": "lightTank" - }, - "4961": { - "is_premium": false, - "name": "Ho-Ri Type I", - "nation": "japan", - "tank_id": 4961, - "tier": 8, - "type": "AT-SPG" - }, - "4977": { - "is_premium": true, - "name": "Scavenger", - "nation": "other", - "tank_id": 4977, - "tier": 5, - "type": "mediumTank" - }, - "4993": { - "is_premium": true, - "name": "P.43/06 anniversario", - "nation": "european", - "tank_id": 4993, - "tier": 6, - "type": "mediumTank" - }, - "5121": { - "is_premium": true, - "name": "AT-1", - "nation": "ussr", - "tank_id": 5121, - "tier": 2, - "type": "AT-SPG" - }, - "5137": { - "is_premium": false, - "name": "Tiger II", - "nation": "germany", - "tank_id": 5137, - "tier": 8, - "type": "heavyTank" - }, - "5153": { - "is_premium": false, - "name": "M5 Stuart", - "nation": "usa", - "tank_id": 5153, - "tier": 3, - "type": "lightTank" - }, - "5169": { - "is_premium": false, - "name": "Type 58", - "nation": "china", - "tank_id": 5169, - "tier": 6, - "type": "mediumTank" - }, - "5185": { - "is_premium": false, - "name": "AMX 13 75", - "nation": "france", - "tank_id": 5185, - "tier": 7, - "type": "lightTank" - }, - "5201": { - "is_premium": true, - "name": "Cruiser Mk. I", - "nation": "uk", - "tank_id": 5201, - "tier": 2, - "type": "lightTank" - }, - "5217": { - "is_premium": false, - "name": "Chi-To SPG", - "nation": "japan", - "tank_id": 5217, - "tier": 7, - "type": "AT-SPG" - }, - "5233": { - "is_premium": true, - "name": "Smasher", - "nation": "other", - "tank_id": 5233, - "tier": 7, - "type": "heavyTank" - }, - "5249": { - "is_premium": true, - "name": "Pudel", - "nation": "european", - "tank_id": 5249, - "tier": 6, - "type": "mediumTank" - }, - "5377": { - "is_premium": false, - "name": "IS-3", - "nation": "ussr", - "tank_id": 5377, - "tier": 8, - "type": "heavyTank" - }, - "5393": { - "is_premium": false, - "name": "VK 16.02 Leopard", - "nation": "germany", - "tank_id": 5393, - "tier": 5, - "type": "lightTank" - }, - "5409": { - "is_premium": false, - "name": "M7", - "nation": "usa", - "tank_id": 5409, - "tier": 4, - "type": "mediumTank" - }, - "5425": { - "is_premium": false, - "name": "WZ-113", - "nation": "china", - "tank_id": 5425, - "tier": 10, - "type": "heavyTank" - }, - "5441": { - "is_premium": true, - "name": "AMX 30 1er prototype", - "nation": "france", - "tank_id": 5441, - "tier": 9, - "type": "mediumTank" - }, - "5457": { - "is_premium": false, - "name": "Comet", - "nation": "uk", - "tank_id": 5457, - "tier": 7, - "type": "mediumTank" - }, - "5473": { - "is_premium": false, - "name": "Mitsu 108", - "nation": "japan", - "tank_id": 5473, - "tier": 5, - "type": "heavyTank" - }, - "5489": { - "is_premium": true, - "name": "Y5 T-34", - "nation": "other", - "tank_id": 5489, - "tier": 5, - "type": "mediumTank" - }, - "5505": { - "is_premium": false, - "name": "TVP T 50/51", - "nation": "european", - "tank_id": 5505, - "tier": 10, - "type": "mediumTank" - }, - "5665": { - "is_premium": true, - "name": "T2 Medium Tank", - "nation": "usa", - "tank_id": 5665, - "tier": 2, - "type": "mediumTank" - }, - "5681": { - "is_premium": true, - "name": "121B", - "nation": "china", - "tank_id": 5681, - "tier": 10, - "type": "mediumTank" - }, - "5713": { - "is_premium": false, - "name": "Centurion Mk. 7/1", - "nation": "uk", - "tank_id": 5713, - "tier": 9, - "type": "mediumTank" - }, - "5729": { - "is_premium": false, - "name": "Ju-Nu", - "nation": "japan", - "tank_id": 5729, - "tier": 6, - "type": "heavyTank" - }, - "5745": { - "is_premium": true, - "name": "Y5 Firefly", - "nation": "other", - "tank_id": 5745, - "tier": 6, - "type": "mediumTank" - }, - "5761": { - "is_premium": false, - "name": "Škoda T 50", - "nation": "european", - "tank_id": 5761, - "tier": 9, - "type": "mediumTank" - }, - "5889": { - "is_premium": false, - "name": "KV-3", - "nation": "ussr", - "tank_id": 5889, - "tier": 7, - "type": "heavyTank" - }, - "5921": { - "is_premium": false, - "name": "M26 Pershing", - "nation": "usa", - "tank_id": 5921, - "tier": 8, - "type": "mediumTank" - }, - "5937": { - "is_premium": true, - "name": "59-Patton", - "nation": "china", - "tank_id": 5937, - "tier": 8, - "type": "mediumTank" - }, - "5953": { - "is_premium": false, - "name": "AMX 38", - "nation": "france", - "tank_id": 5953, - "tier": 2, - "type": "mediumTank" - }, - "5969": { - "is_premium": false, - "name": "Centurion Mk. I", - "nation": "uk", - "tank_id": 5969, - "tier": 8, - "type": "mediumTank" - }, - "5985": { - "is_premium": false, - "name": "Ju-To", - "nation": "japan", - "tank_id": 5985, - "tier": 7, - "type": "heavyTank" - }, - "6001": { - "is_premium": true, - "name": "Y5 ELC bis", - "nation": "other", - "tank_id": 6001, - "tier": 7, - "type": "mediumTank" - }, - "6017": { - "is_premium": false, - "name": "TVP VTU Koncept", - "nation": "european", - "tank_id": 6017, - "tier": 8, - "type": "mediumTank" - }, - "6145": { - "is_premium": false, - "name": "IS-4", - "nation": "ussr", - "tank_id": 6145, - "tier": 10, - "type": "heavyTank" - }, - "6161": { - "is_premium": true, - "name": "Pz.Kpfw. II Luchs", - "nation": "germany", - "tank_id": 6161, - "tier": 4, - "type": "lightTank" - }, - "6177": { - "is_premium": true, - "name": "T18", - "nation": "usa", - "tank_id": 6177, - "tier": 2, - "type": "AT-SPG" - }, - "6193": { - "is_premium": true, - "name": "T-34-3", - "nation": "china", - "tank_id": 6193, - "tier": 8, - "type": "mediumTank" - }, - "6209": { - "is_premium": false, - "name": "AMX 50 B", - "nation": "france", - "tank_id": 6209, - "tier": 10, - "type": "heavyTank" - }, - "6225": { - "is_premium": false, - "name": "FV215b", - "nation": "uk", - "tank_id": 6225, - "tier": 10, - "type": "heavyTank" - }, - "6241": { - "is_premium": false, - "name": "Chi-Se", - "nation": "japan", - "tank_id": 6241, - "tier": 8, - "type": "heavyTank" - }, - "6257": { - "is_premium": true, - "name": "M4/FL10", - "nation": "other", - "tank_id": 6257, - "tier": 7, - "type": "mediumTank" - }, - "6273": { - "is_premium": false, - "name": "Konštrukta T-34/100", - "nation": "european", - "tank_id": 6273, - "tier": 7, - "type": "mediumTank" - }, - "6401": { - "is_premium": true, - "name": "SU-76", - "nation": "ussr", - "tank_id": 6401, - "tier": 3, - "type": "AT-SPG" - }, - "6417": { - "is_premium": true, - "name": "Pz.Kpfw. III/IV", - "nation": "germany", - "tank_id": 6417, - "tier": 5, - "type": "mediumTank" - }, - "6433": { - "is_premium": true, - "name": "T82", - "nation": "usa", - "tank_id": 6433, - "tier": 3, - "type": "AT-SPG" - }, - "6449": { - "is_premium": false, - "name": "WZ-113G FT", - "nation": "china", - "tank_id": 6449, - "tier": 10, - "type": "AT-SPG" - }, - "6465": { - "is_premium": false, - "name": "AMX 12 t", - "nation": "france", - "tank_id": 6465, - "tier": 6, - "type": "lightTank" - }, - "6481": { - "is_premium": true, - "name": "Covenanter", - "nation": "uk", - "tank_id": 6481, - "tier": 4, - "type": "lightTank" - }, - "6497": { - "is_premium": false, - "name": "Type 68", - "nation": "japan", - "tank_id": 6497, - "tier": 9, - "type": "heavyTank" - }, - "6529": { - "is_premium": false, - "name": "Škoda T 25", - "nation": "european", - "tank_id": 6529, - "tier": 6, - "type": "mediumTank" - }, - "6657": { - "is_premium": false, - "name": "T-43", - "nation": "ussr", - "tank_id": 6657, - "tier": 7, - "type": "mediumTank" - }, - "6673": { - "is_premium": true, - "name": "Marder II", - "nation": "germany", - "tank_id": 6673, - "tier": 3, - "type": "AT-SPG" - }, - "6689": { - "is_premium": true, - "name": "T49 ATM", - "nation": "usa", - "tank_id": 6689, - "tier": 7, - "type": "lightTank" - }, - "6705": { - "is_premium": false, - "name": "LT vz. 38", - "nation": "china", - "tank_id": 6705, - "tier": 2, - "type": "lightTank" - }, - "6721": { - "is_premium": false, - "name": "BDR G1 B", - "nation": "france", - "tank_id": 6721, - "tier": 5, - "type": "heavyTank" - }, - "6753": { - "is_premium": false, - "name": "Type 71", - "nation": "japan", - "tank_id": 6753, - "tier": 10, - "type": "heavyTank" - }, - "6785": { - "is_premium": true, - "name": "Škoda T 27", - "nation": "european", - "tank_id": 6785, - "tier": 8, - "type": "mediumTank" - }, - "6913": { - "is_premium": false, - "name": "SU-85B", - "nation": "ussr", - "tank_id": 6913, - "tier": 4, - "type": "AT-SPG" - }, - "6929": { - "is_premium": false, - "name": "Maus", - "nation": "germany", - "tank_id": 6929, - "tier": 10, - "type": "heavyTank" - }, - "6945": { - "is_premium": false, - "name": "M10 Wolverine", - "nation": "usa", - "tank_id": 6945, - "tier": 5, - "type": "AT-SPG" - }, - "6961": { - "is_premium": true, - "name": "WZ-120-1G FT", - "nation": "china", - "tank_id": 6961, - "tier": 8, - "type": "AT-SPG" - }, - "6977": { - "is_premium": false, - "name": "AMX M4 mle. 45", - "nation": "france", - "tank_id": 6977, - "tier": 7, - "type": "heavyTank" - }, - "6993": { - "is_premium": false, - "name": "Cruiser Mk. II", - "nation": "uk", - "tank_id": 6993, - "tier": 1, - "type": "lightTank" - }, - "7009": { - "is_premium": true, - "name": "Type 57", - "nation": "japan", - "tank_id": 7009, - "tier": 8, - "type": "heavyTank" - }, - "7025": { - "is_premium": true, - "name": "Vulcan", - "nation": "other", - "tank_id": 7025, - "tier": 7, - "type": "heavyTank" - }, - "7041": { - "is_premium": true, - "name": "Turbo", - "nation": "european", - "tank_id": 7041, - "tier": 6, - "type": "mediumTank" - }, - "7169": { - "is_premium": false, - "name": "IS-7", - "nation": "ussr", - "tank_id": 7169, - "tier": 10, - "type": "heavyTank" - }, - "7185": { - "is_premium": false, - "name": "VK 30.01 (P)", - "nation": "germany", - "tank_id": 7185, - "tier": 6, - "type": "mediumTank" - }, - "7201": { - "is_premium": false, - "name": "M36 Jackson", - "nation": "usa", - "tank_id": 7201, - "tier": 6, - "type": "AT-SPG" - }, - "7217": { - "is_premium": true, - "name": "WZ-112-2", - "nation": "china", - "tank_id": 7217, - "tier": 8, - "type": "heavyTank" - }, - "7249": { - "is_premium": false, - "name": "FV4202", - "nation": "uk", - "tank_id": 7249, - "tier": 10, - "type": "mediumTank" - }, - "7281": { - "is_premium": true, - "name": "Lycan", - "nation": "other", - "tank_id": 7281, - "tier": 7, - "type": "heavyTank" - }, - "7297": { - "is_premium": false, - "name": "60TP Lewandowskiego", - "nation": "european", - "tank_id": 7297, - "tier": 10, - "type": "heavyTank" - }, - "7425": { - "is_premium": false, - "name": "ISU-152", - "nation": "ussr", - "tank_id": 7425, - "tier": 8, - "type": "AT-SPG" - }, - "7441": { - "is_premium": false, - "name": "VK 45.02 (P) Ausf. B", - "nation": "germany", - "tank_id": 7441, - "tier": 9, - "type": "heavyTank" - }, - "7473": { - "is_premium": false, - "name": "T-34-2G FT", - "nation": "china", - "tank_id": 7473, - "tier": 7, - "type": "AT-SPG" - }, - "7505": { - "is_premium": false, - "name": "Cruiser Mk. IV", - "nation": "uk", - "tank_id": 7505, - "tier": 3, - "type": "mediumTank" - }, - "7537": { - "is_premium": true, - "name": "Nightmare", - "nation": "other", - "tank_id": 7537, - "tier": 5, - "type": "AT-SPG" - }, - "7553": { - "is_premium": false, - "name": "50TP Tyszkiewicza", - "nation": "european", - "tank_id": 7553, - "tier": 9, - "type": "heavyTank" - }, - "7697": { - "is_premium": false, - "name": "Ferdinand", - "nation": "germany", - "tank_id": 7697, - "tier": 8, - "type": "AT-SPG" - }, - "7713": { - "is_premium": true, - "name": "T40", - "nation": "usa", - "tank_id": 7713, - "tier": 4, - "type": "AT-SPG" - }, - "7729": { - "is_premium": false, - "name": "WZ-131G FT", - "nation": "china", - "tank_id": 7729, - "tier": 6, - "type": "AT-SPG" - }, - "7745": { - "is_premium": true, - "name": "Renault FT AC", - "nation": "france", - "tank_id": 7745, - "tier": 2, - "type": "AT-SPG" - }, - "7761": { - "is_premium": false, - "name": "Cruiser Mk. III", - "nation": "uk", - "tank_id": 7761, - "tier": 2, - "type": "mediumTank" - }, - "7793": { - "is_premium": true, - "name": "Annihilator", - "nation": "other", - "tank_id": 7793, - "tier": 7, - "type": "heavyTank" - }, - "7809": { - "is_premium": false, - "name": "53TP Markowskiego", - "nation": "european", - "tank_id": 7809, - "tier": 8, - "type": "heavyTank" - }, - "7937": { - "is_premium": false, - "name": "T-54", - "nation": "ussr", - "tank_id": 7937, - "tier": 9, - "type": "mediumTank" - }, - "7953": { - "is_premium": false, - "name": "Jagdtiger", - "nation": "germany", - "tank_id": 7953, - "tier": 9, - "type": "AT-SPG" - }, - "7985": { - "is_premium": false, - "name": "WZ-111-1G FT", - "nation": "china", - "tank_id": 7985, - "tier": 8, - "type": "AT-SPG" - }, - "8001": { - "is_premium": true, - "name": "Lorraine 40 t", - "nation": "france", - "tank_id": 8001, - "tier": 8, - "type": "mediumTank" - }, - "8017": { - "is_premium": true, - "name": "Valentine AT", - "nation": "uk", - "tank_id": 8017, - "tier": 3, - "type": "AT-SPG" - }, - "8049": { - "is_premium": true, - "name": "Spike", - "nation": "other", - "tank_id": 8049, - "tier": 5, - "type": "mediumTank" - }, - "8065": { - "is_premium": false, - "name": "40TP Habicha", - "nation": "european", - "tank_id": 8065, - "tier": 6, - "type": "heavyTank" - }, - "8193": { - "is_premium": false, - "name": "Object 704", - "nation": "ussr", - "tank_id": 8193, - "tier": 9, - "type": "AT-SPG" - }, - "8209": { - "is_premium": true, - "name": "Pz.Kpfw. 38 (t) n.A.", - "nation": "germany", - "tank_id": 8209, - "tier": 4, - "type": "lightTank" - }, - "8225": { - "is_premium": false, - "name": "T28", - "nation": "usa", - "tank_id": 8225, - "tier": 8, - "type": "AT-SPG" - }, - "8241": { - "is_premium": false, - "name": "WZ-111G FT", - "nation": "china", - "tank_id": 8241, - "tier": 9, - "type": "AT-SPG" - }, - "8257": { - "is_premium": true, - "name": "Renault UE 57", - "nation": "france", - "tank_id": 8257, - "tier": 3, - "type": "AT-SPG" - }, - "8273": { - "is_premium": true, - "name": "Universal Carrier 2-pdr", - "nation": "uk", - "tank_id": 8273, - "tier": 2, - "type": "AT-SPG" - }, - "8305": { - "is_premium": true, - "name": "Titan H-Nd", - "nation": "other", - "tank_id": 8305, - "tier": 7, - "type": "heavyTank" - }, - "8321": { - "is_premium": false, - "name": "45TP Habicha", - "nation": "european", - "tank_id": 8321, - "tier": 7, - "type": "heavyTank" - }, - "8465": { - "is_premium": false, - "name": "Panther II", - "nation": "germany", - "tank_id": 8465, - "tier": 8, - "type": "mediumTank" - }, - "8497": { - "is_premium": true, - "name": "WZ-111 model 5A", - "nation": "china", - "tank_id": 8497, - "tier": 10, - "type": "heavyTank" - }, - "8513": { - "is_premium": true, - "name": "AMX 30 B", - "nation": "france", - "tank_id": 8513, - "tier": 10, - "type": "mediumTank" - }, - "8529": { - "is_premium": false, - "name": "AT 15", - "nation": "uk", - "tank_id": 8529, - "tier": 8, - "type": "AT-SPG" - }, - "8561": { - "is_premium": true, - "name": "Titan T24 57", - "nation": "other", - "tank_id": 8561, - "tier": 6, - "type": "lightTank" - }, - "8577": { - "is_premium": true, - "name": "Lansen C", - "nation": "european", - "tank_id": 8577, - "tier": 8, - "type": "mediumTank" - }, - "8737": { - "is_premium": false, - "name": "T95", - "nation": "usa", - "tank_id": 8737, - "tier": 9, - "type": "AT-SPG" - }, - "8753": { - "is_premium": true, - "name": "M41D", - "nation": "china", - "tank_id": 8753, - "tier": 7, - "type": "lightTank" - }, - "8785": { - "is_premium": false, - "name": "AT 2", - "nation": "uk", - "tank_id": 8785, - "tier": 5, - "type": "AT-SPG" - }, - "8817": { - "is_premium": true, - "name": "Titan Mk. I", - "nation": "other", - "tank_id": 8817, - "tier": 5, - "type": "lightTank" - }, - "8833": { - "is_premium": true, - "name": "Spark", - "nation": "european", - "tank_id": 8833, - "tier": 6, - "type": "mediumTank" - }, - "8961": { - "is_premium": true, - "name": "KV-13", - "nation": "ussr", - "tank_id": 8961, - "tier": 7, - "type": "mediumTank" - }, - "8993": { - "is_premium": false, - "name": "M46 Patton", - "nation": "usa", - "tank_id": 8993, - "tier": 9, - "type": "mediumTank" - }, - "9009": { - "is_premium": true, - "name": "Ox", - "nation": "china", - "tank_id": 9009, - "tier": 6, - "type": "mediumTank" - }, - "9041": { - "is_premium": false, - "name": "Alecto", - "nation": "uk", - "tank_id": 9041, - "tier": 4, - "type": "AT-SPG" - }, - "9073": { - "is_premium": true, - "name": "Titan-54d", - "nation": "other", - "tank_id": 9073, - "tier": 8, - "type": "mediumTank" - }, - "9089": { - "is_premium": true, - "name": "Škoda T 56", - "nation": "european", - "tank_id": 9089, - "tier": 8, - "type": "heavyTank" - }, - "9217": { - "is_premium": true, - "name": "IS-6", - "nation": "ussr", - "tank_id": 9217, - "tier": 8, - "type": "heavyTank" - }, - "9249": { - "is_premium": false, - "name": "T25 AT", - "nation": "usa", - "tank_id": 9249, - "tier": 7, - "type": "AT-SPG" - }, - "9265": { - "is_premium": true, - "name": "Type 58", - "nation": "china", - "tank_id": 9265, - "tier": 6, - "type": "mediumTank" - }, - "9297": { - "is_premium": false, - "name": "FV215b (183)", - "nation": "uk", - "tank_id": 9297, - "tier": 10, - "type": "AT-SPG" - }, - "9329": { - "is_premium": true, - "name": "Titan-150", - "nation": "other", - "tank_id": 9329, - "tier": 6, - "type": "heavyTank" - }, - "9345": { - "is_premium": true, - "name": "Svear", - "nation": "european", - "tank_id": 9345, - "tier": 7, - "type": "mediumTank" - }, - "9489": { - "is_premium": false, - "name": "E 100", - "nation": "germany", - "tank_id": 9489, - "tier": 10, - "type": "heavyTank" - }, - "9505": { - "is_premium": false, - "name": "M103", - "nation": "usa", - "tank_id": 9505, - "tier": 9, - "type": "heavyTank" - }, - "9521": { - "is_premium": true, - "name": "WZ-122 TM", - "nation": "china", - "tank_id": 9521, - "tier": 8, - "type": "mediumTank" - }, - "9553": { - "is_premium": false, - "name": "AT 8", - "nation": "uk", - "tank_id": 9553, - "tier": 6, - "type": "AT-SPG" - }, - "9601": { - "is_premium": true, - "name": "CS-52 LIS", - "nation": "european", - "tank_id": 9601, - "tier": 7, - "type": "mediumTank" - }, - "9745": { - "is_premium": false, - "name": "E 75", - "nation": "germany", - "tank_id": 9745, - "tier": 9, - "type": "heavyTank" - }, - "9761": { - "is_premium": false, - "name": "M24 Chaffee", - "nation": "usa", - "tank_id": 9761, - "tier": 6, - "type": "lightTank" - }, - "9777": { - "is_premium": true, - "name": "WZ-114", - "nation": "china", - "tank_id": 9777, - "tier": 9, - "type": "heavyTank" - }, - "9793": { - "is_premium": false, - "name": "Somua SAu 40", - "nation": "france", - "tank_id": 9793, - "tier": 4, - "type": "AT-SPG" - }, - "9809": { - "is_premium": true, - "name": "Churchill Gun Carrier", - "nation": "uk", - "tank_id": 9809, - "tier": 6, - "type": "AT-SPG" - }, - "9841": { - "is_premium": true, - "name": "Rover", - "nation": "other", - "tank_id": 9841, - "tier": 6, - "type": "lightTank" - }, - "9857": { - "is_premium": true, - "name": "Škoda T 45", - "nation": "european", - "tank_id": 9857, - "tier": 7, - "type": "heavyTank" - }, - "9985": { - "is_premium": false, - "name": "SU-101", - "nation": "ussr", - "tank_id": 9985, - "tier": 8, - "type": "AT-SPG" - }, - "10001": { - "is_premium": false, - "name": "VK 28.01", - "nation": "germany", - "tank_id": 10001, - "tier": 6, - "type": "lightTank" - }, - "10017": { - "is_premium": true, - "name": "M4A3E2 Sherman Jumbo", - "nation": "usa", - "tank_id": 10017, - "tier": 6, - "type": "mediumTank" - }, - "10033": { - "is_premium": false, - "name": "WZ-132A", - "nation": "china", - "tank_id": 10033, - "tier": 9, - "type": "lightTank" - }, - "10049": { - "is_premium": false, - "name": "S35 CA", - "nation": "france", - "tank_id": 10049, - "tier": 5, - "type": "AT-SPG" - }, - "10065": { - "is_premium": false, - "name": "AT 7", - "nation": "uk", - "tank_id": 10065, - "tier": 7, - "type": "AT-SPG" - }, - "10097": { - "is_premium": true, - "name": "Medjay", - "nation": "other", - "tank_id": 10097, - "tier": 5, - "type": "heavyTank" - }, - "10113": { - "is_premium": true, - "name": "Carro da Combattimento 45t", - "nation": "european", - "tank_id": 10113, - "tier": 10, - "type": "mediumTank" - }, - "10241": { - "is_premium": false, - "name": "SU-100M1", - "nation": "ussr", - "tank_id": 10241, - "tier": 7, - "type": "AT-SPG" - }, - "10257": { - "is_premium": false, - "name": "E 50", - "nation": "germany", - "tank_id": 10257, - "tier": 9, - "type": "mediumTank" - }, - "10273": { - "is_premium": false, - "name": "M8A1", - "nation": "usa", - "tank_id": 10273, - "tier": 4, - "type": "AT-SPG" - }, - "10289": { - "is_premium": false, - "name": "WZ-132-1", - "nation": "china", - "tank_id": 10289, - "tier": 10, - "type": "lightTank" - }, - "10353": { - "is_premium": true, - "name": "Pharaoh", - "nation": "other", - "tank_id": 10353, - "tier": 7, - "type": "heavyTank" - }, - "10369": { - "is_premium": false, - "name": "Controcarro 3 Minotauro", - "nation": "european", - "tank_id": 10369, - "tier": 10, - "type": "AT-SPG" - }, - "10497": { - "is_premium": false, - "name": "KV-2", - "nation": "ussr", - "tank_id": 10497, - "tier": 6, - "type": "heavyTank" - }, - "10513": { - "is_premium": false, - "name": "VK 45.02 (P) Ausf. A", - "nation": "germany", - "tank_id": 10513, - "tier": 8, - "type": "heavyTank" - }, - "10529": { - "is_premium": true, - "name": "T67", - "nation": "usa", - "tank_id": 10529, - "tier": 5, - "type": "AT-SPG" - }, - "10545": { - "is_premium": true, - "name": "Wind", - "nation": "china", - "tank_id": 10545, - "tier": 6, - "type": "lightTank" - }, - "10625": { - "is_premium": false, - "name": "Controcarro 1 Mk. 2", - "nation": "european", - "tank_id": 10625, - "tier": 9, - "type": "AT-SPG" - }, - "10753": { - "is_premium": false, - "name": "ST-I", - "nation": "ussr", - "tank_id": 10753, - "tier": 9, - "type": "heavyTank" - }, - "10769": { - "is_premium": false, - "name": "Tiger (P)", - "nation": "germany", - "tank_id": 10769, - "tier": 7, - "type": "heavyTank" - }, - "10785": { - "is_premium": false, - "name": "T110E5", - "nation": "usa", - "tank_id": 10785, - "tier": 10, - "type": "heavyTank" - }, - "10801": { - "is_premium": true, - "name": "Panlong", - "nation": "china", - "tank_id": 10801, - "tier": 6, - "type": "AT-SPG" - }, - "10817": { - "is_premium": false, - "name": "AMX AC mle. 46", - "nation": "france", - "tank_id": 10817, - "tier": 7, - "type": "AT-SPG" - }, - "10881": { - "is_premium": false, - "name": "Semovente Controcarro mod. 1964", - "nation": "european", - "tank_id": 10881, - "tier": 8, - "type": "AT-SPG" - }, - "11009": { - "is_premium": false, - "name": "KV-4", - "nation": "ussr", - "tank_id": 11009, - "tier": 8, - "type": "heavyTank" - }, - "11025": { - "is_premium": false, - "name": "Sturer Emil", - "nation": "germany", - "tank_id": 11025, - "tier": 7, - "type": "AT-SPG" - }, - "11041": { - "is_premium": false, - "name": "T25/2", - "nation": "usa", - "tank_id": 11041, - "tier": 7, - "type": "AT-SPG" - }, - "11073": { - "is_premium": false, - "name": "AMX 50 Foch", - "nation": "france", - "tank_id": 11073, - "tier": 9, - "type": "AT-SPG" - }, - "11137": { - "is_premium": false, - "name": "Semovente Controcarro mod. 1956", - "nation": "european", - "tank_id": 11137, - "tier": 7, - "type": "AT-SPG" - }, - "11265": { - "is_premium": true, - "name": "T-150", - "nation": "ussr", - "tank_id": 11265, - "tier": 6, - "type": "heavyTank" - }, - "11281": { - "is_premium": true, - "name": "Kampfpanzer 70", - "nation": "germany", - "tank_id": 11281, - "tier": 9, - "type": "heavyTank" - }, - "11297": { - "is_premium": false, - "name": "T28 Prototype", - "nation": "usa", - "tank_id": 11297, - "tier": 8, - "type": "AT-SPG" - }, - "11393": { - "is_premium": false, - "name": "Semovente M43 Bassotto", - "nation": "european", - "tank_id": 11393, - "tier": 6, - "type": "AT-SPG" - }, - "11521": { - "is_premium": false, - "name": "IS-8", - "nation": "ussr", - "tank_id": 11521, - "tier": 9, - "type": "heavyTank" - }, - "11537": { - "is_premium": false, - "name": "Jagdpanther II", - "nation": "germany", - "tank_id": 11537, - "tier": 8, - "type": "AT-SPG" - }, - "11553": { - "is_premium": false, - "name": "M18 Hellcat", - "nation": "usa", - "tank_id": 11553, - "tier": 6, - "type": "AT-SPG" - }, - "11585": { - "is_premium": false, - "name": "ARL V39", - "nation": "france", - "tank_id": 11585, - "tier": 6, - "type": "AT-SPG" - }, - "11649": { - "is_premium": false, - "name": "Semovente M41", - "nation": "european", - "tank_id": 11649, - "tier": 5, - "type": "AT-SPG" - }, - "11777": { - "is_premium": false, - "name": "KV-1", - "nation": "ussr", - "tank_id": 11777, - "tier": 5, - "type": "heavyTank" - }, - "11793": { - "is_premium": false, - "name": "Nashorn", - "nation": "germany", - "tank_id": 11793, - "tier": 6, - "type": "AT-SPG" - }, - "11809": { - "is_premium": true, - "name": "T23E3", - "nation": "usa", - "tank_id": 11809, - "tier": 7, - "type": "mediumTank" - }, - "11905": { - "is_premium": true, - "name": "Shadowhunter", - "nation": "european", - "tank_id": 11905, - "tier": 6, - "type": "mediumTank" - }, - "12033": { - "is_premium": false, - "name": "SU-122-54", - "nation": "ussr", - "tank_id": 12033, - "tier": 9, - "type": "AT-SPG" - }, - "12049": { - "is_premium": false, - "name": "Jagdpanzer E 100", - "nation": "germany", - "tank_id": 12049, - "tier": 10, - "type": "AT-SPG" - }, - "12065": { - "is_premium": true, - "name": "T95E2", - "nation": "usa", - "tank_id": 12065, - "tier": 8, - "type": "mediumTank" - }, - "12097": { - "is_premium": false, - "name": "AMX AC mle. 48", - "nation": "france", - "tank_id": 12097, - "tier": 8, - "type": "AT-SPG" - }, - "12161": { - "is_premium": true, - "name": "Strv K", - "nation": "european", - "tank_id": 12161, - "tier": 10, - "type": "heavyTank" - }, - "12305": { - "is_premium": false, - "name": "E 50 Ausf. M", - "nation": "germany", - "tank_id": 12305, - "tier": 10, - "type": "mediumTank" - }, - "12321": { - "is_premium": true, - "name": "T6E1 Grizzly", - "nation": "usa", - "tank_id": 12321, - "tier": 4, - "type": "mediumTank" - }, - "12417": { - "is_premium": true, - "name": "Bisonte C45", - "nation": "european", - "tank_id": 12417, - "tier": 8, - "type": "heavyTank" - }, - "12545": { - "is_premium": true, - "name": "K-91", - "nation": "ussr", - "tank_id": 12545, - "tier": 9, - "type": "heavyTank" - }, - "12673": { - "is_premium": true, - "name": "Bofors Tornvagn", - "nation": "european", - "tank_id": 12673, - "tier": 8, - "type": "heavyTank" - }, - "13073": { - "is_premium": true, - "name": "Pz.Kpfw. II Ausf. G", - "nation": "germany", - "tank_id": 13073, - "tier": 3, - "type": "lightTank" - }, - "13089": { - "is_premium": false, - "name": "T110E4", - "nation": "usa", - "tank_id": 13089, - "tier": 10, - "type": "AT-SPG" - }, - "13185": { - "is_premium": true, - "name": "Vz. 55", - "nation": "european", - "tank_id": 13185, - "tier": 10, - "type": "heavyTank" - }, - "13329": { - "is_premium": true, - "name": "Durchbruchswagen 2", - "nation": "germany", - "tank_id": 13329, - "tier": 4, - "type": "heavyTank" - }, - "13345": { - "is_premium": true, - "name": "T26E4 SuperPershing", - "nation": "usa", - "tank_id": 13345, - "tier": 8, - "type": "mediumTank" - }, - "13569": { - "is_premium": false, - "name": "Object 268", - "nation": "ussr", - "tank_id": 13569, - "tier": 10, - "type": "AT-SPG" - }, - "13825": { - "is_premium": false, - "name": "T-62A", - "nation": "ussr", - "tank_id": 13825, - "tier": 10, - "type": "mediumTank" - }, - "13841": { - "is_premium": false, - "name": "Indien-Panzer", - "nation": "germany", - "tank_id": 13841, - "tier": 8, - "type": "mediumTank" - }, - "13857": { - "is_premium": false, - "name": "T110E3", - "nation": "usa", - "tank_id": 13857, - "tier": 10, - "type": "AT-SPG" - }, - "13889": { - "is_premium": false, - "name": "AMX 50 Foch (155)", - "nation": "france", - "tank_id": 13889, - "tier": 10, - "type": "AT-SPG" - }, - "14097": { - "is_premium": false, - "name": "VK 30.01 (D)", - "nation": "germany", - "tank_id": 14097, - "tier": 6, - "type": "mediumTank" - }, - "14113": { - "is_premium": false, - "name": "M48 Patton", - "nation": "usa", - "tank_id": 14113, - "tier": 10, - "type": "mediumTank" - }, - "14145": { - "is_premium": false, - "name": "AMX ELC bis", - "nation": "france", - "tank_id": 14145, - "tier": 5, - "type": "lightTank" - }, - "14337": { - "is_premium": false, - "name": "Object 263", - "nation": "ussr", - "tank_id": 14337, - "tier": 10, - "type": "AT-SPG" - }, - "14609": { - "is_premium": false, - "name": "Leopard 1", - "nation": "germany", - "tank_id": 14609, - "tier": 10, - "type": "mediumTank" - }, - "14625": { - "is_premium": false, - "name": "T69", - "nation": "usa", - "tank_id": 14625, - "tier": 8, - "type": "mediumTank" - }, - "14865": { - "is_premium": false, - "name": "Leopard Prototyp A", - "nation": "germany", - "tank_id": 14865, - "tier": 9, - "type": "mediumTank" - }, - "14881": { - "is_premium": false, - "name": "T57 Heavy Tank", - "nation": "usa", - "tank_id": 14881, - "tier": 10, - "type": "heavyTank" - }, - "15137": { - "is_premium": true, - "name": "T21", - "nation": "usa", - "tank_id": 15137, - "tier": 6, - "type": "lightTank" - }, - "15393": { - "is_premium": false, - "name": "T54E1", - "nation": "usa", - "tank_id": 15393, - "tier": 9, - "type": "mediumTank" - }, - "15441": { - "is_premium": true, - "name": "Chieftain/T95", - "nation": "uk", - "tank_id": 15441, - "tier": 8, - "type": "heavyTank" - }, - "15617": { - "is_premium": true, - "name": "Object 907", - "nation": "ussr", - "tank_id": 15617, - "tier": 10, - "type": "mediumTank" - }, - "15649": { - "is_premium": false, - "name": "T71", - "nation": "usa", - "tank_id": 15649, - "tier": 7, - "type": "lightTank" - }, - "15697": { - "is_premium": true, - "name": "Chieftain Mk. 6", - "nation": "uk", - "tank_id": 15697, - "tier": 10, - "type": "heavyTank" - }, - "15889": { - "is_premium": true, - "name": "VK 30.02 (M)", - "nation": "germany", - "tank_id": 15889, - "tier": 6, - "type": "mediumTank" - }, - "15905": { - "is_premium": true, - "name": "M60", - "nation": "usa", - "tank_id": 15905, - "tier": 10, - "type": "mediumTank" - }, - "15937": { - "is_premium": false, - "name": "Renault R35", - "nation": "france", - "tank_id": 15937, - "tier": 1, - "type": "mediumTank" - }, - "15953": { - "is_premium": true, - "name": "FV201 (A45)", - "nation": "uk", - "tank_id": 15953, - "tier": 7, - "type": "heavyTank" - }, - "16145": { - "is_premium": true, - "name": "Pz.Sfl. IVc", - "nation": "germany", - "tank_id": 16145, - "tier": 5, - "type": "AT-SPG" - }, - "16193": { - "is_premium": true, - "name": "M4A1 Revalorisé", - "nation": "france", - "tank_id": 16193, - "tier": 8, - "type": "mediumTank" - }, - "16209": { - "is_premium": false, - "name": "Cruiser Mk. I", - "nation": "uk", - "tank_id": 16209, - "tier": 1, - "type": "lightTank" - }, - "16401": { - "is_premium": false, - "name": "Waffenträger auf Pz. IV", - "nation": "germany", - "tank_id": 16401, - "tier": 9, - "type": "AT-SPG" - }, - "16449": { - "is_premium": true, - "name": "AMX 13 57", - "nation": "france", - "tank_id": 16449, - "tier": 7, - "type": "lightTank" - }, - "16465": { - "is_premium": false, - "name": "Cruiser Mk. I", - "nation": "uk", - "tank_id": 16465, - "tier": 1, - "type": "lightTank" - }, - "16641": { - "is_premium": false, - "name": "MT-25", - "nation": "ussr", - "tank_id": 16641, - "tier": 6, - "type": "lightTank" - }, - "16657": { - "is_premium": false, - "name": "Rhm.-Borsig Waffenträger", - "nation": "germany", - "tank_id": 16657, - "tier": 8, - "type": "AT-SPG" - }, - "16673": { - "is_premium": false, - "name": "T37", - "nation": "usa", - "tank_id": 16673, - "tier": 6, - "type": "lightTank" - }, - "16705": { - "is_premium": true, - "name": "AMX M4 mle. 49", - "nation": "france", - "tank_id": 16705, - "tier": 8, - "type": "heavyTank" - }, - "16897": { - "is_premium": false, - "name": "Object 140", - "nation": "ussr", - "tank_id": 16897, - "tier": 10, - "type": "mediumTank" - }, - "17169": { - "is_premium": true, - "name": "Pz.Kpfw. IV Ausf. A", - "nation": "germany", - "tank_id": 17169, - "tier": 3, - "type": "mediumTank" - }, - "17217": { - "is_premium": true, - "name": "Eraser BP44", - "nation": "france", - "tank_id": 17217, - "tier": 6, - "type": "heavyTank" - }, - "17233": { - "is_premium": false, - "name": "FV4004 Conway", - "nation": "uk", - "tank_id": 17233, - "tier": 9, - "type": "AT-SPG" - }, - "17425": { - "is_premium": false, - "name": "Pz.Kpfw. IV Ausf. D", - "nation": "germany", - "tank_id": 17425, - "tier": 4, - "type": "mediumTank" - }, - "17473": { - "is_premium": true, - "name": "AMX Defender", - "nation": "france", - "tank_id": 17473, - "tier": 8, - "type": "lightTank" - }, - "17489": { - "is_premium": false, - "name": "Charioteer", - "nation": "uk", - "tank_id": 17489, - "tier": 8, - "type": "AT-SPG" - }, - "17729": { - "is_premium": true, - "name": "Somua SM", - "nation": "france", - "tank_id": 17729, - "tier": 8, - "type": "heavyTank" - }, - "17745": { - "is_premium": true, - "name": "FV217 Badger", - "nation": "uk", - "tank_id": 17745, - "tier": 10, - "type": "AT-SPG" - }, - "17953": { - "is_premium": false, - "name": "M41 Walker Bulldog", - "nation": "usa", - "tank_id": 17953, - "tier": 7, - "type": "lightTank" - }, - "17985": { - "is_premium": true, - "name": "Bretagne Panther", - "nation": "france", - "tank_id": 17985, - "tier": 6, - "type": "mediumTank" - }, - "18001": { - "is_premium": false, - "name": "FV4005", - "nation": "uk", - "tank_id": 18001, - "tier": 10, - "type": "AT-SPG" - }, - "18177": { - "is_premium": false, - "name": "T-54 ltwt.", - "nation": "ussr", - "tank_id": 18177, - "tier": 8, - "type": "lightTank" - }, - "18209": { - "is_premium": false, - "name": "T49", - "nation": "usa", - "tank_id": 18209, - "tier": 8, - "type": "lightTank" - }, - "18241": { - "is_premium": true, - "name": "Bat.-Châtillon Bourrasque", - "nation": "france", - "tank_id": 18241, - "tier": 8, - "type": "mediumTank" - }, - "18257": { - "is_premium": false, - "name": "Challenger", - "nation": "uk", - "tank_id": 18257, - "tier": 7, - "type": "AT-SPG" - }, - "18433": { - "is_premium": false, - "name": "LTTB", - "nation": "ussr", - "tank_id": 18433, - "tier": 7, - "type": "lightTank" - }, - "18449": { - "is_premium": false, - "name": "Spähpanzer Ru 251", - "nation": "germany", - "tank_id": 18449, - "tier": 8, - "type": "lightTank" - }, - "18465": { - "is_premium": false, - "name": "M2 Light Tank", - "nation": "usa", - "tank_id": 18465, - "tier": 1, - "type": "lightTank" - }, - "18497": { - "is_premium": true, - "name": "Lorraine 40 t Fearless", - "nation": "france", - "tank_id": 18497, - "tier": 8, - "type": "mediumTank" - }, - "18513": { - "is_premium": true, - "name": "Chimera", - "nation": "uk", - "tank_id": 18513, - "tier": 8, - "type": "mediumTank" - }, - "18689": { - "is_premium": true, - "name": "T-70/57", - "nation": "ussr", - "tank_id": 18689, - "tier": 3, - "type": "lightTank" - }, - "18721": { - "is_premium": false, - "name": "M2 Light Tank", - "nation": "usa", - "tank_id": 18721, - "tier": 1, - "type": "lightTank" - }, - "18753": { - "is_premium": true, - "name": "AMX Canon d'assaut 105", - "nation": "france", - "tank_id": 18753, - "tier": 8, - "type": "AT-SPG" - }, - "18769": { - "is_premium": true, - "name": "Caernarvon Action X", - "nation": "uk", - "tank_id": 18769, - "tier": 8, - "type": "heavyTank" - }, - "18945": { - "is_premium": true, - "name": "ISU-130", - "nation": "ussr", - "tank_id": 18945, - "tier": 8, - "type": "AT-SPG" - }, - "18961": { - "is_premium": false, - "name": "Spähpanzer SP I C", - "nation": "germany", - "tank_id": 18961, - "tier": 7, - "type": "lightTank" - }, - "18977": { - "is_premium": true, - "name": "T95E6", - "nation": "usa", - "tank_id": 18977, - "tier": 10, - "type": "heavyTank" - }, - "19009": { - "is_premium": true, - "name": "AMXmas", - "nation": "france", - "tank_id": 19009, - "tier": 6, - "type": "lightTank" - }, - "19025": { - "is_premium": true, - "name": "Defender Mk. 1", - "nation": "uk", - "tank_id": 19025, - "tier": 8, - "type": "mediumTank" - }, - "19201": { - "is_premium": false, - "name": "T-26", - "nation": "ussr", - "tank_id": 19201, - "tier": 1, - "type": "lightTank" - }, - "19217": { - "is_premium": false, - "name": "Grille 15", - "nation": "germany", - "tank_id": 19217, - "tier": 10, - "type": "AT-SPG" - }, - "19233": { - "is_premium": true, - "name": "Chrysler K", - "nation": "usa", - "tank_id": 19233, - "tier": 8, - "type": "heavyTank" - }, - "19265": { - "is_premium": true, - "name": "Charles", - "nation": "france", - "tank_id": 19265, - "tier": 6, - "type": "AT-SPG" - }, - "19281": { - "is_premium": true, - "name": "Super Conqueror", - "nation": "uk", - "tank_id": 19281, - "tier": 10, - "type": "heavyTank" - }, - "19457": { - "is_premium": false, - "name": "T-26", - "nation": "ussr", - "tank_id": 19457, - "tier": 1, - "type": "lightTank" - }, - "19473": { - "is_premium": true, - "name": "Krupp-38(D)", - "nation": "germany", - "tank_id": 19473, - "tier": 5, - "type": "mediumTank" - }, - "19489": { - "is_premium": true, - "name": "T28 Defender", - "nation": "usa", - "tank_id": 19489, - "tier": 8, - "type": "AT-SPG" - }, - "19521": { - "is_premium": true, - "name": "Bat-Chat-25CL", - "nation": "france", - "tank_id": 19521, - "tier": 9, - "type": "lightTank" - }, - "19537": { - "is_premium": false, - "name": "Vickers Light 105", - "nation": "uk", - "tank_id": 19537, - "tier": 10, - "type": "lightTank" - }, - "19713": { - "is_premium": true, - "name": "Loza's M4-A2 Sherman", - "nation": "ussr", - "tank_id": 19713, - "tier": 6, - "type": "mediumTank" - }, - "19729": { - "is_premium": true, - "name": "Tiger 131", - "nation": "germany", - "tank_id": 19729, - "tier": 6, - "type": "heavyTank" - }, - "19745": { - "is_premium": true, - "name": "T26E5", - "nation": "usa", - "tank_id": 19745, - "tier": 8, - "type": "heavyTank" - }, - "19777": { - "is_premium": true, - "name": "AltProto AMX 30", - "nation": "france", - "tank_id": 19777, - "tier": 8, - "type": "mediumTank" - }, - "19793": { - "is_premium": false, - "name": "Vickers Cruiser", - "nation": "uk", - "tank_id": 19793, - "tier": 9, - "type": "lightTank" - }, - "19969": { - "is_premium": true, - "name": "T-22 medium", - "nation": "ussr", - "tank_id": 19969, - "tier": 10, - "type": "mediumTank" - }, - "19985": { - "is_premium": true, - "name": "Rheinmetall Skorpion G", - "nation": "germany", - "tank_id": 19985, - "tier": 8, - "type": "AT-SPG" - }, - "20001": { - "is_premium": false, - "name": "T92E1", - "nation": "usa", - "tank_id": 20001, - "tier": 9, - "type": "lightTank" - }, - "20033": { - "is_premium": true, - "name": "Char Futur 4", - "nation": "france", - "tank_id": 20033, - "tier": 9, - "type": "mediumTank" - }, - "20049": { - "is_premium": false, - "name": "FV301", - "nation": "uk", - "tank_id": 20049, - "tier": 8, - "type": "lightTank" - }, - "20241": { - "is_premium": false, - "name": "Pz.Kpfw. II", - "nation": "germany", - "tank_id": 20241, - "tier": 1, - "type": "lightTank" - }, - "20257": { - "is_premium": false, - "name": "XM551 Sheridan", - "nation": "usa", - "tank_id": 20257, - "tier": 10, - "type": "lightTank" - }, - "20305": { - "is_premium": true, - "name": "Centurion Mk. 5/1 RAAC", - "nation": "uk", - "tank_id": 20305, - "tier": 8, - "type": "mediumTank" - }, - "20481": { - "is_premium": true, - "name": "Object 252U", - "nation": "ussr", - "tank_id": 20481, - "tier": 8, - "type": "heavyTank" - }, - "20497": { - "is_premium": false, - "name": "VK 100.01 (P)", - "nation": "germany", - "tank_id": 20497, - "tier": 8, - "type": "heavyTank" - }, - "20513": { - "is_premium": true, - "name": "T54E2", - "nation": "usa", - "tank_id": 20513, - "tier": 8, - "type": "heavyTank" - }, - "20561": { - "is_premium": true, - "name": "Turtle Mk. I", - "nation": "uk", - "tank_id": 20561, - "tier": 8, - "type": "AT-SPG" - }, - "20737": { - "is_premium": true, - "name": "SU-130PM", - "nation": "ussr", - "tank_id": 20737, - "tier": 8, - "type": "AT-SPG" - }, - "20753": { - "is_premium": false, - "name": "Mäuschen", - "nation": "germany", - "tank_id": 20753, - "tier": 9, - "type": "heavyTank" - }, - "20769": { - "is_premium": true, - "name": "T25 Pilot Number 1", - "nation": "usa", - "tank_id": 20769, - "tier": 8, - "type": "mediumTank" - }, - "20817": { - "is_premium": true, - "name": "Explorer", - "nation": "uk", - "tank_id": 20817, - "tier": 6, - "type": "mediumTank" - }, - "20993": { - "is_premium": true, - "name": "T-2020", - "nation": "ussr", - "tank_id": 20993, - "tier": 8, - "type": "heavyTank" - }, - "21009": { - "is_premium": true, - "name": "Panzer 58", - "nation": "germany", - "tank_id": 21009, - "tier": 8, - "type": "mediumTank" - }, - "21025": { - "is_premium": true, - "name": "T26E3 Eagle 7", - "nation": "usa", - "tank_id": 21025, - "tier": 7, - "type": "mediumTank" - }, - "21073": { - "is_premium": true, - "name": "Dreadnought", - "nation": "uk", - "tank_id": 21073, - "tier": 6, - "type": "AT-SPG" - }, - "21249": { - "is_premium": true, - "name": "Thunder", - "nation": "ussr", - "tank_id": 21249, - "tier": 6, - "type": "heavyTank" - }, - "21265": { - "is_premium": true, - "name": "VK 168.01 (P)", - "nation": "germany", - "tank_id": 21265, - "tier": 8, - "type": "heavyTank" - }, - "21281": { - "is_premium": true, - "name": "Rudolph", - "nation": "usa", - "tank_id": 21281, - "tier": 6, - "type": "mediumTank" - }, - "21329": { - "is_premium": true, - "name": "GSOR 1008", - "nation": "uk", - "tank_id": 21329, - "tier": 8, - "type": "AT-SPG" - }, - "21505": { - "is_premium": true, - "name": "T-44-85", - "nation": "ussr", - "tank_id": 21505, - "tier": 7, - "type": "mediumTank" - }, - "21521": { - "is_premium": true, - "name": "E 75 TS", - "nation": "germany", - "tank_id": 21521, - "tier": 8, - "type": "heavyTank" - }, - "21537": { - "is_premium": true, - "name": "Sherman Easy 8", - "nation": "usa", - "tank_id": 21537, - "tier": 6, - "type": "mediumTank" - }, - "21585": { - "is_premium": true, - "name": "Blasteroid", - "nation": "uk", - "tank_id": 21585, - "tier": 6, - "type": "heavyTank" - }, - "21761": { - "is_premium": true, - "name": "STG", - "nation": "ussr", - "tank_id": 21761, - "tier": 8, - "type": "mediumTank" - }, - "21777": { - "is_premium": true, - "name": "VK 90.01 (P)", - "nation": "germany", - "tank_id": 21777, - "tier": 10, - "type": "heavyTank" - }, - "21793": { - "is_premium": true, - "name": "XM551 Sheridan Missile", - "nation": "usa", - "tank_id": 21793, - "tier": 10, - "type": "lightTank" - }, - "21841": { - "is_premium": true, - "name": "Caliban", - "nation": "uk", - "tank_id": 21841, - "tier": 8, - "type": "heavyTank" - }, - "22017": { - "is_premium": true, - "name": "T-34-85", - "nation": "ussr", - "tank_id": 22017, - "tier": 6, - "type": "mediumTank" - }, - "22033": { - "is_premium": true, - "name": "Agent", - "nation": "germany", - "tank_id": 22033, - "tier": 6, - "type": "lightTank" - }, - "22049": { - "is_premium": true, - "name": "Magnus", - "nation": "usa", - "tank_id": 22049, - "tier": 6, - "type": "heavyTank" - }, - "22097": { - "is_premium": true, - "name": "Churchill VIII", - "nation": "uk", - "tank_id": 22097, - "tier": 6, - "type": "heavyTank" - }, - "22273": { - "is_premium": true, - "name": "Object 260", - "nation": "ussr", - "tank_id": 22273, - "tier": 10, - "type": "heavyTank" - }, - "22289": { - "is_premium": true, - "name": "Tiger I", - "nation": "germany", - "tank_id": 22289, - "tier": 6, - "type": "heavyTank" - }, - "22305": { - "is_premium": true, - "name": "AE Phase I", - "nation": "usa", - "tank_id": 22305, - "tier": 9, - "type": "heavyTank" - }, - "22353": { - "is_premium": true, - "name": "Churchill W", - "nation": "uk", - "tank_id": 22353, - "tier": 6, - "type": "heavyTank" - }, - "22529": { - "is_premium": true, - "name": "LT-432", - "nation": "ussr", - "tank_id": 22529, - "tier": 8, - "type": "lightTank" - }, - "22545": { - "is_premium": true, - "name": "Kanonenjagdpanzer 105", - "nation": "germany", - "tank_id": 22545, - "tier": 8, - "type": "AT-SPG" - }, - "22561": { - "is_premium": true, - "name": "TS-5", - "nation": "usa", - "tank_id": 22561, - "tier": 8, - "type": "AT-SPG" - }, - "22785": { - "is_premium": true, - "name": "Triumphant", - "nation": "ussr", - "tank_id": 22785, - "tier": 6, - "type": "mediumTank" - }, - "22801": { - "is_premium": true, - "name": "Icebreaker", - "nation": "germany", - "tank_id": 22801, - "tier": 6, - "type": "heavyTank" - }, - "22817": { - "is_premium": false, - "name": "M-VI-Yoh", - "nation": "usa", - "tank_id": 22817, - "tier": 10, - "type": "heavyTank" - }, - "23041": { - "is_premium": true, - "name": "T-34 shielded", - "nation": "ussr", - "tank_id": 23041, - "tier": 5, - "type": "mediumTank" - }, - "23057": { - "is_premium": true, - "name": "Kunze Panzer", - "nation": "germany", - "tank_id": 23057, - "tier": 7, - "type": "lightTank" - }, - "23073": { - "is_premium": false, - "name": "M-V-Yoh", - "nation": "usa", - "tank_id": 23073, - "tier": 9, - "type": "heavyTank" - }, - "23297": { - "is_premium": true, - "name": "Object 244", - "nation": "ussr", - "tank_id": 23297, - "tier": 6, - "type": "heavyTank" - }, - "23313": { - "is_premium": true, - "name": "Kampfpanzer 50 t", - "nation": "germany", - "tank_id": 23313, - "tier": 10, - "type": "mediumTank" - }, - "23329": { - "is_premium": false, - "name": "M-III-Yoh", - "nation": "usa", - "tank_id": 23329, - "tier": 8, - "type": "heavyTank" - }, - "23553": { - "is_premium": true, - "name": "MS-1", - "nation": "ussr", - "tank_id": 23553, - "tier": 2, - "type": "lightTank" - }, - "23569": { - "is_premium": true, - "name": "Pz. IV Gargoyle", - "nation": "germany", - "tank_id": 23569, - "tier": 5, - "type": "mediumTank" - }, - "23585": { - "is_premium": false, - "name": "M-VII-Yoh", - "nation": "usa", - "tank_id": 23585, - "tier": 7, - "type": "heavyTank" - }, - "23809": { - "is_premium": false, - "name": "Object 84", - "nation": "ussr", - "tank_id": 23809, - "tier": 9, - "type": "lightTank" - }, - "23825": { - "is_premium": true, - "name": "Krupp-Steyr Waffenträger", - "nation": "germany", - "tank_id": 23825, - "tier": 7, - "type": "AT-SPG" - }, - "23841": { - "is_premium": true, - "name": "Super Hellcat", - "nation": "usa", - "tank_id": 23841, - "tier": 7, - "type": "AT-SPG" - }, - "24065": { - "is_premium": false, - "name": "LTG", - "nation": "ussr", - "tank_id": 24065, - "tier": 7, - "type": "lightTank" - }, - "24081": { - "is_premium": true, - "name": "U-Panzer", - "nation": "germany", - "tank_id": 24081, - "tier": 6, - "type": "mediumTank" - }, - "24097": { - "is_premium": true, - "name": "BLTZ9000", - "nation": "usa", - "tank_id": 24097, - "tier": 6, - "type": "mediumTank" - }, - "24321": { - "is_premium": false, - "name": "T-100 LT", - "nation": "ussr", - "tank_id": 24321, - "tier": 10, - "type": "lightTank" - }, - "24337": { - "is_premium": true, - "name": "M48A2 Räumpanzer", - "nation": "germany", - "tank_id": 24337, - "tier": 8, - "type": "mediumTank" - }, - "24577": { - "is_premium": true, - "name": "Object 268 Version 4", - "nation": "ussr", - "tank_id": 24577, - "tier": 10, - "type": "AT-SPG" - }, - "24593": { - "is_premium": true, - "name": "Keiler", - "nation": "germany", - "tank_id": 24593, - "tier": 8, - "type": "heavyTank" - }, - "24609": { - "is_premium": true, - "name": "Concept 1B", - "nation": "usa", - "tank_id": 24609, - "tier": 10, - "type": "heavyTank" - }, - "24849": { - "is_premium": true, - "name": "Kryos", - "nation": "germany", - "tank_id": 24849, - "tier": 6, - "type": "AT-SPG" - }, - "24865": { - "is_premium": true, - "name": "Scepter", - "nation": "usa", - "tank_id": 24865, - "tier": 8, - "type": "heavyTank" - }, - "25089": { - "is_premium": true, - "name": "Object 752", - "nation": "ussr", - "tank_id": 25089, - "tier": 9, - "type": "heavyTank" - }, - "25105": { - "is_premium": true, - "name": "Barkhan", - "nation": "germany", - "tank_id": 25105, - "tier": 6, - "type": "heavyTank" - }, - "25345": { - "is_premium": true, - "name": "Object 274a", - "nation": "ussr", - "tank_id": 25345, - "tier": 8, - "type": "mediumTank" - }, - "25361": { - "is_premium": true, - "name": "Waffenträger Ritter", - "nation": "germany", - "tank_id": 25361, - "tier": 9, - "type": "AT-SPG" - }, - "25377": { - "is_premium": true, - "name": "T77", - "nation": "usa", - "tank_id": 25377, - "tier": 8, - "type": "heavyTank" - }, - "25601": { - "is_premium": true, - "name": "IS-2 shielded", - "nation": "ussr", - "tank_id": 25601, - "tier": 7, - "type": "heavyTank" - }, - "25617": { - "is_premium": true, - "name": "HWK 30", - "nation": "germany", - "tank_id": 25617, - "tier": 8, - "type": "lightTank" - }, - "25633": { - "is_premium": true, - "name": "M-IV-Y", - "nation": "usa", - "tank_id": 25633, - "tier": 8, - "type": "heavyTank" - }, - "25857": { - "is_premium": true, - "name": "Object 777 Version II", - "nation": "ussr", - "tank_id": 25857, - "tier": 10, - "type": "heavyTank" - }, - "25889": { - "is_premium": true, - "name": "Ranger", - "nation": "usa", - "tank_id": 25889, - "tier": 6, - "type": "lightTank" - }, - "26113": { - "is_premium": true, - "name": "Object 452K", - "nation": "ussr", - "tank_id": 26113, - "tier": 9, - "type": "heavyTank" - }, - "26129": { - "is_premium": true, - "name": "Epsilon", - "nation": "germany", - "tank_id": 26129, - "tier": 6, - "type": "AT-SPG" - }, - "26145": { - "is_premium": true, - "name": "High Score", - "nation": "usa", - "tank_id": 26145, - "tier": 5, - "type": "mediumTank" - }, - "26401": { - "is_premium": true, - "name": "Enforcer", - "nation": "usa", - "tank_id": 26401, - "tier": 6, - "type": "AT-SPG" - }, - "26657": { - "is_premium": true, - "name": "ASTRON Rex 105 mm", - "nation": "usa", - "tank_id": 26657, - "tier": 8, - "type": "mediumTank" - }, - "26913": { - "is_premium": true, - "name": "Frosty", - "nation": "usa", - "tank_id": 26913, - "tier": 6, - "type": "lightTank" - }, - "27169": { - "is_premium": true, - "name": "Cyborg", - "nation": "usa", - "tank_id": 27169, - "tier": 6, - "type": "heavyTank" - }, - "27425": { - "is_premium": false, - "name": "TL-7-120", - "nation": "usa", - "tank_id": 27425, - "tier": 9, - "type": "heavyTank" - }, - "27681": { - "is_premium": true, - "name": "Pawlack Tank", - "nation": "usa", - "tank_id": 27681, - "tier": 6, - "type": "heavyTank" - }, - "27937": { - "is_premium": true, - "name": "T49 Fearless", - "nation": "usa", - "tank_id": 27937, - "tier": 8, - "type": "lightTank" - }, - "28449": { - "is_premium": true, - "name": "T42", - "nation": "usa", - "tank_id": 28449, - "tier": 7, - "type": "mediumTank" - }, - "51201": { - "is_premium": true, - "name": "KV-220 Beta-Test", - "nation": "ussr", - "tank_id": 51201, - "tier": 5, - "type": "heavyTank" - }, - "51457": { - "is_premium": true, - "name": "Matilda IV", - "nation": "ussr", - "tank_id": 51457, - "tier": 5, - "type": "mediumTank" - }, - "51473": { - "is_premium": true, - "name": "Pz.Kpfw. V/IV", - "nation": "germany", - "tank_id": 51473, - "tier": 5, - "type": "mediumTank" - }, - "51489": { - "is_premium": true, - "name": "T2 Light Tank", - "nation": "usa", - "tank_id": 51489, - "tier": 2, - "type": "lightTank" - }, - "51713": { - "is_premium": true, - "name": "Churchill III", - "nation": "ussr", - "tank_id": 51713, - "tier": 5, - "type": "heavyTank" - }, - "51729": { - "is_premium": true, - "name": "Pz.Kpfw. II Ausf. J", - "nation": "germany", - "tank_id": 51729, - "tier": 3, - "type": "lightTank" - }, - "51745": { - "is_premium": true, - "name": "Ram II", - "nation": "usa", - "tank_id": 51745, - "tier": 5, - "type": "mediumTank" - }, - "51809": { - "is_premium": true, - "name": "Type 98 Ke-Ni Otsu", - "nation": "japan", - "tank_id": 51809, - "tier": 3, - "type": "lightTank" - }, - "51985": { - "is_premium": true, - "name": "Pz.Kpfw. S35 739 (f)", - "nation": "germany", - "tank_id": 51985, - "tier": 3, - "type": "mediumTank" - }, - "52065": { - "is_premium": true, - "name": "Hetzer Kame SP", - "nation": "japan", - "tank_id": 52065, - "tier": 4, - "type": "AT-SPG" - }, - "52225": { - "is_premium": true, - "name": "BT-SV", - "nation": "ussr", - "tank_id": 52225, - "tier": 3, - "type": "lightTank" - }, - "52241": { - "is_premium": true, - "name": "Pz.Kpfw. B2 740 (f)", - "nation": "germany", - "tank_id": 52241, - "tier": 4, - "type": "heavyTank" - }, - "52257": { - "is_premium": true, - "name": "M4A2E4 Sherman", - "nation": "usa", - "tank_id": 52257, - "tier": 5, - "type": "mediumTank" - }, - "52481": { - "is_premium": true, - "name": "Valentine II", - "nation": "ussr", - "tank_id": 52481, - "tier": 4, - "type": "lightTank" - }, - "52497": { - "is_premium": true, - "name": "Pz.Kpfw. 38H 735 (f)", - "nation": "germany", - "tank_id": 52497, - "tier": 2, - "type": "lightTank" - }, - "52513": { - "is_premium": true, - "name": "M6A2E1", - "nation": "usa", - "tank_id": 52513, - "tier": 7, - "type": "heavyTank" - }, - "52561": { - "is_premium": false, - "name": "Tortoise", - "nation": "uk", - "tank_id": 52561, - "tier": 9, - "type": "AT-SPG" - }, - "52737": { - "is_premium": true, - "name": "M3 Light", - "nation": "ussr", - "tank_id": 52737, - "tier": 3, - "type": "lightTank" - }, - "52769": { - "is_premium": true, - "name": "M22 Locust", - "nation": "usa", - "tank_id": 52769, - "tier": 3, - "type": "lightTank" - }, - "52993": { - "is_premium": true, - "name": "A-32", - "nation": "ussr", - "tank_id": 52993, - "tier": 4, - "type": "mediumTank" - }, - "53025": { - "is_premium": true, - "name": "M6A2E1 EXP", - "nation": "usa", - "tank_id": 53025, - "tier": 8, - "type": "heavyTank" - }, - "53249": { - "is_premium": true, - "name": "KV-5", - "nation": "ussr", - "tank_id": 53249, - "tier": 8, - "type": "heavyTank" - }, - "53505": { - "is_premium": true, - "name": "T-127", - "nation": "ussr", - "tank_id": 53505, - "tier": 3, - "type": "lightTank" - }, - "53537": { - "is_premium": true, - "name": "T1E6", - "nation": "usa", - "tank_id": 53537, - "tier": 2, - "type": "lightTank" - }, - "53585": { - "is_premium": true, - "name": "Matilda Black Prince", - "nation": "uk", - "tank_id": 53585, - "tier": 5, - "type": "mediumTank" - }, - "53761": { - "is_premium": true, - "name": "SU-85I", - "nation": "ussr", - "tank_id": 53761, - "tier": 5, - "type": "AT-SPG" - }, - "53841": { - "is_premium": true, - "name": "TOG II*", - "nation": "uk", - "tank_id": 53841, - "tier": 6, - "type": "heavyTank" - }, - "54097": { - "is_premium": true, - "name": "AT 15A", - "nation": "uk", - "tank_id": 54097, - "tier": 7, - "type": "AT-SPG" - }, - "54273": { - "is_premium": true, - "name": "SU-76I", - "nation": "ussr", - "tank_id": 54273, - "tier": 3, - "type": "AT-SPG" - }, - "54289": { - "is_premium": true, - "name": "Löwe", - "nation": "germany", - "tank_id": 54289, - "tier": 8, - "type": "heavyTank" - }, - "54353": { - "is_premium": true, - "name": "Excelsior", - "nation": "uk", - "tank_id": 54353, - "tier": 5, - "type": "heavyTank" - }, - "54529": { - "is_premium": true, - "name": "Tetrarch", - "nation": "ussr", - "tank_id": 54529, - "tier": 2, - "type": "lightTank" - }, - "54545": { - "is_premium": true, - "name": "T-25", - "nation": "germany", - "tank_id": 54545, - "tier": 5, - "type": "mediumTank" - }, - "54785": { - "is_premium": true, - "name": "SU-100Y", - "nation": "ussr", - "tank_id": 54785, - "tier": 6, - "type": "AT-SPG" - }, - "54801": { - "is_premium": true, - "name": "T-15", - "nation": "germany", - "tank_id": 54801, - "tier": 3, - "type": "lightTank" - }, - "54865": { - "is_premium": true, - "name": "Light Mk. VIC", - "nation": "uk", - "tank_id": 54865, - "tier": 2, - "type": "lightTank" - }, - "55057": { - "is_premium": true, - "name": "Pz.Kpfw. IV hydrostat.", - "nation": "germany", - "tank_id": 55057, - "tier": 5, - "type": "mediumTank" - }, - "55073": { - "is_premium": true, - "name": "T7 Combat Car", - "nation": "usa", - "tank_id": 55073, - "tier": 2, - "type": "lightTank" - }, - "55297": { - "is_premium": true, - "name": "SU-122-44", - "nation": "ussr", - "tank_id": 55297, - "tier": 7, - "type": "AT-SPG" - }, - "55313": { - "is_premium": true, - "name": "8,8 cm Pak 43 Jagdtiger", - "nation": "germany", - "tank_id": 55313, - "tier": 8, - "type": "AT-SPG" - }, - "55889": { - "is_premium": true, - "name": "Cromwell B", - "nation": "uk", - "tank_id": 55889, - "tier": 6, - "type": "mediumTank" - }, - "56097": { - "is_premium": true, - "name": "M4A3E8 Fury", - "nation": "usa", - "tank_id": 56097, - "tier": 6, - "type": "mediumTank" - }, - "56577": { - "is_premium": true, - "name": "LTP", - "nation": "ussr", - "tank_id": 56577, - "tier": 3, - "type": "lightTank" - }, - "56609": { - "is_premium": true, - "name": "T28 Concept", - "nation": "usa", - "tank_id": 56609, - "tier": 7, - "type": "AT-SPG" - }, - "57105": { - "is_premium": true, - "name": "Dicker Max", - "nation": "germany", - "tank_id": 57105, - "tier": 6, - "type": "AT-SPG" - }, - "57361": { - "is_premium": true, - "name": "Pz.Kpfw. IV Schmalturm", - "nation": "germany", - "tank_id": 57361, - "tier": 6, - "type": "mediumTank" - }, - "57617": { - "is_premium": true, - "name": "Panther/M10", - "nation": "germany", - "tank_id": 57617, - "tier": 7, - "type": "mediumTank" - }, - "58641": { - "is_premium": false, - "name": "VK 72.01 (K)", - "nation": "germany", - "tank_id": 58641, - "tier": 10, - "type": "heavyTank" - }, - "58881": { - "is_premium": true, - "name": "IS-5 (Object 730)", - "nation": "ussr", - "tank_id": 58881, - "tier": 8, - "type": "heavyTank" - }, - "59137": { - "is_premium": true, - "name": "IS-2 (1945)", - "nation": "ussr", - "tank_id": 59137, - "tier": 7, - "type": "heavyTank" - }, - "59649": { - "is_premium": true, - "name": "ISU-122S", - "nation": "ussr", - "tank_id": 59649, - "tier": 7, - "type": "AT-SPG" - }, - "59665": { - "is_premium": true, - "name": "Großtraktor - Krupp", - "nation": "germany", - "tank_id": 59665, - "tier": 3, - "type": "heavyTank" - }, - "59905": { - "is_premium": true, - "name": "T-54 first prototype", - "nation": "ussr", - "tank_id": 59905, - "tier": 8, - "type": "mediumTank" - }, - "60161": { - "is_premium": true, - "name": "IS-2Sh", - "nation": "ussr", - "tank_id": 60161, - "tier": 8, - "type": "heavyTank" - }, - "60177": { - "is_premium": true, - "name": "Panther mit 8,8 cm L/71", - "nation": "germany", - "tank_id": 60177, - "tier": 8, - "type": "mediumTank" - }, - "60417": { - "is_premium": true, - "name": "IS-3 Defender", - "nation": "ussr", - "tank_id": 60417, - "tier": 8, - "type": "heavyTank" - }, - "60929": { - "is_premium": true, - "name": "BT-7 artillery", - "nation": "ussr", - "tank_id": 60929, - "tier": 3, - "type": "lightTank" - }, - "62737": { - "is_premium": true, - "name": "leKpz M 41 90 mm", - "nation": "germany", - "tank_id": 62737, - "tier": 8, - "type": "lightTank" - }, - "62977": { - "is_premium": true, - "name": "T-44-100", - "nation": "ussr", - "tank_id": 62977, - "tier": 8, - "type": "mediumTank" - }, - "62993": { - "is_premium": true, - "name": "VK 45.03", - "nation": "germany", - "tank_id": 62993, - "tier": 7, - "type": "heavyTank" - }, - "63553": { - "is_premium": true, - "name": "AMX Chasseur de chars", - "nation": "france", - "tank_id": 63553, - "tier": 8, - "type": "mediumTank" - }, - "63585": { - "is_premium": true, - "name": "Tiger Kuromorimine SP", - "nation": "japan", - "tank_id": 63585, - "tier": 6, - "type": "heavyTank" - }, - "63601": { - "is_premium": true, - "name": "Dracula", - "nation": "other", - "tank_id": 63601, - "tier": 7, - "type": "mediumTank" - }, - "63841": { - "is_premium": true, - "name": "Panzer IV Ankou Special", - "nation": "japan", - "tank_id": 63841, - "tier": 5, - "type": "mediumTank" - }, - "64001": { - "is_premium": true, - "name": "T-34-85 Rudy", - "nation": "ussr", - "tank_id": 64001, - "tier": 7, - "type": "mediumTank" - }, - "64017": { - "is_premium": true, - "name": "Tankenstein", - "nation": "germany", - "tank_id": 64017, - "tier": 7, - "type": "heavyTank" - }, - "64065": { - "is_premium": true, - "name": "FCM 50 t", - "nation": "france", - "tank_id": 64065, - "tier": 8, - "type": "mediumTank" - }, - "64081": { - "is_premium": true, - "name": "Mk I* Heavy Tank", - "nation": "uk", - "tank_id": 64081, - "tier": 1, - "type": "heavyTank" - }, - "64257": { - "is_premium": true, - "name": "T-34-85 Victory", - "nation": "ussr", - "tank_id": 64257, - "tier": 6, - "type": "mediumTank" - }, - "64273": { - "is_premium": true, - "name": "Snowstorm Jagdtiger 8.8", - "nation": "germany", - "tank_id": 64273, - "tier": 8, - "type": "AT-SPG" - }, - "64337": { - "is_premium": true, - "name": "AC IV Sentinel", - "nation": "uk", - "tank_id": 64337, - "tier": 6, - "type": "mediumTank" - }, - "64529": { - "is_premium": true, - "name": "E 25", - "nation": "germany", - "tank_id": 64529, - "tier": 7, - "type": "AT-SPG" - }, - "64561": { - "is_premium": true, - "name": "112 Glacial", - "nation": "china", - "tank_id": 64561, - "tier": 8, - "type": "heavyTank" - }, - "64593": { - "is_premium": true, - "name": "Angry Connor", - "nation": "uk", - "tank_id": 64593, - "tier": 5, - "type": "AT-SPG" - }, - "64769": { - "is_premium": true, - "name": "IS-6 Fearless", - "nation": "ussr", - "tank_id": 64769, - "tier": 8, - "type": "heavyTank" - }, - "64785": { - "is_premium": false, - "name": "Pz.Kpfw. II", - "nation": "germany", - "tank_id": 64785, - "tier": 1, - "type": "lightTank" - }, - "64801": { - "is_premium": true, - "name": "T34 Independence", - "nation": "usa", - "tank_id": 64801, - "tier": 8, - "type": "heavyTank" - }, - "64849": { - "is_premium": true, - "name": "AC 1 Sentinel", - "nation": "uk", - "tank_id": 64849, - "tier": 4, - "type": "mediumTank" - }, - "65025": { - "is_premium": false, - "name": "MS-1 mod. 1", - "nation": "ussr", - "tank_id": 65025, - "tier": 1, - "type": "lightTank" - }, - "65041": { - "is_premium": false, - "name": "Leichttraktor", - "nation": "germany", - "tank_id": 65041, - "tier": 1, - "type": "lightTank" - }, - "65057": { - "is_premium": false, - "name": "T1 Cunningham", - "nation": "usa", - "tank_id": 65057, - "tier": 1, - "type": "lightTank" - }, - "65105": { - "is_premium": false, - "name": "Vickers Medium Mk. I", - "nation": "uk", - "tank_id": 65105, - "tier": 1, - "type": "mediumTank" - }, - "65281": { - "is_premium": false, - "name": "MS-1 mod. 1", - "nation": "ussr", - "tank_id": 65281, - "tier": 1, - "type": "lightTank" - }, - "65297": { - "is_premium": false, - "name": "Leichttraktor", - "nation": "germany", - "tank_id": 65297, - "tier": 1, - "type": "lightTank" - }, - "65313": { - "is_premium": false, - "name": "T1 Cunningham", - "nation": "usa", - "tank_id": 65313, - "tier": 1, - "type": "lightTank" - }, - "65329": { - "is_premium": true, - "name": "Type 62", - "nation": "china", - "tank_id": 65329, - "tier": 7, - "type": "lightTank" - }, - "65361": { - "is_premium": false, - "name": "Vickers Medium Mk. I", - "nation": "uk", - "tank_id": 65361, - "tier": 1, - "type": "mediumTank" - }, - "65377": { - "is_premium": true, - "name": "Type 3 Chi-Nu Kai", - "nation": "japan", - "tank_id": 65377, - "tier": 5, - "type": "mediumTank" - } - }, - "userStr": { - "1_test": "#germany_vehicles:1_test", - "1_test_short": "#germany_vehicles:1_test_short", - "85mm_D5T-85BML": "85 mm D-5T-85BML", - "8V135-2": "8V135-2", - "A-20": "A-20", - "A-20_mod_1938": "A-20 mod. 1938", - "A-20_mod_1941": "A-20 mod. 1941", - "A-20bis": "SP-3", - "A-32": "A-32", - "A-32_mod_1939": "A-32", - "A05_M4_105BD": "High Score", - "A05_M4_105BD_short": "High Score", - "A100_T49": "T49", - "A100_T49_Custom": "T49 Hornet", - "A100_T49_Custom_short": "T49 Hornet", - "A100_T49_Custom_short_v2": "T49 Guanlong", - "A100_T49_Custom_v2": "T49 Guanlong", - "A100_T49_short": "T49", - "A102_T28_concept": "T28 Concept", - "A102_T28_concept_Custom": "T28 HTC Independence", - "A102_T28_concept_Custom_short": "T28 HTC Independence", - "A102_T28_concept_short": "T28 HTC", - "A104_M4A3E8A": "M4A3E8 Fury", - "A104_M4A3E8A_descr": "The M4A3E8 Fury is the sergeant Wardaddy's tank from the movie Fury.", - "A104_M4A3E8A_short": "Fury", - "A111_T25_Pilot": "T25 Pilot Number 1", - "A111_T25_Pilot_Custom": "T25 Intrepid", - "A111_T25_Pilot_Custom_short": "T25 Intrepid", - "A111_T25_Pilot_short": "T25 Pilot 1", - "A115_Chrysler_K": "Chrysler K", - "A115_Chrysler_K_Custom": "Chrysler K Tomahawk", - "A115_Chrysler_K_Custom_short": "Chrysler K Tomahawk", - "A115_Chrysler_K_short": "Chrysler K", - "A116_XM551": "XM551 Sheridan", - "A116_XM551_Custom": "Sheridan Sidewinder", - "A116_XM551_Custom_short": "Sheridan Sidewinder", - "A116_XM551_Exp": "XM551 Sheridan Missile", - "A116_XM551_Exp_short": "Sheridan Missile", - "A116_XM551_short": "Sheridan", - "A117_T26E5": "T26E5", - "A117_T26E5_Custom": "T26E5 Sheriff", - "A117_T26E5_Custom_short": "T26E5 Sheriff", - "A117_T26E5_short": "T26E5", - "A121_T26E3": "T26E3 Eagle 7", - "A121_T26E3_short": "T26E3 Eagle 7", - "A122_TS-5": "TS-5", - "A122_TS-5_Custom": "TS-5 Bison", - "A122_TS-5_Custom_short": "TS-5 Bison", - "A122_TS-5_short": "TS-5", - "A124_T54E2": "T54E2", - "A124_T54E2_Custom": "T54E2 Shark", - "A124_T54E2_Custom_short": "T54E2 Shark", - "A124_T54E2_short": "T54E2", - "A125_AEP_1": "AE Phase I", - "A125_AEP_1_short": "AE Phase I", - "A128_Concept_1b": "Concept 1B", - "A128_Concept_1b_Custom": "Concept 1B Thunderbird", - "A128_Concept_1b_Custom_short": "Concept 1B Thunderbird", - "A128_Concept_1b_short": "Concept 1B", - "A130_Super_Hellcat": "Super Hellcat", - "A130_Super_Hellcat_Custom": "Super Hellcat Delta", - "A130_Super_Hellcat_Custom_short": "Super Hellcat Delta", - "A130_Super_Hellcat_short": "Super Hellcat", - "A132_T77": "T77", - "A132_T77_Custom": "T77 Cannonball", - "A132_T77_Custom_short": "T77 Cannonball", - "A132_T77_short": "T77", - "A136_T42": "T42", - "A136_T42_short": "T42", - "A140_ASTRON_REX_105": "ASTRON Rex 105 mm", - "A140_ASTRON_REX_105_Custom": "ASTRON Rex Ayame", - "A140_ASTRON_REX_105_Custom_short": "ASTRON Rex Ayame", - "A140_ASTRON_REX_105_short": "ASTRON Rex", - "A141_M-4-Y": "M-IV-Y", - "A141_M-4-Y_Custom": "M-IV-Y Stalker", - "A141_M-4-Y_Custom_short": "M-IV-Y Stalker", - "A141_M-4-Y_short": "M-IV-Y", - "A142_Pawlack_Tank": "Pawlack Tank", - "A142_Pawlack_Tank_short": "Pawlack Tank", - "A146_TL_7_120": "TL-7-120", - "A146_TL_7_120_Custom": "TL-7-120 Arachnid", - "A146_TL_7_120_Custom_short": "TL-7-120 Arachnid", - "A146_TL_7_120_short": "TL-7-120", - "A68_T28D": "T28 Defender", - "A68_T28D_Custom": "Defender D-Gunner", - "A68_T28D_Custom_short": "Defender D-Gunner", - "A68_T28D_short": "T28 Defender", - "A77_T6M": "T6E1 Grizzly", - "A77_T6M_short": "T6E1 Grizzly", - "A95_T95E6": "T95E6", - "A95_T95E6_Custom": "T95E6 Piranha", - "A95_T95E6_Custom_short": "T95E6 Piranha", - "A95_T95E6_short": "T95E6", - "A99_T92E1": "T92E1", - "A99_T92E1_Custom": "T92E1 Poison", - "A99_T92E1_Custom_short": "T92E1 Poison", - "A99_T92E1_short": "T92E1", - "AEC": "2x AEC", - "AEC3": "AEC3", - "AEC_A189": "AEC A189", - "AEC_A190": "AES A190", - "AEC_A1909": "AEC A190", - "AEC_Type_179": "AEC Type 179", - "AMO-3": "AMO-3", - "AMX38": "AMX 38", - "AMX38_short": "AMX 38", - "AMX40": "AMX 40", - "AMX40_short": "AMX 40", - "AMX50_Foch": "AMX 50 Foch", - "AMX50_Foch_Custom": "AMX 50 Foch Destroyer", - "AMX50_Foch_Custom_Short": "Foch Destroyer", - "AMX50_Foch_short": "Foch", - "AMX_12t": "AMX 12 t", - "AMX_12t_short": "AMX 12 t", - "AMX_13_75": "AMX 13 75", - "AMX_13_75_short": "AMX 13 75", - "AMX_13_90": "AMX 13 90", - "AMX_13_90_short": "AMX 13 90", - "AMX_50Fosh_155": "AMX 50 Foch (155)", - "AMX_50Fosh_155_short": "Foch 155", - "AMX_50_100": "AMX 50 100", - "AMX_50_100_Custom": "AMX 50 100 Basher", - "AMX_50_100_Custom_short": "AMX 50 100 Basher", - "AMX_50_100_short": "AMX 50 100", - "AMX_50_120": "AMX 50 120", - "AMX_50_120_short": "AMX 50 120", - "AMX_50_68t": "AMX 50 B", - "AMX_50_68t_Custom": "AMX 50 B Veteran", - "AMX_50_68t_Custom_short": "AMX 50 B Veteran", - "AMX_50_68t_short": "AMX 50 B", - "AMX_AC_Mle1946": "AMX AC mle. 46", - "AMX_AC_Mle1946_short": "AMX AC 46", - "AMX_AC_Mle1948": "AMX AC mle. 48", - "AMX_AC_Mle1948_short": "AMX AC 48", - "AMX_B_270": "AMX B 270", - "AMX_B_270BP": "AMXmas B 270", - "AMX_M4_1945": "AMX M4 mle. 45", - "AMX_M4_1945_short": "AMX M4 45", - "AMX_dracula": "Dracula", - "AMX_dracula_Custom": "Dracula Two-faced", - "AMX_dracula_Custom_short": "Dracula Two-faced", - "AMX_dracula_short": "Dracula", - "AOI-1490": "Continental AOI-1490", - "AOI-1490A": "Continental AOI-1490-A", - "AOI_695": "AOI-695", - "AOSI_1490-1": "Continental AOSI-1490-1", - "AOS_895": "AOS-895", - "APCNR": "Pzgr. 41 HK", - "APCNR.erz": "Pzgr. 41 StK", - "APCR_Type1A": "APCR Shell", - "APHE_Type1A": "AP Shell", - "APHE_Type1_TKA": "AP Shell", - "APHE_Type1_TKB": "AP Shell", - "APHE_Type94C": "AP Shell", - "APHE_Type94D": "AP Shell", - "AP_M74_CN": "AP Shell", - "AP_MkA": "AP Shell", - "AP_MkB": "AP Shell", - "ARL_44": "ARL 44", - "ARL_44BP": "Eraser BP44", - "ARL_44BP_short": "Eraser BP44", - "ARL_44_short": "ARL 44", - "ARL_V39": "ARL V39", - "ARL_V39_short": "ARL V39", - "ASh_8": "Ash 8", - "AT-1": "AT-1", - "AT-1bis": "AT-1 Bis", - "AVCR_1100_1": "AVCR 1100 1", - "AVS-1195A": "AVS-1195A", - "AVS_1195": "AVS-1195", - "AV_1195": "AV-1195", - "AV_1790": "AV 1790", - "Achilles_FR": "Charles", - "Achilles_FR_Custom": "Charles the Gallant", - "Achilles_FR_Custom_short": "Charles the Gallant", - "Achilles_FR_short": "Charles", - "Aether_W-20": "Aether W-20", - "Akawara": "O-47", - "Akawara_Custom": "O-47 Phoenix", - "Akawara_Custom_short": "O-47 Phoenix", - "Akawara_short": "O-47", - "Alfa_RA1000I_C_A": "Alfa RA1000I C.A.", - "American_LFF_Corporation_V12": "American LaFrance 312A V-12", - "Armstrong-Siddeley": "Armstrong Siddeley", - "Armstrong_Siddeley": "Armstrong Siddeley V4", - "Arv-8": "Arv-8", - "Aster": "Aster", - "Aster_M": "Aster M", - "B-1bis_captured": "Pz.Kpfw. B2 740 (f)", - "B-1bis_captured_short": "Pz. B2", - "B1": "B1", - "B1_short": "B1", - "B2_740Fturm": "Pz.Kpfw. B2 740 (f)", - "B2ketten": "Pz.Kpfw. B2 740 (f)", - "BAMM_V12": "VAMM V12", - "BD-2": "BD-2", - "BDR_G1B": "BDR G1 B", - "BDR_G1B_short": "BDR G1 B", - "BDT-5A-engine": "Y5 T-34", - "BDT-5B-engine": "Y5 Firefly", - "BDT-5C-engine": "Y5 ELC bis", - "BMW": "BMW-Va", - "BT-2": "BT-2", - "BT-2_bis": "BT-5", - "BT-2_cylindrical": "BT-2", - "BT-2bis": "BT-5", - "BT-5": "BT-5", - "BT-5_cylindrical": "BT-7 mod. 1935", - "BT-5_cylindrical_short": "BT-7 mod. 1935", - "BT-7": "BT-7", - "BT-7_conic": "BT-7 mod. 1937", - "BT-7_conic_short": "BT-7 mod. 1937", - "BT-SV": "BT-SV", - "Bat_Chatillon25t": "Bat.-Châtillon 25 t", - "Bat_Chatillon25t_Custom": "Bat.-Châtillon 25 t Musketeer", - "Bat_Chatillon25t_Custom_short": "B-C 25 t Musketeer", - "Bat_Chatillon25t_Custom_short_v2": "B-C 25 t Runner", - "Bat_Chatillon25t_Custom_v2": "B-C 25 t Runner", - "Bat_Chatillon25t_short": "B-C 25 t", - "Bedford_Twin-Six": "Bedford Twin-Six", - "Berliet_MDP": "Berliet MDP", - "Bison_I": "Sturmpanzer I Bison", - "Bison_I_short": "Bison", - "Buick_series_60": "Buick Series 60", - "CH38_WZ113G-FT": "WZ-113G FT", - "CH38_WZ113G-FT_Custom_short_v2": "WZ-113G FT Cute Baby", - "CH38_WZ113G-FT_Custom_v2": "WZ-113G FT Cute Baby", - "CH38_WZ113G-FT_short": "WZ-113G FT", - "CH38_WZ113G_FT_Custom": "WZ-113G FT Lucky", - "CH38_WZ113G_FT_Custom_short": "WZ-113G FT Lucky", - "CKD_AXK": "ČKD AXK", - "CKD_AXL": "ČKD AXL", - "Cadillac_V8": "3xCadillacV8", - "Cannone_da_105_51_M68": "Cannone da 105/51 M68", - "Cannone_da_105_51_M68_short": "Cannone da 105/51 M68", - "Cannone_da_105_Rh_V1": "Cannone da 105 Rh V1", - "Cannone_da_105_Rh_V1_short": "Cannone da 105 Rh V1", - "Cannone_da_75_50_Skoda": "75 mm Cannone da 75/50 Skoda", - "Cannone_da_75_50_Skoda_short": "75 mm Cannone da 75/50 Skoda", - "Cannone_da_90_42": "Cannone da 90/42", - "Cannone_da_90_42_short": "Cannone da 90/42", - "Cannone_da_90_53": "Cannone da 90/53", - "Cannone_da_90_53_short": "Cannone da 90/53", - "Cannone_da_90_74": "Cannone da 90/74", - "Cannone_da_90_74_short": "Cannone da 90/74", - "Cannone_da_90_Rh": "Cannone da 90 Rh", - "Cannone_da_90_Rh_short": "Cannone da 90 Rh", - "Canon_de_105_court_mle_1934S_AC": "105 mm AC court mle. 1934 S", - "Canon_de_105_court_mle_1934S_AC_short": "105 mm AC court\\nmle. 1934 S", - "Canon_de_105_mle_1930_Schneider_AC": "105 mm AC mle. 1930 S", - "Canon_de_105_mle_1930_Schneider_AC_short": "105 mm AC\\nmle. 1930 S", - "Canon_de_90mm_Vo_930_m_s": "Canon de 90 mm Vo 930 m/s", - "Ch01_Type59": "Type 59", - "Ch01_Type59_Custom": "Type 59 Qinglong", - "Ch01_Type59_Custom_short": "Type 59 Qinglong", - "Ch02_Type62": "Type 62", - "Ch02_Type62_Dragon": "Type 62 Dragon", - "Ch03_WZ-111": "WZ-111", - "Ch04_T34_1": "T-34-1", - "Ch05_T34_2": "T-34-2", - "Ch06_Renault_NC31": "Renault NC-31", - "Ch06_Renault_NC31_short": "NC-31", - "Ch07_Vickers_MkE_Type_BT26": "Vickers Mk. E Type B", - "Ch07_Vickers_MkE_Type_BT26_short": "VAE Type B", - "Ch08_Type97_Chi_Ha": "Type 2597 Chi-Ha", - "Ch08_Type97_Chi_Ha_short": "Chi-Ha", - "Ch09_M5": "M5A1 Stuart", - "Ch10_IS2": "IS-2", - "Ch11_110": "WZ-110", - "Ch12_111_1_2_3": "WZ-111 model 1-4", - "Ch12_111_1_2_3_short": "WZ-111 1-4", - "Ch14_T34_3": "T-34-3", - "Ch14_T34_3_short": "T-34-3", - "Ch15_59_16": "59-16", - "Ch15_59_16_BP": "Wind", - "Ch15_59_16_BP_Custom": "Free Wind", - "Ch15_59_16_BP_Custom_short": "Free Wind", - "Ch15_59_16_BP_short": "Wind", - "Ch16_WZ_131": "WZ-131", - "Ch17_WZ131_1_WZ132": "WZ-132", - "Ch18_WZ-120": "WZ-120", - "Ch19_121": "WZ-121", - "Ch19_121_Custom": "WZ-121 Typhoon", - "Ch19_121_Custom_short": "WZ-121 Typhoon", - "Ch20_Type58": "Type 58", - "Ch21_T34": "Type T-34", - "Ch22_113": "WZ-113", - "Ch22_113_Custom": "WZ-113 Bronze Warrior", - "Ch22_113_Custom_short": "WZ-113 Bronze Warrior", - "Ch22_113_Custom_short_v2": "WZ-113 Nine-Colors", - "Ch22_113_Custom_v2": "WZ-113 Nine-Colors", - "Ch23_112": "112 Glacial", - "Ch24_Type64": "Type 64", - "Ch24_Type64_short": "Type 64", - "Ch25_121_mod_1971B": "121B", - "Ch25_121_mod_1971B_short": "121B", - "Ch26_59_Patton": "59-Patton", - "Ch26_59_Patton_Custom": "59-Patton Aratinga", - "Ch26_59_Patton_Custom_short": "59-Patton Aratinga", - "Ch26_59_Patton_short": "59-Patton", - "Ch28_WZ_132A": "WZ-132A", - "Ch28_WZ_132A_short": "WZ-132A", - "Ch29_Type_62C_prot": "WZ-132-1", - "Ch29_Type_62C_prot_Custom": "WZ-132-1 Futuro", - "Ch29_Type_62C_prot_Custom_short": "WZ-132-1 Futuro", - "Ch29_Type_62C_prot_short": "WZ-132-1", - "Ch34_WZ-131G_FT": "WZ-131G FT", - "Ch34_WZ-131G_FT_BP": "Panlong", - "Ch34_WZ-131G_FT_BP_Custom": "Panlong Immortal", - "Ch34_WZ-131G_FT_BP_Custom_short": "Panlong Immortal", - "Ch34_WZ-131G_FT_BP_short": "Panlong", - "Ch34_WZ-131G_FT_short": "WZ-131G FT", - "Ch35_T-34-2G_FT": "T-34-2G FT", - "Ch35_T-34-2G_FT_short": "T-34-2G FT", - "Ch36_WZ-111-1G_FT": "WZ-111-1G FT", - "Ch36_WZ-111-1G_FT_short": "WZ-111-1G FT", - "Ch37_WZ-111G_FT": "WZ-111G FT", - "Ch37_WZ-111G_FT_short": "WZ-111G FT", - "Ch39_WZ120_1G_FT": "WZ-120-1G FT", - "Ch39_WZ120_1G_FT_Custom": "WZ-120-1 FT Enraged", - "Ch39_WZ120_1G_FT_Custom_short": "WZ-120-1 FT Enraged", - "Ch39_WZ120_1G_FT_Custom_short_v2": "WZ-120-1 FT Gongbi", - "Ch39_WZ120_1G_FT_Custom_v2": "WZ-120-1 FT Gongbi", - "Ch39_WZ120_1G_FT_short": "WZ-120-1G FT", - "Ch41_WZ_111_5A": "WZ-111 model 5A", - "Ch41_WZ_111_5A_Custom": "WZ-111 5A Serpent", - "Ch41_WZ_111_5A_Custom_short": "WZ-111 5A Serpent", - "Ch41_WZ_111_5A_short": "WZ-111 5A", - "Ch42_M41D": "M41D", - "Ch42_M41D_Custom": "M41D Jade", - "Ch42_M41D_Custom_short": "M41D Jade", - "Ch42_M41D_short": "M41D", - "Ch43_WZ-122_2": "WZ-122 TM", - "Ch43_WZ-122_2_short": "WZ-122 TM", - "Ch45_WZ_114": "WZ-114", - "Ch45_WZ_114_Custom": "WZ-114 Zhulong", - "Ch45_WZ_114_Custom_short": "WZ-114 Zhulong", - "Ch45_WZ_114_short": "WZ-114", - "Ch_LT_vz38": "LT vz. 38", - "Ch_LT_vz38_short": "LT vz. 38", - "Ch_WZ-112v2": "WZ-112-2", - "Ch_WZ-112v2_short": "WZ-112-2", - "Ch_WZ_112v2_Custom": "WZ-112-2 Celestial", - "Ch_WZ_112v2_Custom_short": "WZ-112-2 Celestial", - "Chassis1_A140_ASTRON_REX_105": "Rex", - "Chassis1_A140_ASTRON_REX_105_short": "Rex", - "Chassis1_Ch16_WZ_131": "WZ-131", - "Chassis1_Ch16_WZ_131_short": "WZ-131", - "Chassis1_Ch17_WZ131_1_WZ132": "WZ-131-1", - "Chassis1_Ch17_WZ131_1_WZ132_short": "WZ-131-1", - "Chassis1_Ch28_WZ_132A": "WZ-132A", - "Chassis1_Ch28_WZ_132A_short": "WZ-132A", - "Chassis1_Ch29_Type_62C_prot": "WZ-132-1", - "Chassis1_Ch29_Type_62C_prot_short": "WZ-132-1", - "Chassis1_Ch45_WZ_114": "WZ-114", - "Chassis1_Ch45_WZ_114_short": "WZ-114", - "Chassis1_F117_Alt_Proto_AMX_30": "AltProto AMX 30", - "Chassis1_F117_Alt_Proto_AMX_30_short": "A.P. AMX 30", - "Chassis1_GB09_Churchill_VII_BP": "Churchill W", - "Chassis1_GB09_Churchill_VII_BP_short": "Churchill W", - "Chassis1_It10_P43_bis_BP": "Shadowhunter", - "Chassis1_It10_P43_bis_BP_short": "Shadowhunter", - "Chassis1_It15_Semovente_M41": "Semovente M41", - "Chassis1_It15_Semovente_M41_short": "Semovente M41", - "Chassis1_It16_Bassoto": "Semovente M43", - "Chassis1_It16_Bassoto_short": "Semovente M43", - "Chassis1_It17_Semovente_mod_56": "CC mod. 56", - "Chassis1_It17_Semovente_mod_56_short": "CC mod. 56", - "Chassis1_It18_Semovente_mod_64": "CC-64 tipo A", - "Chassis1_It18_Semovente_mod_64_short": "CC-64 tipo A", - "Chassis1_It19_Controcarro_1MK2_Ciclope": "Controcarro 1 Mk. 2 tipo A", - "Chassis1_It19_Controcarro_1MK2_Ciclope_short": "CC 1 Mk. 2 tipo A", - "Chassis1_It22_Progetto_C45_mod_71": "Bisonte C45", - "Chassis1_It22_Progetto_C45_mod_71_short": "Bisonte C45", - "Chassis1_M24_Chaffee_BP": "Frosty", - "Chassis1_M24_Chaffee_BP_short": "Frosty", - "Chassis1_M6E2V2_BP": "Cyborg", - "Chassis1_M6E2V2_BP_short": "Cyborg", - "Chassis1_Oth34_PanzerMummy": "Royal Tread", - "Chassis1_Oth34_PanzerMummy_short": "Royal Tread", - "Chassis1_Oth35_PanzerMejai": "Desert Wanderer", - "Chassis1_Oth35_PanzerMejai_short": "Desert Wanderer", - "Chassis1_R119_Object_777": "Object 777 Version II", - "Chassis1_R119_Object_777_short": "Obj. 777 II", - "Chassis1_S31_Strv_K": "Stridsvagn KRV", - "Chassis1_S31_Strv_K_short": "Stridsvagn KRV", - "Chassis1_S32_Bofors_Tornvagn": "Bofors Tornvagn", - "Chassis1_S32_Bofors_Tornvagn_short": "Tornvagn", - "Chassis2_Ch16_WZ_131": "WZ-131 model 2", - "Chassis2_Ch16_WZ_131_short": "WZ-131 model 2", - "Chassis2_Ch17_WZ131_1_WZ132": "WZ-132", - "Chassis2_Ch17_WZ131_1_WZ132_short": "WZ-132", - "Chassis2_Ch28_WZ_132A": "WZ-132B", - "Chassis2_Ch28_WZ_132A_short": "WZ-132B", - "Chassis2_It15_Semovente_M41": "Semovente M42", - "Chassis2_It15_Semovente_M41_short": "Semovente M42", - "Chassis2_It16_Bassoto": "Semovente M44", - "Chassis2_It16_Bassoto_short": "Semovente M44", - "Chassis2_It17_Semovente_mod_56": "CC mod. 57", - "Chassis2_It17_Semovente_mod_56_short": "CC mod. 57", - "Chassis2_It18_Semovente_mod_64": "CC-64 tipo B", - "Chassis2_It18_Semovente_mod_64_short": "CC-64 tipo B", - "Chassis2_It19_Controcarro_1MK2_Ciclope": "Controcarro 1 Mk. 2 tipo B", - "Chassis2_It19_Controcarro_1MK2_Ciclope_short": "CC 1 Mk. 2 tipo B", - "Chi_Ha": "Type 97 Chi-Ha", - "Chi_Ha_short": "Chi-Ha", - "Chi_He": "Type 1 Chi-He", - "Chi_He_short": "Chi-He", - "Chi_Ni": "Chi-Ni", - "Chi_Ni_short": "Chi-Ni", - "Chi_Nu": "Type 3 Chi-Nu", - "Chi_Nu_Kai": "Type 3 Chi-Nu Kai", - "Chi_Nu_Kai_short": "Chi-Nu Kai", - "Chi_Nu_short": "Chi-Nu", - "Chi_Ri": "Type 5 Chi-Ri", - "Chi_Ri_short": "Chi-Ri", - "Chi_To": "Type 4 Chi-To", - "Chi_To_short": "Chi-To", - "Chrysler_1200_HP": "Chrysler 1200 HP", - "Churchill_LL": "Churchill III", - "Churchll": "Churchill", - "Churchll_mk_III": "Churchill III", - "Continental_AOI-1194-1-2": "Continental AOI 1194-1-2", - "Continental_AOI-1195-1": "Continental AOI 1195-1", - "Continental_AOI-1195-1-2": "Continental AOI 1195-1-2", - "Continental_AOI-1195-2": "Continental AOI 1195-2", - "Continental_AOI-1195-5": "Continental AOI 1195-5", - "Continental_AOI-1195-5A": "Continental AOI-1195-5A", - "Continental_AOI-1195-5_short": "Continental AOI 1195-5", - "Continental_AOI-1195-6": "Continental AOI 1195-6", - "Continental_AOI-628-1": "Continental AOI-628-1", - "Continental_AOI-628-1_short": "Continental\\nAOI-628-1", - "Continental_AOI-628-2": "Continental AOI-628-2", - "Continental_AOI-628-2A": "Continental AOI-628-2A", - "Continental_AOI-628-2_short": "Continental\\nAOI-628-2", - "Continental_AOI_1490": "Continental AOI-1490", - "Continental_AOI_1490M": "Continental AOI-1490-M", - "Continental_AOI_1490_1": "Continental AOI-1490-1", - "Continental_AOS-895-1": "Continental AOS-895-1", - "Continental_AOS-895-1_short": "Continental\\nAOS-895-1", - "Continental_AOS-895-3": "AN/GRC-8", - "Continental_AOS-895-3M": "Continental AOS-895-3M", - "Continental_AOS-895-3V2": "Continental AOS-895-3-2", - "Continental_AOS-895-3_short": "AN/GRC-8", - "Continental_AOSI-1195-1": "Continental AOSI 1195-1", - "Continental_AOSI-1195-1A": "Continental AOSI-1195-1A", - "Continental_AOSI-1195-5": "Continental AOSI-1195-5", - "Continental_AOSI-895-5": "Continental AOSI-895-5", - "Continental_AOSI-895-5FR": "Continental AOSI-895-5", - "Continental_AOSI-895-5_short": "Continental\\nAOSI-895-5", - "Continental_AOSI_1195_5A": "Continental AOSI-1195-5 Y IV", - "Continental_AOSI_895_5_Prem": "Continental AOSI-895-5", - "Continental_AV-1790-1": "Continental AV-1790-1", - "Continental_AV-1790-1A": "Continental AV-1790-1", - "Continental_AV-1790-1M2": "Continental AV-1790-5A", - "Continental_AV-1790-1M2_short": "Continental\\nAV-1790-5A", - "Continental_AV-1790-1_short": "Continental\\nAV-1790-1", - "Continental_AV-1790-3": "Continental AV-1790-3", - "Continental_AV-1790-3A1": "Continental AV-1790-3A1", - "Continental_AV-1790-3A1_short": "Continental\\nAV-1790-3A1", - "Continental_AV-1790-3_short": "Continental\\nAV-1790-3", - "Continental_AV-1790-5": "Continental AV-1790-5", - "Continental_AV-1790-5B": "Continental AV-1790-5B", - "Continental_AV-1790-5B2": "Continental AV-1790-5B2", - "Continental_AV-1790-5B_short": "Continental\\nAV-1790-5B", - "Continental_AV-1790-7B": "Continental AV-1790-7B", - "Continental_AV-1790-7B_short": "Continental\\nAV-1790-7B", - "Continental_AV-1790-7C": "Continental AV-1790-7C", - "Continental_AVDS-1790-2": "Continental AVDS-1790-2", - "Continental_AVDS-1790-2_short": "Continental\\nAVDS-1790-2", - "Continental_AVI_1790_8": "Continental AVI-1790-8", - "Continental_AV_1790_5B": "Continental AV-1790-5B", - "Continental_AV_1790_5C": "Continental AV-1790-5C", - "Continental_AV_1790_5C_short": "Continental\\nAV-1790-5C", - "Continental_R-670-5": "Continental R-670-5", - "Continental_R-670-5_short": "Continental\\nR-670-5", - "Continental_R-975-C1": "Continental R-975-C1", - "Continental_R-975-C1B": "Continental R-975-C1/41", - "Continental_R-975-C1B_short": "Continental R-975-C1/41", - "Continental_R-975-C1_short": "Continental\\nR-975-C1", - "Continental_R670-7": "Continental R670-7", - "Continental_R670-7_short": "Continental\\nR670-7", - "Continental_W-670": "Continental W-670", - "Continental_W-670-9A": "Continental W-670", - "Continental_W-670-9A_short": "Continental\\nW-670", - "Continental_W-670_LL": "Continental W-670", - "Continental_W-670_short": "Continental\\nW-670", - "Continental_W670-7": "Continental W-670-7", - "Continental_W670-7_short": "Continental\\nW-670-7", - "Cromwell_lion": "Cromwell Football", - "Cromwell_lion_short": "Cromwell Football", - "Crysler_A57": "Chrysler A57", - "Cuiberson_T-10204": "Guiberson T-10204", - "Cuiberson_T-10204_short": "Guiberson\\nT-10204", - "Cummins_VT8-460": "Cummins VT8-460", - "Cunningham": "Cunningham V-type", - "Cunningham_D1": "Cunningham D1", - "Cunningham_D2": "Cunningham D2", - "Cunningham_E1": "Cunningham E1", - "Cunningham_E2": "Cunningham E2", - "Cunningham_type_3": "Cunningham V8", - "Cz02_T50": "Škoda T 50", - "Cz02_T50_short": "Škoda T 50", - "Cz04_T50_51": "TVP T 50/51", - "Cz04_T50_51_Custom": "TVP 50/51 Instigator", - "Cz04_T50_51_Custom_short": "TVP 50/51 Instigator", - "Cz04_T50_51_short": "TVP T 50/51", - "Cz05_T34-100": "Konštrukta T-34/100", - "Cz05_T34-100_short": "T-34/100", - "Cz07_TVP_46": "TVP VTU Koncept", - "Cz07_TVP_46_short": "TVP VTU", - "Cz08_T-25": "Škoda T 25", - "Cz08_T-25BP": "Spark", - "Cz08_T-25BP_Custom": "Spark Extreme", - "Cz08_T-25BP_Custom_short": "Spark Extreme", - "Cz08_T-25BP_short": "Spark", - "Cz08_T-25_short": "Škoda T 25", - "Cz13_T-27": "Škoda T 27", - "Cz13_T-27_Custom": "Škoda T 27 Rampant", - "Cz13_T-27_Custom_short": "Škoda T 27 Rampant", - "Cz13_T-27_short": "Škoda T 27", - "Cz14_Skoda_T-56": "Škoda T 56", - "Cz14_Skoda_T-56_Custom": "Škoda T 56 Golem", - "Cz14_Skoda_T-56_Custom_short": "Škoda T 56 Golem", - "Cz14_Skoda_T-56_short": "Škoda T 56", - "Cz15_Skoda_T-45": "Škoda T 45", - "Cz15_Skoda_T-45_Custom": "Škoda T 45 Skvělý", - "Cz15_Skoda_T-45_Custom_short": "Škoda T 45 Skvělý", - "Cz15_Skoda_T-45_short": "Škoda T 45", - "Cz17_Vz_55": "Vz. 55", - "Cz17_Vz_55_short": "Vz. 55", - "D-744": "D-744", - "D1": "D1", - "D1_short": "D1", - "D2": "D2", - "D2_short": "D2", - "D37812": "D37812", - "D37812_LL": "D37812", - "D39273": "D39273", - "D51066": "D51066", - "D51072": "D51072", - "D51080": "D51080", - "DB_605": "DB 605", - "DB_605NY": "DB 605 A", - "DG": "DG", - "DMA-RD-620C2": "DMA RD-620C2", - "DW_II": "Durchbruchswagen 2", - "DW_II_short": "D.W. 2", - "Daimler-Benz_DB-603": "DB 603", - "Daimler-Benz_DB-603A2": "MB 507", - "Daimler-Benz_DB-603A2_short": "MB 507", - "Daimler-Benz_MB-809": "Daimler-Benz MB 809", - "Daimler-Benz_MB-809_short": "MB 809", - "Daimler_Knight_V6": "Daimler-Knight V6", - "Detroit_8V104D": "AMX Defender", - "Detroit_Diesel_6V53T": "Detroit Diesel 6V53T", - "Detroit_Diesel_6V53T-S": "Detroit Diesel 6V53T-S", - "Detroit_Diesel_6V53T-S_short": "Detroit Diesel\\n6V53T-S", - "Detroit_Diesel_6V53T_short": "Detroit Diesel\\n6V53T", - "Detroit_Diesel_8V71T": "Detroit Diesel 8V71T", - "DickerMax": "Dicker Max", - "DickerMax_short": "Dicker Max", - "DuckHeart": "DuckHeart", - "E-100": "E 100", - "E-100_Custom": "E 100 Stalwart", - "E-100_Custom_short": "E 100 Stalwart", - "E-100_Custom_short_v2": "E 100 Jötunn", - "E-100_Custom_v2": "E 100 Jötunn", - "E-100_short": "E 100", - "E-25": "E 25", - "E-25_Custom": "E 25 \\\"#1\\\"", - "E-50": "E 50", - "E-50_short": "E 50", - "E-75": "E 75", - "E-75_Custom": "E 75 Garm", - "E-75_Custom_short": "E 75 Garm", - "E-75_short": "E 75", - "E50_Ausf_M": "E 50 Ausf. M", - "E50_Ausf_M_Custom": "E 50 M Roaring", - "E50_Ausf_M_Custom_short": "E 50 M Roaring", - "E50_Ausf_M_Custom_short_v2": "E 50 M Monster", - "E50_Ausf_M_Custom_short_v3": "E 50 M Monster Evo.", - "E50_Ausf_M_Custom_v2": "E 50 Ausf. M Monster", - "E50_Ausf_M_Custom_v3": "E 50 M Monster Evo.", - "E50_Ausf_M_short": "E 50 M", - "ELC_AMX": "AMX ELC bis", - "ELC_AMX_short": "AMX ELC bis", - "E_9": "E-9", - "Engine_1_test": "#germany_vehicles:Engine_1_test", - "Engine_1_test_short": "#germany_vehicles:Engine_1_test_short", - "F01_Vickers_MkF": "Vickers Mk. F", - "F01_Vickers_MkF_short": "Vickers Mk. F", - "F113_PzV_FR": "Bretagne Panther", - "F113_PzV_FR_short": "Bretagne Panther", - "F114_Projet_4_1": "Char Futur 4", - "F114_Projet_4_1_short": "Char Futur 4", - "F116_Bat_Chat_Bourrasque": "Bat.-Châtillon Bourrasque", - "F116_Bat_Chat_Bourrasque_Custom": "Bourrasque Noir", - "F116_Bat_Chat_Bourrasque_Custom_short": "Bourrasque Noir", - "F116_Bat_Chat_Bourrasque_short": "B-C Bourrasque", - "F117_Alt_Proto_AMX_30": "AltProto AMX 30", - "F117_Alt_Proto_AMX_30_Custom": "A.P. AMX 30 Disruptor", - "F117_Alt_Proto_AMX_30_Custom_short": "A.P. AMX 30 Disruptor", - "F117_Alt_Proto_AMX_30_short": "A.P. AMX 30", - "F19_Lorraine40t": "Lorraine 40 t", - "F19_Lorraine40t_Com": "Lorraine 40 t Fearless", - "F19_Lorraine40t_Com_short": "Lorraine Fearless", - "F19_Lorraine40t_short": "Lorraine 40 t", - "F68_AMX_Chasseur_de_char_46": "AMX Chasseur de chars", - "F68_AMX_Chasseur_de_char_46_Custom": "AMX CDC Liberty", - "F68_AMX_Chasseur_de_char_46_Custom_short": "AMX CDC Liberty", - "F68_AMX_Chasseur_de_char_46_short": "AMX CDC", - "F69_AMX_13D": "AMX Defender", - "F69_AMX_13D_short": "AMX Defender", - "F69_AMX_13_57GF": "AMX 13 57", - "F69_AMX_13_57GF_short": "AMX 13 57", - "F71_AMX_30_prototype": "AMX 30 1er prototype", - "F71_AMX_30_prototype_Custom": "AMX 30 Win Peaks", - "F71_AMX_30_prototype_Custom_short": "AMX 30 Win Peaks", - "F71_AMX_30_prototype_short": "AMX 30 1er prot.", - "F72_AMX_30": "AMX 30 B", - "F72_AMX_30_Custom": "AMX 30 B Corsica", - "F72_AMX_30_Custom_short": "AMX 30 B Corsica", - "F72_AMX_30_short": "AMX 30 B", - "F73_M4A1_Revalorise": "M4A1 Revalorisé", - "F73_M4A1_Revalorise_short": "M4A1 Rev.", - "F74_AMX_M4_1949": "AMX M4 mle. 49", - "F74_AMX_M4_1949_short": "AMX M4 49", - "F75_Char_de_25t": "Bat.-Châtillon 25 t AP", - "F75_Char_de_25t_short": "B-C 25 t AP", - "F82_AMX_M4_Mle1949_Ter": "AMX M4 mle. 54", - "F82_AMX_M4_Mle1949_Ter_Custom": "AMX M4 54 Firebird", - "F82_AMX_M4_Mle1949_Ter_Custom_short": "AMX M4 54 Firebird", - "F82_AMX_M4_Mle1949_Ter_short": "AMX M4 mle. 54", - "F84_Somua_SM": "Somua SM", - "F84_Somua_SM_Custom": "Somua Grenadier", - "F84_Somua_SM_Custom_short": "Somua Grenadier", - "F84_Somua_SM_short": "Somua SM", - "F89_Canon_dassaut_105": "AMX Canon d'assaut 105", - "F89_Canon_dassaut_105_Custom": "CDA 105 Égalité", - "F89_Canon_dassaut_105_Custom_short": "CDA 105 Égalité", - "F89_Canon_dassaut_105_short": "AMX CDA 105", - "FCM50t_fwhite": "FCM 50 t Titi", - "FCM50t_fwhite_short": "FCM 50 t Titi", - "FCM50t_liliam": "FCM 50 t Zizou", - "FCM50t_liliam_short": "FCM 50 t Zizou", - "FCM_36Pak40": "FCM 36 Pak 40", - "FCM_36Pak40_short": "FCM36Pak40", - "FCM_50t": "FCM 50 t", - "FCM_50t_Custom": "FCM 50 t Twister", - "FCM_50t_Custom_short": "FCM 50 t Twister", - "FIAT_RA1050I_C_A": "FIAT RA1050I C.A.", - "F_AMX-12BP": "AMXmas", - "F_AMX-12BP_Custom": "AMXmas Avalanche", - "F_AMX-12BP_Custom_short": "AMXmas Avalanche", - "F_AMX-12BP_short": "AMXmas", - "Ferdinand": "Ferdinand", - "Ferdinandstandardketten": "Ferdinand", - "Ferdinandturm": "Ferdinand", - "Ferdinandvetstaerktketten": "Elefant", - "Fiat_V10": "Fiat V-10", - "Firefly_GuP": "Firefly Saunders SP", - "Firefly_GuP_short": "Firefly Saunders SP", - "First_april": "#other_vehicles:First_april", - "First_april_short": "#other_vehicles:First_april_short", - "Ford_GAA": "Ford GAA", - "Ford_GAA-3": "Christmas Spirit", - "Ford_GAA-4": "Ford GAA", - "Ford_GAA_2": "Ford GAA", - "Ford_GAA_FR": "Ford GAA", - "Ford_GAC": "Ford GAZ", - "Ford_GAC_1": "None", - "Ford_GAF": "Ford GAF", - "Ford_GAFM2": "Ford GAFM2A1", - "Ford_GAFM2A": "T28 Defender", - "Ford_GAFM4": "Ford GAFM2A2", - "Ford_GAF_500": "Ford GAF (500)", - "Ford_GAF_V8": "Ford GAF V8", - "Ford_GAN": "Ford GAN", - "Ford_GAN_BP": "Ford GAN", - "Ford_GAN_T": "Ford GAN No. 17", - "Ford_GAN_T23": "None", - "Ford_V8-221": "Ford V8-221", - "Ford_V8-239": "Ford V8-239", - "Frankengine": "Stein Type 1", - "Frankengine_short": "Stein Type 1", - "Franklin": "Franklin", - "G100_Gtraktor_Krupp": "Großtraktor - Krupp", - "G100_Gtraktor_Krupp_short": "Großtraktor", - "G103_RU_251": "Spähpanzer Ru 251", - "G103_RU_251_Custom": "Ru 251 Breakneck", - "G103_RU_251_Custom_short": "Ru 251 Breakneck", - "G103_RU_251_short": "Ru 251", - "G106_PzKpfwPanther_AusfF": "Panther mit 8,8 cm L/71", - "G106_PzKpfwPanther_AusfF_short": "Panther 8,8", - "G109_Steyr_WT": "Krupp-Steyr Waffenträger", - "G109_Steyr_WT_Custom": "Steyr WT Snakebite", - "G109_Steyr_WT_Custom_short": "Steyr WT Snakebite", - "G109_Steyr_WT_short": "Steyr WT", - "G110_Mauschen": "Mäuschen", - "G110_Mauschen_short": "Mäuschen", - "G112_JagdPanzer_105": "Kanonenjagdpanzer 105", - "G112_JagdPanzer_105_short": "Kanonenjagdpanzer", - "G113_SP_I_C": "Spähpanzer SP I C", - "G113_SP_I_C_short": "SP I C", - "G114_Rheinmetall_Scorpion": "Rheinmetall Skorpion G", - "G114_Rheinmetall_Scorpion_Custom": "Skorpion G Stinging", - "G114_Rheinmetall_Scorpion_Custom_short": "Skorpion G Stinging", - "G114_Rheinmetall_Scorpion_short": "Skorpion G", - "G115_VK10001P": "VK 100.01 (P)", - "G115_VK10001P_short": "VK 100.01 (P)", - "G118_VK4503": "VK 45.03", - "G119_Pz58_Mutz": "Panzer 58", - "G119_Pz58_Mutz_Custom": "Panzer 58 Mutz", - "G119_Pz58_Mutz_Custom_short": "Panzer 58 Mutz", - "G119_Pz58_Mutz_short": "Panzer 58", - "G120_M41_90": "leKpz M 41 90 mm", - "G120_M41_90_Custom": "M 41 90 Cruel Twist", - "G120_M41_90__Custom_short": "M 41 90 Cruel Twist", - "G120_M41_90_short": "leKpz M 41 90 mm", - "G121_Grille_15_L63": "Grille 15", - "G121_Grille_15_L63_Custom": "Grille 15 RA1DER", - "G121_Grille_15_L63_Custom_short": "Grille 15 RA1DER", - "G121_Grille_15_L63_Custom_short_v3": "Grille 15 Förster", - "G121_Grille_15_L63_Custom_v3": "Grille 15 Förster", - "G121_Grille_15_L63_short": "Grille 15", - "G136_Tiger_131": "Tiger 131", - "G136_Tiger_131_short": "Tiger 131", - "G138_VK16801P": "VK 168.01 (P)", - "G138_VK16801P_short": "VK 168.01 (P)", - "G140_HWK_30": "HWK 30", - "G140_HWK_30_short": "HWK 30", - "G142_M48RPz": "M48A2 Räumpanzer", - "G142_M48RPz_Custom": "Räumpanzer Rhino", - "G142_M48RPz_Custom_short": "Räumpanzer Rhino", - "G142_M48RPz_short": "M48A2 Räumpanzer", - "G144_Kpz_50t": "Kampfpanzer 50 t", - "G144_Kpz_50t_Custom": "Kpz 50 t König", - "G144_Kpz_50t_Custom_short": "Kpz 50 t König", - "G144_Kpz_50t_Custom_short_v2": "Kpz 50 t Jäger", - "G144_Kpz_50t_Custom_v2": "Kpz 50 t Jäger", - "G144_Kpz_50t_short": "Kpz 50 t", - "G147_mPanzer_64": "Kunze Panzer", - "G147_mPanzer_64_Custom": "Kunze Pz. Daredevil", - "G147_mPanzer_64_Custom_short": "Kunze Pz. Daredevil", - "G147_mPanzer_64_short": "Kunze Panzer", - "G20_Marder_II": "Marder II", - "G20_Marder_II_short": "Marder II", - "GAZ-11": "GAZ-11", - "GAZ-203": "GAZ-203", - "GAZ-203F": "GAZ-203F", - "GAZ-34-BT": "GAM-34-BT", - "GAZ-74B": "SU-85A", - "GAZ-74b": "SU-85B", - "GAZ-M1": "GAZ-M1", - "GAZ_M1": "GAZ-M1", - "GAZ_O": "GAZ-O", - "GAZ_OU": "GAZ-OU", - "GB01_Medium_Mark_I": "Vickers Medium Mk. I", - "GB01_Medium_Mark_I_short": "Medium I", - "GB03_Cruiser_Mk_I": "Cruiser Mk. I", - "GB03_Cruiser_Mk_I_short": "Cruiser I", - "GB03_Cruiser_Mk_I_tutorial_bot": "#uk_vehicles:GB03_Cruiser_Mk_I_tutorial_bot", - "GB03_Cruiser_Mk_I_tutorial_bot2": "#uk_vehicles:GB03_Cruiser_Mk_I_tutorial_bot2", - "GB03_Cruiser_Mk_I_tutorial_bot2_short": "#uk_vehicles:GB03_Cruiser_Mk_I_tutorial_bot2_short", - "GB03_Cruiser_Mk_I_tutorial_bot_short": "#uk_vehicles:GB03_Cruiser_Mk_I_tutorial_bot_short", - "GB04_Valentine": "Valentine Mk. IX", - "GB04_Valentine_short": "Valentine Mk. IX", - "GB05_Vickers_Medium_Mk_II": "Vickers Medium Mk. II", - "GB05_Vickers_Medium_Mk_II_short": "Medium II", - "GB06_Vickers_Medium_Mk_III": "Vickers Medium Mk. III", - "GB06_Vickers_Medium_Mk_III_short": "Medium III", - "GB07_Matilda": "Matilda", - "GB07_Matilda_short": "Matilda", - "GB08_Churchill_I": "Churchill I", - "GB08_Churchill_I_short": "Churchill I", - "GB09_Churchill_VII": "Churchill VII", - "GB09_Churchill_VII_BP": "Churchill W", - "GB09_Churchill_VII_BP_Custom": "Churchill W Defender", - "GB09_Churchill_VII_BP_Custom_short": "Churchill W Defender", - "GB09_Churchill_VII_BP_short": "Churchill W", - "GB109_GSOR_1008": "GSOR 1008", - "GB109_GSOR_1008_Custom": "GSOR 1008 Excalibur", - "GB109_GSOR_1008_Custom_short": "GSOR 1008 Excalibur", - "GB109_GSOR_1008_short": "GSOR 1008", - "GB10_Black_Prince": "Black Prince", - "GB10_Black_Prince_short": "Black Prince", - "GB112_Caliban": "Caliban", - "GB112_Caliban_Custom": "Caliban Hugo", - "GB112_Caliban_Custom_short": "Caliban Hugo", - "GB112_Caliban_short": "Caliban", - "GB11_Caernarvon": "Caernarvon", - "GB11_Caernarvon_short": "Caernarvon", - "GB12_Conqueror": "Conqueror", - "GB13_FV215b": "FV215b", - "GB13_FV215b_Custom": "FV215b Octopus", - "GB13_FV215b_Custom_short": "FV215b Octopus", - "GB13_FV215b_Custom_short_v2": "FV215b Octopus X", - "GB13_FV215b_Custom_v2": "FV215b Octopus X", - "GB19_Sherman_Firefly": "Sherman Firefly", - "GB19_Sherman_Firefly_short": "Sherman Firefly", - "GB20_Crusader": "Crusader", - "GB20_Crusader_short": "Crusader", - "GB21_A27BP": "Explorer", - "GB21_A27BP_short": "Explorer", - "GB21_Cromwell": "Cromwell", - "GB21_Cromwell_short": "Cromwell", - "GB22_Comet": "Comet", - "GB22_Comet_short": "Comet", - "GB23_Centurion": "Centurion Mk. I", - "GB23_Centurion_short": "Centurion I", - "GB24_Centurion_Mk3": "Centurion Mk. 7/1", - "GB24_Centurion_Mk3_short": "Centurion 7/1", - "GB32_Tortoise": "Tortoise", - "GB35_Sentinel_AC_IV": "AC IV Sentinel", - "GB35_Sentinel_AC_IV_short": "AC IV Sentinel", - "GB39_Universal_CarrierQF2": "Universal Carrier 2-pdr", - "GB39_Universal_CarrierQF2_short": "UC 2-pdr", - "GB40_Gun_Carrier_BP": "Dreadnought", - "GB40_Gun_Carrier_BP_Custom": "Dreadnought Lancelot", - "GB40_Gun_Carrier_BP_Custom_short": "Dreadnought Lancelot", - "GB40_Gun_Carrier_BP_short": "Dreadnought", - "GB40_Gun_Carrier_Churchill": "Churchill Gun Carrier", - "GB40_Gun_Carrier_Churchill_short": "Churchill GC", - "GB41_Challenger": "Challenger", - "GB41_Challenger_short": "Challenger", - "GB42_Valentine_AT": "Valentine AT", - "GB44_Archer_Custom": "Angry Connor", - "GB44_Archer_Custom_short": "Angry Connor", - "GB48_FV215b_183": "FV215b (183)", - "GB48_FV215b_183_Custom": "FV215b 183 Hammer", - "GB48_FV215b_183_Custom_short": "FV215b 183 Hammer", - "GB48_FV215b_183_short": "FV215b 183", - "GB51_Excelsior": "Excelsior", - "GB52_FV201_A45": "FV201 (A45)", - "GB52_FV201_A45_short": "FV201 (A45)", - "GB57_Alecto": "Alecto", - "GB58_Cruiser_Mk_III": "Cruiser Mk. III", - "GB58_Cruiser_Mk_III_short": "Cruiser III", - "GB59_Cruiser_Mk_IV": "Cruiser Mk. IV", - "GB59_Cruiser_Mk_IV_short": "Cruiser IV", - "GB60_Covenanter": "Covenanter", - "GB60_Covenanter_short": "Covenanter", - "GB63_TOG_II": "TOG II*", - "GB63_TOG_II_BP": "Blasteroid", - "GB63_TOG_II_BP_Custom": "Blasteroid Neo", - "GB63_TOG_II_BP_Custom_short": "Blasteroid Neo", - "GB63_TOG_II_BP_short": "Blasteroid", - "GB68_Matilda_Black_Prince": "Matilda Black Prince", - "GB68_Matilda_Black_Prince_short": "Matilda BP", - "GB69_Cruiser_Mk_II": "Cruiser Mk. II", - "GB69_Cruiser_Mk_II_short": "Cruiser II", - "GB70_FV4202_105": "FV4202", - "GB70_FV4202_105_Custom": "FV4202 Safari", - "GB70_FV4202_105_Custom_short": "FV4202 Safari", - "GB70_FV4202_105_short": "FV4202", - "GB71_AT_15A": "AT 15A", - "GB72_AT15": "AT 15", - "GB73_AT2": "AT 2", - "GB74_AT8": "AT 8", - "GB75_AT7": "AT 7", - "GB76_Mk_VIC": "Light Mk. VIC", - "GB76_Mk_VIC_short": "Light VIC", - "GB80_Charioteer": "Charioteer", - "GB80_Charioteer_short": "Charioteer", - "GB81_FV4004": "FV4004 Conway", - "GB81_FV4004_short": "Conway", - "GB83_4005": "FV4005", - "GB83_4005_Custom": "FV4005 Overkill", - "GB83_4005_Custom_short": "FV4005 Overkill", - "GB83_4005_short": "FV4005", - "GB84_Chieftain_Mk6": "Chieftain Mk. 6", - "GB84_Chieftain_Mk6_Custom": "Chieftain Mk. 6 Royal", - "GB84_Chieftain_Mk6_Custom_short": "Chieftain Mk. 6 Royal", - "GB84_Chieftain_Mk6_short": "Chieftain Mk. 6", - "GB85_Cromwell_Berlin": "Cromwell B", - "GB85_Cromwell_Berlin_Custom": "Cromwell B Berlin", - "GB85_Cromwell_Berlin_Custom_short": "Cromwell B Berlin", - "GB85_Cromwell_Berlin_short": "Cromwell B", - "GB87_Chieftain_T95_turret": "Chieftain/T95", - "GB87_Chieftain_T95_turret_Custom": "Chieftain/T95 Heritage", - "GB87_Chieftain_T95_turret_Custom_short": "Chieftain/T95 Heritage", - "GB87_Chieftain_T95_turret_short": "Chieftain", - "GB91_Super_Conqueror": "Super Conqueror", - "GB91_Super_Conqueror_Custom": "Super Conqueror Rex", - "GB91_Super_Conqueror_Custom_short": "Super Conqueror Rex", - "GB91_Super_Conqueror_short": "Super Conqueror", - "GB92_FV217": "FV217 Badger", - "GB92_FV217_Custom": "Badger, Esq.", - "GB92_FV217_Custom_short": "Badger, Esq.", - "GB92_FV217_short": "FV217 Badger", - "GB93_Caernarvon_AX": "Caernarvon Action X", - "GB93_Caernarvon_AX_Custom": "Action X Resolute", - "GB93_Caernarvon_AX_Custom_short": "Action X Resolute", - "GB93_Caernarvon_AX_short": "Action X", - "GB93_Caernarvon_AX_skin_Custom": "Action X Morpho", - "GB93_Caernarvon_AX_skin_Custom_short": "Action X Morpho", - "GB94_Centurion_Mk5-1_RAAC": "Centurion Mk. 5/1 RAAC", - "GB94_Centurion_Mk5-1_RAAC_Custom": "Centurion RAAC Dingo", - "GB94_Centurion_Mk5-1_RAAC_Custom_short": "Centurion RAAC Dingo", - "GB94_Centurion_Mk5-1_RAAC_short": "Centurion Mk. 5/1", - "GB97_Chimera": "Chimera", - "GB97_Chimera_Custom": "Chimera Phantom", - "GB97_Chimera_Custom_short": "Chimera Phantom", - "GB97_Chimera_short": "Chimera", - "GB99_Turtle_Mk1": "Turtle Mk. I", - "GB99_Turtle_Mk1_Custom": "Turtle Carapace", - "GB99_Turtle_Mk1_Custom_short": "Turtle Carapace", - "GB99_Turtle_Mk1_short": "Turtle Mk. I", - "GB_Centurion_MkDF": "Defender Mk. 1", - "GB_Centurion_MkDF_short": "Defender Mk. 1", - "GB_Churchill_MkVI": "Churchill Mk. VI", - "GB_Churchill_MkVIII": "Churchill VIII", - "GB_Churchill_MkVIII_short": "Churchill VIII", - "GB_Churchill_MkVI_short": "Churchill Mk. VI", - "GB_FV301": "FV301", - "GB_FV301_short": "FV301", - "GB_Mark_I": "Mk I* Heavy Tank", - "GB_Mark_I_short": "Mk I* Heavy Tank", - "GB_Sherman_V": "Sherman V", - "GB_Sherman_V_short": "Sherman V", - "GB_Vickers_Cruiser": "Vickers Cruiser", - "GB_Vickers_Cruiser_short": "Vickers CR", - "GB_Vickers_Light_105": "Vickers Light 105", - "GB_Vickers_Light_105_Custom": "Vickers L Valiant", - "GB_Vickers_Light_105_Custom_short": "Vickers L Valiant", - "GB_Vickers_Light_105_short": "Vickers Light", - "GMC": "GMC 6004", - "GMC_6-71_Mk10": "GMC 6004 6-71S", - "GMC_6-71_Mk11": "GMC 6004 6-71A", - "GMC_6004": "GMC 6004T", - "GMC_6046": "GMC 6046", - "GMC_6046A": "GMC 6046A", - "GMC_6046_BP": "GMC 6046B", - "GMC_Allison_V1710-E32": "GMC Allison V1710-E32", - "GMC_Allison_V1710-E32_short": "GMC Allison\\nV1710-E32", - "GMS-6046B": "GMS 6046B", - "GM_6-71_M": "GM 6062 6-71M", - "GM_DIESEL_P_V2": "GM Diesel P V2", - "GM_X-11": "GM X-11", - "GM_X-12": "GM X-12", - "GTD": "GTD", - "GT_101": "GT 101", - "G_E": "G.W. E 100", - "G_E-75RT": "Keiler", - "G_E-75RT_Custom": "Keiler Exterminator", - "G_E-75RT_Custom_short": "Keiler Exterminator", - "G_E-75RT_short": "Keiler", - "G_E-75TS": "E 75 TS", - "G_E-75TS_short": "E 75 TS", - "G_E_75TS_Custom": "E 75 TS Eagle", - "G_E_75TS_Custom_short": "E 75 TS Eagle", - "G_E_short": "G.W. E 100", - "G_KpfPz_70": "Kampfpanzer 70", - "G_KpfPz_70_Custom": "Kpz 70 Cavalry", - "G_KpfPz_70_Custom_short": "Kpz 70 Cavalry", - "G_KpfPz_70_short": "Kpz 70", - "G_Panther": "G.W. Panther", - "G_Panther_short": "G.W. Panther", - "G_Tiger": "G.W. Tiger", - "G_Tiger_short": "G.W. Tiger", - "G_VK3502_BP": "Barkhan", - "G_VK3502_BP_Custom": "Barkhan Expedition", - "G_VK3502_BP_Custom_short": "Barkhan Expedition", - "G_VK3502_BP_short": "Barkhan", - "G_VK9001P": "VK 90.01 (P)", - "G_VK9001P_Custom": "VK 90.01 (P) Blade", - "G_VK9001P_Custom_short": "VK 90.01 (P) Blade", - "G_VK9001P_short": "VK 90.01 (P)", - "General_Motors_6046": "General Motors 6046", - "General_Motors_6V53T": "General Motors 6V53T", - "General_Motors_6V53TA": "General Motors 6V53TA Exp.", - "Gipsy_Major_IIIA": "Gipsy Major IIIA", - "Grille": "Grille", - "Grille15_skin_Grille_Custom": "Grille 15 Troublemaker", - "Grille15_skin_Grille_Custom_short": "Grille 15 Troublemaker", - "Grille_mod_1": "Grille", - "Grille_short": "Grille", - "Grilleketten_ausf_A": "Grille", - "Grilleketten_ausf_B": "Grille verstärkteketten", - "Guiberson_T-1020-4": "Guiberson T-1020-4", - "Guiberson_T-1020-4_short": "Guiberson\\nT-1020-4", - "H35ketten": "Pz.Kpfw. 38H 735 (f)", - "H39_captured": "Pz.Kpfw. 38H 735 (f)", - "H39_captured_short": "Pz. 38H", - "H39turm": "Pz.Kpfw. 38H 735 (f)", - "HVS1": "HVS1", - "HVS2": "HVS2", - "HVSST31": "HVSS T31", - "HVSST35": "HVSS T35", - "HVSS_T48": "HVSS T48", - "HVSS_T51": "HVSS T51", - "Ha_Go": "Type 95 Ha-Go", - "Ha_Go_short": "Ha-Go", - "Hellsing_type1": "Helsing type1", - "Hellsing_type1_short": "Helsing type1", - "Hercules_HXE": "Hercules HXE", - "Hetzer": "Hetzer", - "Hetzer_0": "Jagdpanzer 38 (t)", - "Hetzer_Ausf_A": "Jagdpanzer 38 (t)", - "Hetzer_Ausf_B": "Jagdpanzer 38 (t) verstärkteketten", - "Hetzer_GuP": "Hetzer Kame SP", - "Hetzer_GuP_0": "Jagdpanzer 38 (t)", - "Hetzer_GuP_Ausf_A": "Jagdpanzer 38 (t)", - "Hetzer_GuP_short": "Hetzer Kame SP", - "Hetzer_short": "Hetzer", - "Hispano-Suiza_HS110": "Hispano-Suiza HS 110", - "Hispano-Suiza_HS110A": "Hispano-Suiza HS 110-2", - "Hispano-Suiza_HS110_A": "Hispano-Suiza HS 110 pré-série", - "Hotchkiss": "Hotchkiss", - "Hotchkiss_6_Zyl_Benzin_154_PS": "Hotchkiss 6-Zyl. Benzin 154 PS", - "Hotchkiss_6_Zyl_Benzin_164_PS": "Hotchkiss 6-Zyl. Benzin 164 PS", - "Hotchkiss_6_Zyl_Benzin_195_PS": "Hotchkiss 6-Zyl. Benzin 195 PS", - "Hummel": "Hummel", - "Hummel_0": "Hummel", - "Hummel_Ausf_1943": "Hummel", - "Hummel_Ausf_1944": "Hummel verstärkteketten", - "Hummel_short": "Hummel", - "ID36S_6V_CA": "ID36S 6V CA", - "IS": "IS", - "IS-1": "IS-1", - "IS-122": "IS-122", - "IS-2M": "IS-2M", - "IS-2Sh": "IS-2Sh", - "IS-2_GuP": "IS-2 Pravda SP", - "IS-2_GuP_short": "IS-2 Pravda SP", - "IS-3": "IS-3", - "IS-3M": "IS-3M", - "IS-3_Custom": "IS-3 Bear", - "IS-3_Custom_short": "IS-3 Bear", - "IS-3_K": "Kirovets-1", - "IS-4": "IS-4", - "IS-6": "IS-6", - "IS-6_Fearless": "IS-6 Fearless", - "IS-6_Fearless_short": "IS-6 Fearless", - "IS-7": "IS-7", - "IS-7_Custom": "IS-7 Battle-hardened", - "IS-7_Custom_short": "IS-7 Battle-hardened", - "IS-85": "IS-85", - "IS8": "IS-8", - "ISU-152": "ISU-152", - "ISU-152_01": "ISU-152", - "ISU-152_Custom_short_v2": "ISU-152 Tundra", - "ISU-152_Custom_v2": "ISU-152 Tundra", - "ISU-152_mod_1945": "ISU-152M", - "ISU152_Custom": "ISU-152 Hunter", - "ISU152_Custom_short": "ISU-152 Hunter", - "IS_2Sh_Custom": "IS-2Sh Imperturbable", - "IS_2Sh_Custom_short": "IS-2Sh Imperturbable", - "IS_4_Custom": "IS-4 Skilled", - "IS_4_Custom_short": "IS-4 Skilled", - "IS_6_Custom": "IS-6 Legend", - "IS_6_Custom_short": "IS-6 Legend", - "IS_Custom": "IS Steppe", - "IS_Custom_short": "IS Steppe", - "Indien_Panzer": "Indien-Panzer", - "Indien_Panzer_short": "Indien-Pz.", - "It08_Progetto_M40_mod65": "Progetto M40 mod. 65", - "It08_Progetto_M40_mod65_Custom": "Progetto 65 Gladiator", - "It08_Progetto_M40_mod65_Custom_short": "Progetto 65 Gladiator", - "It08_Progetto_M40_mod65_short": "Progetto 65", - "It09_P43_ter": "P.43 ter", - "It09_P43_ter_short": "P.43 ter", - "It10_P43_BD": "P.43/06 anniversario", - "It10_P43_BD_short": "P.43/06 ann.", - "It10_P43_bis": "P.43 bis", - "It10_P43_bis_BP": "Shadowhunter", - "It10_P43_bis_BP_Custom": "Shadowhunter Enchanted", - "It10_P43_bis_BP_Custom_short": "Shadowhunter Enchanted", - "It10_P43_bis_BP_short": "Shadowhunter", - "It10_P43_bis_short": "P.43 bis", - "It12_Prototipo_Standard_B": "Prototipo Standard B", - "It12_Prototipo_Standard_B_short": "Standard B", - "It13_Progetto_M35_mod_46": "Progetto M35 mod. 46", - "It13_Progetto_M35_mod_46_Custom": "Progetto 46 Reborn", - "It13_Progetto_M35_mod_46_Custom_short": "Progetto 46 Reborn", - "It13_Progetto_M35_mod_46_short": "Progetto 46", - "It14_P44_Pantera": "P.44 Pantera", - "It14_P44_Pantera_Custom": "P.44 Pantera Furious", - "It14_P44_Pantera_Custom_short": "P.44 Pantera Furious", - "It14_P44_Pantera_short": "P.44 Pantera", - "It15_Semovente_M41": "Semovente M41", - "It15_Semovente_M41_short": "Semovente M41", - "It16_Bassoto": "Semovente M43 Bassotto", - "It16_Bassoto_short": "Bassotto", - "It17_Semovente_mod_56": "Semovente Controcarro mod. 1956", - "It17_Semovente_mod_56_short": "SMV CC-56", - "It18_Semovente_mod_64": "Semovente Controcarro mod. 1964", - "It18_Semovente_mod_64_short": "SMV CC-64", - "It19_Controcarro_1MK2_Ciclope": "Controcarro 1 Mk. 2", - "It19_Controcarro_1MK2_Ciclope_short": "CC 1 Mk. 2", - "It20_Car_Comb_45t": "Carro da Combattimento 45t", - "It20_Car_Comb_45t_Custom": "Carro 45t Glitch", - "It20_Car_Comb_45t_Custom_short": "Carro 45t Glitch", - "It20_Car_Comb_45t_short": "Carro 45t", - "It21_Controcarro3_Minotauro": "Controcarro 3 Minotauro", - "It21_Controcarro3_Minotauro_Custom": "Minotauro Moderno", - "It21_Controcarro3_Minotauro_Custom_short": "Minotauro Moderno", - "It21_Controcarro3_Minotauro_short": "Minotauro", - "It22_Progetto_C45_mod_71": "Bisonte C45", - "It22_Progetto_C45_mod_71_Custom": "Bisonte Unbreakable", - "It22_Progetto_C45_mod_71_Custom_short": "Bisonte Unbreakable", - "It22_Progetto_C45_mod_71_short": "Bisonte C45", - "J12_Shinobi": "Shinobi", - "J12_Shinobi_short": "Shinobi", - "J20_Ho_Ri_type3": "Ho-Ri Type III", - "J20_Ho_Ri_type3_Custom": "Ho-Ri Samurai", - "J20_Ho_Ri_type3_Custom_short": "Ho-Ri Samurai", - "J20_Ho_Ri_type3_short": "Ho-Ri", - "J21_Ho_Ri": "Ho-Ri Type II", - "J21_Ho_Ri_short": "Ho-Ri T.II", - "J22_Ho_Ri_type96": "Ho-Ri Type I", - "J22_Ho_Ri_type96_short": "Ho-Ri T.I", - "J23_Chi_To_SPG": "Chi-To SPG", - "J23_Chi_To_SPG_short": "Chi-To SPG", - "J24_Type_57": "Type 57", - "J24_Type_57_Custom": "Type 57 Mecha", - "J24_Type_57_Custom_short": "Type 57 Mecha", - "J24_Type_57_short": "Type 57", - "J25_Type71": "Type 71", - "J25_Type71_Custom": "Type 71 Sankai", - "J25_Type71_Custom_short": "Type 71 Sankai", - "J25_Type71_short": "Type 71", - "J26_Type_68": "Type 68", - "J26_Type_68_short": "Type 68", - "J27_Chi_Se": "Chi-Se", - "J27_Chi_Se_short": "Chi-Se", - "J28_Ju_To": "Ju-To", - "J28_Ju_To_short": "Ju-To", - "J29_Ju_Nu": "Ju-Nu", - "J29_Ju_Nu_short": "Ju-Nu", - "J30_Mitsu_108": "Mitsu 108", - "J30_Mitsu_108_short": "Mitsu 108", - "J_V12_M": "American la France “J” V-12", - "JagdPanther": "Jagdpanther", - "JagdPantherII": "Jagdpanther II", - "JagdPantherII_short": "JPanther II", - "JagdPanther_short": "JPanther", - "JagdPzIV": "Jagdpanzer IV", - "JagdPzIV_0": "Jagdpanzer IV", - "JagdPzIV_AusfBP": "Kryos", - "JagdPzIV_AusfBP_Custom": "Kryos Survivor", - "JagdPzIV_AusfBP_Custom_short": "Kryos Survivor", - "JagdPzIV_AusfBP_short": "Kryos", - "JagdPzIV_Ausf_1940": "Jagdpanzer IV 0-Serie", - "JagdPzIV_Ausf_1943": "Jagdpanzer IV", - "JagdPzIV_short": "Jg.Pz. IV", - "JagdPz_E100": "Jagdpanzer E 100", - "JagdPz_E100_Custom": "Jagdpanzer Mighty", - "JagdPz_E100_Custom_short": "Jg.Pz. Mighty", - "JagdPz_E100_short": "Jg.Pz. E 100", - "JagdTiger": "Jagdtiger", - "JagdTiger_SdKfz_185": "8,8 cm Pak 43 Jagdtiger", - "JagdTiger_SdKfz_185_Snowstorm": "Snowstorm Jagdtiger 8.8", - "JagdTiger_SdKfz_185_Snowstorm_short": "JgTig.8,8 cm (2015)", - "JagdTiger_SdKfz_185_short": "JgTig.8,8 cm", - "JagdTiger_Standardketten": "Jagdtiger", - "JagdTiger_Vetstaerktketten": "Jagdtiger verstärkteketten", - "JagdTiger_turm": "Jagdtiger", - "JgPzPanther": "Jagdpanther", - "JgPzV_Standardketten": "Jagdpanther", - "JgPzV_Vetstaerktketten": "Jagdpanther verstärkteketten", - "K-91": "K-91", - "K-91_Custom": "K-91 Burn", - "K-91_Custom_short": "K-91 Burn", - "K-91_short": "K-91", - "KV": "KV", - "KV-13": "KV-13", - "KV-1s": "KV-1S", - "KV-1s-BP": "Thunder", - "KV-1s-BP_short": "Thunder", - "KV-1s_bis": "KV-1S Bis", - "KV-2": "KV-2", - "KV-220": "KV-220", - "KV-220-1": "KV-220 mod. 1", - "KV-220_mod": "T-220", - "KV-220_test": "KV-220 Beta-Test", - "KV-220_test_short": "KV-220 T", - "KV-3": "KV-3", - "KV-3bis": "KV-3 Bis", - "KV-5": "KV-5", - "KV1": "KV-1", - "KV1S_mod_42": "KV-1S mod. 1942", - "KV1S_mod_43": "KV-85", - "KV1_Custom": "KV-1 Ghost", - "KV1_Custom_short": "KV-1 Ghost", - "KV1s_bear": "KV-1S Football", - "KV1s_bear_short": "KV-1S Football", - "KV2": "KV-2", - "KV220": "KV-220", - "KV2_Custom": "KV-2 Menacing", - "KV2_Custom_short": "KV-2 Menacing", - "KV4": "KV-4", - "KV_1": "KV-1", - "KV_1_Custom": "KV-1 Warrior", - "KV_1_Custom_short": "KV-1 Warrior", - "KV_2": "KV-2", - "KZ-V8": "KZ V8", - "Kawasaki_Ha60_IB": "Kawasaki Ha60 IB", - "Kawasaki_Ha60_Ic": "Kawasaki Ha60 Ic", - "Kawasaki_Ha8": "Kawasaki Ha8", - "Kawasaki_Ha9_IIC": "Kawasaki Ha9 IIC", - "Kawasaki_Ha9_IID": "Kawasaki Ha9 IID", - "Ke_Ho": "Type 5 Ke-Ho", - "Ke_Ho_short": "Ke-Ho", - "Ke_Ni": "Type 98 Ke-Ni", - "Ke_Ni_B": "Type 98 Ke-Ni Otsu", - "Ke_Ni_B_short": "Ke-Ni Otsu", - "Ke_Ni_short": "Ke-Ni", - "Krupp-38D": "Krupp-38(D)", - "Krupp-38D_short": "Krupp-38(D)", - "Krupp_M301": "Krupp M301", - "Krupp_M305": "Krupp M305", - "Krupp_M311": "Krupp M311", - "LT-40_turm": "LT vz. 40", - "LTP": "LTP", - "Leichtertaktorketten": "Leichttraktor", - "LeichtertaktorkettenB": "Leichttraktor verstärkteketten", - "Leopard1": "Leopard 1", - "Leopard1_Custom": "Leopard 1 Wild", - "Leopard1_Custom_short": "Leopard 1 Wild", - "Leopard1_Custom_short_v2": "Leopard 1 Irbis", - "Leopard1_Custom_v2": "Leopard 1 Irbis", - "Leopard1_short": "Leopard 1", - "Leopardketten_Ausf_A": "VK 16.02 Leopard", - "Leopardketten_Ausf_B": "VK 16.02 Leopard verstärkteketten", - "Leopardturm": "VK 16.02 Leopard", - "Leyland": "2x Leyland E164", - "Leyland_L60_No_4": "Leyland L60 No. 4 Mk. 13A", - "Leyland_V6": "2x Leyland E165", - "Liberty": "Liberty", - "Licoming_0-435t": "Lycoming O-435T", - "Lowe": "Löwe", - "Lowe_Custom": "Löwe Reliant", - "Lowe_Custom_2": "Löwe Colossus", - "Lowe_Custom_short": "Löwe Reliant", - "Lowe_Custom_short_2": "Löwe Colossus", - "Ltraktor": "Leichttraktor", - "Ltraktor_short": "L.Tr.", - "Luchsketten_Ausf_A": "Luchs", - "Luchsketten_Ausf_B": "Luchs verstärkteketten", - "Lupus_chassis": "Lupus", - "Lupus_turret": "Lupus", - "M-17A": "M-17A", - "M-17E": "M-17E", - "M-17F": "M-17F", - "M-17FA": "M-17FO", - "M-17L": "M-17L", - "M-17T": "M-17T", - "M-3-Y": "M-III-Yoh", - "M-3-Y-Midas": "Scepter", - "M-3-Y-Midas_Custom": "Scepter Impeccable", - "M-3-Y-Midas_Custom_short": "Scepter Impeccable", - "M-3-Y-Midas_short": "Scepter", - "M-3-Y_short": "M-III-Yoh", - "M-5-400": "M-5-400", - "M-5-400F": "M-5-400F", - "M-5-400F_Bis": "M-5-400F Bis", - "M-5-Y": "M-V-Yoh", - "M-5-Y_short": "M-V-Yoh", - "M-50T": "M-50T", - "M-50TD": "M-50TD", - "M-50TV2": "M-50TV2", - "M-50TV2K": "M-50TV2K", - "M-6-Y": "M-VI-Yoh", - "M-6-Y_Custom": "M-VI-Yoh Pride", - "M-6-Y_Custom_short": "M-VI-Yoh Pride", - "M-6-Y_Custom_short_v2": "M-VI-Yoh Ankylosaurus", - "M-6-Y_Custom_v2": "M-VI-Yoh Ankylosaurus", - "M-6-Y_short": "M-VI-Yoh", - "M-7-Y": "M-VII-Yoh", - "M-7-Y_short": "M-VII-Yoh", - "M-80": "M-80", - "M103": "M103", - "M10_Wolverine": "M10 Wolverine", - "M10_Wolverine_short": "Wolverine", - "M12": "M12", - "M18E2BP": "BLTZ9000", - "M18E2BP_short": "BLTZ9000", - "M18_Hellcat": "M18 Hellcat", - "M18_Hellcat_Custom": "Hellcat Arracourt", - "M18_Hellcat_Custom_short": "Hellcat Arracourt", - "M18_Hellcat_short": "Hellcat", - "M22_Locust": "M22 Locust", - "M22_Locust_short": "Locust", - "M24A1": "M24A1", - "M24_Chaffee": "M24 Chaffee", - "M24_Chaffee_BP": "Frosty", - "M24_Chaffee_BP_Custom": "Frosty Jinglebell", - "M24_Chaffee_BP_Custom_short": "Frosty Jinglebell", - "M24_Chaffee_BP_short": "Frosty", - "M24_Chaffee_short": "Chaffee", - "M2D27810": "M2D27810", - "M2D27812": "M2D27812", - "M2M19": "M2M19", - "M2M7": "M2M7", - "M2_lt": "M2 Light Tank", - "M2_lt_short": "M2 Light", - "M2_med": "M2 Medium Tank", - "M2_med_short": "M2 Medium", - "M36_BP": "Enforcer", - "M36_BP_Custom": "Vandal", - "M36_BP_Custom_short": "Vandal", - "M36_BP_short": "Enforcer", - "M36_Slagger": "M36 Jackson", - "M36_Slagger_short": "Jackson", - "M36_Slagger_skin_volar": "M36 Valor", - "M36_Slagger_skin_volar_short": "M36 Valor", - "M37": "M37", - "M3_Grant": "M3 Lee", - "M3_S": "M3 S", - "M3_Stuart": "M3 Stuart", - "M3_Stuart_LL": "M3 Light", - "M40M43": "M40/M43", - "M41": "M41", - "M41_Bulldog": "M41 Walker Bulldog", - "M41_Bulldog_short": "M41 Bulldog", - "M44": "M5", - "M46_Patton": "M46 Patton", - "M46_Patton_short": "M46 Patton", - "M47": "M47", - "M48A1": "M48 Patton", - "M48A1_Body_Count_Custom": "M48 Patton Pit Bull", - "M48A1_Body_Count_Custom_short": "M48 Pit Bull", - "M48A1_Lucky_Forward_Custom": "M48 Patton Beast", - "M48A1_Lucky_Forward_Custom_short": "M48 Beast", - "M48A1_short": "M48 Patton", - "M4A2E4": "M4A2E4 Sherman", - "M4A2E4_short": "M4A2E4", - "M4A3E8TU": "Sherman Easy 8", - "M4A3E8TU_short": "Sherman Easy 8", - "M4A3E8_BP": "Rudolph", - "M4A3E8_BP_short": "Rudolph", - "M4A3E8_Sherman": "M4A3E8 Sherman", - "M4A3E8_Sherman_Custom": "M4A3E8 Shrieking", - "M4A3E8_Sherman_Custom_short": "M4A3E8 Shrieking", - "M4A3E8_Sherman_short": "M4A3E8", - "M4_Sherman": "M4 Sherman", - "M4_Sherman_Custom": "M4 Sherman Unity", - "M4_Sherman_Custom_short": "M4 Sherman Unity", - "M4_Sherman_short": "M4 Sherman", - "M5_Stuart": "M5 Stuart", - "M5_Stuart_short": "M5 Stuart", - "M6": "M6", - "M60": "M60", - "M62T80": "T23E3", - "M6A2E1": "M6A2E1", - "M6A2E1_exp": "M6A2E1 EXP", - "M6A2E1_exp_short": "M6A2E1 EXP", - "M6D1": "M6D1", - "M6D2": "M6D2", - "M6E2BP": "Magnus", - "M6E2BP_short": "Magnus", - "M6E2V2_BP": "Cyborg", - "M6E2V2_BP_Custom": "Cyborg T-800", - "M6E2V2_BP_Custom_short": "Cyborg T-800", - "M6E2V2_BP_short": "Cyborg", - "M7": "M5A1", - "M7D1": "M7D1", - "M7_Priest": "M7 Priest", - "M7_med": "M7", - "M7_med_short": "M7", - "M8A1": "M8A1", - "MATHIS": "MATHIS", - "MB-503": "Daimler-Benz MB 503", - "MB-503_short": "MB 503", - "MB-837": "MTU MB 837", - "MB-837-Aa": "MB 837 Aa", - "MB-837-Aa_A": "MB 837 Aa", - "MB-837-Ba-600": "MB 837 Ea 600", - "MB-837-Ea-600": "MB 837 Ea 600", - "MB-837A": "MTU MB 837 A", - "MB-837A_short": "MB 837 A", - "MB-837M": "MB 837 M", - "MB-838_CAM-500": "MTU MB 838 CaM 500", - "MB-838_CAM-500_1": "MTU MB 838 CaM 500A", - "MB-838_CAM-500_1_short": "MB 838 CaM 500A", - "MB-838_CAM-500_short": "MB 838 CaM 500", - "MB507": "DB 603 A2", - "MB517": "MB 517", - "MB_822_Ka_500": "MB 822 Ka-500", - "MB_837_Aa": "MB 837 Aa", - "MB_837_Ea": "MB 837 Ea", - "MB_837_F": "MB 837 F", - "MB_837_K": "MB 837 K", - "MS-1": "MS-1 mod. 1", - "MS-1obr21": "MS-1", - "MS-1obr21_Custom": "MS-1 1926", - "MS-1obr21_Custom_short": "MS-1 1926", - "MS-1obr21_short": "MS-1", - "MT-5-1": "MT-5-1", - "MT-50TM": "MT-50TM", - "MT25": "MT-25", - "MTLS-1G14": "MTLS-1G14", - "MTLS_D1": "MTLS D1", - "MTLS_T1": "MTLS T1", - "MTU_870": "MTU 870", - "MTU_870M": "MTU 870M", - "MTU_970": "MTU 970", - "MTU_970M": "MTU 970M", - "MTU_MB-838_Ca_M-500": "MTU MB 838 CaM 500B", - "MT_50_T": "MT-50T", - "M_40": "M-40", - "MarderII": "Marder II", - "MarderIIketten_ausf_B": "Marder II verstärkteketten", - "Marderturm": "Marder II", - "Mathis_300_exp": "Mathis 300-2", - "Matilda": "Matilda II", - "Matilda_II_LL": "Matilda IV", - "Maus": "Maus", - "Maus_short": "Maus", - "Maus_skin_Legion_Custom": "Maus Legionary", - "Maus_skin_Legion_Custom_short": "Maus Legionary", - "Maus_skin_Maus_Custom": "Maus 313", - "Maus_skin_Maus_Custom_short": "Maus 313", - "Mausketten": "Maus", - "Mausturm": "Maus", - "Maybach_HL100": "Maybach HL 100", - "Maybach_HL100V2": "Maybach HL 100 B", - "Maybach_HL45P": "Maybach HL 45 P", - "Maybach_HL57TR": "Maybach HL 57 TR", - "Maybach_HL62TR": "Maybach HL 62 TR", - "Maybach_HL62TRM": "Maybach HL 62 TRM", - "Maybach_HL66P": "Maybach HL 66 P", - "Maybach_HL66Px2": "2 x Maybach HL 66 P", - "Maybach_HL90": "Maybach HL 90", - "Maybach_HL_108TR": "Maybach HL 108 TR", - "Maybach_HL_108_TR_R": "Maybach HL 108 TR R", - "Maybach_HL_108x2": "Maybach HL 108x2", - "Maybach_HL_116": "Maybach HL 116", - "Maybach_HL_116_S": "Maybach HL 116 S", - "Maybach_HL_120": "Maybach HL 120", - "Maybach_HL_120BP": "Maybach HL 120", - "Maybach_HL_120_TR": "Maybach HL 120 TR", - "Maybach_HL_120_TRM": "Maybach HL 120 TRM", - "Maybach_HL_120_TRMB": "Maybach HL 120 TRM Ausf. A", - "Maybach_HL_120_TRM_112_R": "Maybach HL 120 TRM 112 R", - "Maybach_HL_120_TRM_A": "Maybach HL 120 TRM", - "Maybach_HL_120_TRM_BT": "Maybach HL 120 TRM", - "Maybach_HL_120_TRM_C": "Maybach HL 120 TRM V12", - "Maybach_HL_120_TRM_R": "Maybach HL 120 TRM R", - "Maybach_HL_120_TRM_RBP": "Maybach HL 120 TRM R", - "Maybach_HL_120_TR_A": "Maybach HL 120 TRM 112", - "Maybach_HL_120_TR_R": "Maybach HL 120 TR R", - "Maybach_HL_120_WH": "Dark Soul", - "Maybach_HL_150": "Maybach HL 150", - "Maybach_HL_150BP": "Maybach HL 150 B", - "Maybach_HL_157": "Maybach HL 157", - "Maybach_HL_157P": "Maybach HL 157 P", - "Maybach_HL_157_P": "Maybach HL 157 P", - "Maybach_HL_174": "Maybach HL 174", - "Maybach_HL_174_05": "Maybach HL 87", - "Maybach_HL_190": "Maybach HL 190", - "Maybach_HL_190_short": "Maybach HL 190", - "Maybach_HL_210P30": "Maybach HL 210 P30", - "Maybach_HL_210P30A": "Maybach HL 230 P30", - "Maybach_HL_210P30B": "Maybach HL 210 P30 B", - "Maybach_HL_210P30_05": "Maybach HL 110 P30", - "Maybach_HL_210P30_Ausf_A": "Maybach HL 230 P30", - "Maybach_HL_210_BP": "Maybach HL 210", - "Maybach_HL_210_TRM_P30": "Maybach HL 210 TRM P30", - "Maybach_HL_210_TRM_P30_A": "Maybach HL 210 TRM P30", - "Maybach_HL_210_TRM_P30_short": "Maybach\\nHL 210 TRM P30", - "Maybach_HL_210_TRM_P30v2": "Maybach HL 210 TRM P30 V2", - "Maybach_HL_210_TRM_P45": "Maybach HL 210 TRM P45", - "Maybach_HL_210_TRM_P45TU": "Maybach HL 210 TRM P45", - "Maybach_HL_210_TRM_P45_BP": "Maybach HL 210 TRM P45 B", - "Maybach_HL_210_TRM_P45_short": "Maybach HL 210\\nTRM P45", - "Maybach_HL_230": "Maybach HL 230", - "Maybach_HL_230P30A": "Maybach HL 230 P30", - "Maybach_HL_230P30A_Desert": "Flaming Heart", - "Maybach_HL_230P30_2500rpm": "Maybach HL 230 P30 2500 rpm", - "Maybach_HL_230P45": "Maybach HL 230 P45F", - "Maybach_HL_230P45C": "Maybach HL 230 P45C", - "Maybach_HL_230P45_05": "Maybach HL 130 P50", - "Maybach_HL_230P45_short": "Maybach HL 230\\nP45F", - "Maybach_HL_230_F": "Maybach HL 230 F", - "Maybach_HL_230_TRM_P30": "Maybach HL 230 TRM P30 pl.", - "Maybach_HL_230_TRM_P30V2": "Maybach HL 230 TRM P30 V2", - "Maybach_HL_230_TRM_P30_FR": "Maybach HL 230 TRM P30 fr.", - "Maybach_HL_230_TRM_P30_short": "Maybach\\nHL 230 TRM P30", - "Maybach_HL_230_TRM_P30v3": "Maybach HL 230 TRM P30 V3", - "Maybach_HL_230_TRM_P45": "Maybach HL 230 TRM P45", - "Maybach_HL_230_TRM_P45A": "Maybach HL 230 TRM P45 A", - "Maybach_HL_230_TRM_P45_short": "Maybach HL 230\\nTRM P45", - "Maybach_HL_234": "Maybach HL 234 TRM P45", - "Maybach_HL_234P30_Ausf_A": "Maybach HL 234 P30 Ausf. A", - "Maybach_HL_234P30_Ausf_HW": "Maybach HL 234 P30 Ausf. H", - "Maybach_HL_234P45": "Maybach HL 234 P45", - "Maybach_HL_234P45A": "Maybach V-12 ACC", - "Maybach_HL_234P45A_short": "Maybach V-12 ACC", - "Maybach_HL_234P45B": "Maybach HL 234 P45 B", - "Maybach_HL_234P45_Com": "Maybach HL 234 P45", - "Maybach_HL_234P45_short": "Maybach HL 234 P45", - "Maybach_HL_234P50": "Maybach HL 295 Ausf. A", - "Maybach_HL_234P60": "Maybach Neues Projekt", - "Maybach_HL_234_TRM_P30": "Maybach HL 234 TRM P30", - "Maybach_HL_234_TRM_P30_short": "Maybach\\nHL 234 TRM P30", - "Maybach_HL_234_TRM_P45_1": "Maybach HL 234 TRM P45", - "Maybach_HL_235": "Maybach HL 235", - "Maybach_HL_295": "Maybach HL 295", - "Maybach_HL_295_003_Somua": "Maybach HL 295.003 \\\"Somua\\\"", - "Maybach_HL_295_F": "Maybach HL 295 F", - "Maybach_HL_295_FA": "Maybach HL 295 F", - "Maybach_HL_45P": "Maybach HL 45 P", - "Maybach_NL38TR": "Maybach HL 38 TR", - "Meadows_DAV": "Meadows D.A.V.", - "Meadows_DAV_MkI": "Meadows D.A.V. O.C.", - "Meadows_ESTB": "Meadows E.S.T.L.", - "Meadows_HOP": "Meadows H.O.P.", - "Meadows_HOP_OC": "Meadows H.O.P. O.C.", - "Medous_MAT": "Meadows MAT", - "Meteor_Mark_IV_Mod_271": "Meteor Mark IV Mod 271", - "Meteorite_Mk_202": "Meteorite Mk. 202", - "Minkovsky_engine": "Minkovsky engine", - "Mitsubishi": "Mitsubishi A475VD", - "Mitsubishi_10ZF": "Mitsubishi 10ZF", - "Mitsubishi_12ZF": "Mitsubishi 12ZF", - "Mitsubishi_12ZM": "Mitsubishi 12ZM", - "Mitsubishi_12ZT": "Mitsubishi 12ZT", - "Mitsubishi_MT11": "Mitsubishi MT11", - "Mk.II_CS": "57mm Common Shell", - "Mod_T-10": "IS-3", - "Moteur_moderne_3M_27_1_01": "Moteur moderne 3M.27.1.01", - "N19Mk3": "WS №19 Mk.III", - "NC27": "Renault Otsu", - "NC27_short": "R. Otsu", - "NORINCO_12150L7": "NORINCO 12150L", - "NVSS_T66": "HVSS T66", - "NVSS_T80": "HVSS T80", - "Nashorn": "Nashorn", - "Nashorn_BP": "Epsilon", - "Nashorn_BP_Custom": "Epsilon Sagittarius", - "Nashorn_BP_Custom_short": "Epsilon Sagittarius", - "Nashorn_BP_short": "Epsilon", - "Nashorn_short": "Nashorn", - "Nuffield_liberty_MKII": "Nuffield Liberty Mk. II", - "Nuffield_liberty_MKIV": "Nuffield Liberty Mk. III", - "Nuffield_liberty_MKV": "Nuffield Liberty Mk. V", - "Nuffield_liberty_MKV_BP": "Nuffield Liberty Mk. VB", - "Nuffield_liberty_MKV_HW": "Nuffield Liberty Mk. VH", - "OQF_17pdr_Gun_Mk_IV_SH_SH": "OQF 17-pdr Gun Mk. IV SH-SH", - "OQF_17pdr_Gun_Mk_VII": "OQF 17-pdr Gun Mk. VII", - "OQF_17pdr_Gun_Mk_VIIG": "OQF 17-pdr Gun Mk. VII G", - "OQF_17pdr_Gun_Mk_VIIG_short": "OQF 17-pdr Gun Mk. VII G", - "OQF_17pdr_Gun_Mk_VIII": "OQF 17-pdr Gun Mk. VIII", - "OQF_17pdr_Gun_Mk_VIII_short": "OQF 17-pdr Gun Mk. VIII", - "OQF_17pdr_Gun_Mk_VII_BP": "OQF 17-pdr Gun Mk. VII", - "OQF_17pdr_Gun_Mk_VII_BP_short": "OQF 17-pdr Gun Mk. VII", - "OQF_3pdr_Gun": "OQF 3-pdr Gun Mk. II", - "OQF_55_pdr_AT_Gun": "OQF 55-pdr AT Gun", - "OQF_55_pdr_AT_Gun_short": "OQF 55-pdr AT Gun", - "OQF_77mm_AT_Gun_MkII": "OQF 77 mm AT Gun Mk. II", - "OQF_77mm_Gun_MkII": "OQF 77 mm Gun Mk. II", - "OQF_77mm_Gun_MkII_BP": "OQF 77 mm Gun Mk. II B", - "OQF_77mm_Gun_MkII_BP_short": "OQF 77 mm Gun Mk. II B", - "Object263": "Object 263", - "Object263_Custom": "Obj. 263 Graywolf", - "Object263_Custom_short": "Obj. 263 Graywolf", - "Object263_short": "Obj. 263", - "Object268": "Object 268", - "Object268_Custom": "Object 268 Chaser", - "Object268_Custom_short": "Obj. 268 Chaser", - "Object268_short": "Obj. 268", - "Object_140": "Object 140", - "Object_140_Custom": "Object 140 Terror", - "Object_140__Custom_short": "Object 140 Terror", - "Object_140_short": "Obj. 140", - "Object_212": "212A", - "Object_212_short": "212A", - "Object_261": "Object 261", - "Object_261_short": "Obj. 261", - "Object_704": "Object 704", - "Object_704_short": "Obj. 704", - "Object_907": "Object 907", - "Object_907_Custom": "Object 907 Ruthless", - "Object_907_Custom_short": "Object 907 Ruthless", - "Object_907_short": "Obj. 907", - "Observer": "Spectator", - "Odr_mle._1932A": "AP Shell", - "Odr_mle._1932B": "AP Shell", - "Odr_mle._1932C": "AP Shell", - "Odr_mle._1932D": "AP Shell", - "Odr_mle._1935A": "APCR Shell", - "Odr_mle._1935B": "APCR Shell", - "Odr_mle._1935C": "APCR Shell", - "Oe_mle._1932A": "HE Shell", - "Ordnance_QF_2pdr_AT_Gun_Mk.X": "QF Mk. IX", - "Ordnance_QF_2pdr_AT_Gun_Mk.X_short": "QF Mk. IX", - "Oth08_WH_Vindicator": "Vindicator Ultramarines", - "Oth08_WH_Vindicator_Dark_Angels_Custom": "Vindicator Dark Angels", - "Oth08_WH_Vindicator_Dark_Angels_Custom_short": "Vindicator DA", - "Oth08_WH_Vindicator_Imperial_Fists_Custom": "Vindicator Imperial Fists", - "Oth08_WH_Vindicator_Imperial_Fists_Custom_short": "Vindicator IF", - "Oth08_WH_Vindicator_short": "Vindicator UM", - "Oth09_WH_Predator": "Predator Ultramarines", - "Oth09_WH_Predator_Dark_Angels_Custom": "Predator Dark Angels", - "Oth09_WH_Predator_Dark_Angels_Custom_short": "Predator DA", - "Oth09_WH_Predator_Imperial_Fists_Custom": "Predator Imperial Fists", - "Oth09_WH_Predator_Imperial_Fists_Custom_short": "Predator IF", - "Oth09_WH_Predator_short": "Predator UM", - "Oth10_WarDuck": "WarDuck", - "Oth10_WarDuck_short": "WarDuck", - "Oth11_T-55A": "T 55A", - "Oth11_T-55A_short": "T 55A", - "Oth12_Sega_Hafen": "Hafen", - "Oth12_Sega_Hafen_short": "Hafen", - "Oth14_MdM_AshKnight": "Gravedigger", - "Oth14_MdM_AshKnight_short": "Gravedigger", - "Oth15_RustCrab": "Scavenger", - "Oth15_RustCrab_short": "Scavenger", - "Oth16_KV_TG": "Smasher", - "Oth16_KV_TG_short": "Smasher", - "Oth17_BDT-5A": "Y5 T-34", - "Oth17_BDT-5A_short": "Y5 T-34", - "Oth18_BDT-5B": "Y5 Firefly", - "Oth18_BDT-5B_short": "Y5 Firefly", - "Oth19_BDT-5C": "Y5 ELC bis", - "Oth19_BDT-5C_short": "Y5 ELC bis", - "Oth20_M4FL10": "M4/FL10", - "Oth20_M4FL10_short": "M4/FL10", - "Oth22_Sega_Vulcan": "Vulcan", - "Oth22_Sega_Vulcan_short": "Vulcan", - "Oth23_Werewolf": "Lycan", - "Oth23_Werewolf_Custom": "Lycan Knight", - "Oth23_Werewolf_Custom_short": "Lycan Knight", - "Oth23_Werewolf_short": "Lycan", - "Oth24_StuH_42HW": "Nightmare", - "Oth24_StuH_42HW_short": "Nightmare", - "Oth25_HWT20_FalconFire": "Annihilator", - "Oth25_HWT20_FalconFire_short": "Annihilator", - "Oth26_StrvM42HW": "Spike", - "Oth26_StrvM42HW_short": "Spike", - "Oth27_M24_AMX": "Titan T24 57", - "Oth27_M24_AMX_Custom": "Titan Flare", - "Oth27_M24_AMX_Custom_short": "Titan Flare", - "Oth27_M24_AMX_short": "Titan T24 57", - "Oth28_Sturmfeur_HW": "Titan H-Nd", - "Oth28_Sturmfeur_HW_short": "Titan H-Nd", - "Oth29_A15HW": "Titan Mk. I", - "Oth29_A15HW_short": "Titan Mk. I", - "Oth30_T54MS": "Titan-54d", - "Oth30_T54MS_short": "Titan-54d", - "Oth31_T150BP": "Titan-150", - "Oth31_T150BP_Custom": "Titan-150 Trailblazer", - "Oth31_T150BP_Custom_short": "Titan-150 Trailblazer", - "Oth31_T150BP_short": "Titan-150", - "Oth33_Blizzard_BP": "Rover", - "Oth33_Blizzard_BP_Custom": "Rover LunarX", - "Oth33_Blizzard_BP_Custom_short": "Rover LunarX", - "Oth33_Blizzard_BP_short": "Rover", - "Oth34_PanzerMummy": "Pharaoh", - "Oth34_PanzerMummy_Custom": "Pharaoh All-Seeing", - "Oth34_PanzerMummy_Custom_short": "Pharaoh All-Seeing", - "Oth34_PanzerMummy_short": "Pharaoh", - "Oth35_PanzerMejai": "Medjay", - "Oth35_PanzerMejai_short": "Medjay", - "PLInz_RWA": "PLInz R.W.A.", - "PLT_27": "PLT-27", - "Paccar_120_325HT": "Gas Boiler", - "Paccar_125-770HR": "Furnace", - "Panther_II": "Panther II", - "Panther_II_short": "Panther II", - "Panther_M10": "Panther/M10", - "Panther_M10_short": "Panther/M10", - "PanzerJager": "Panzerjäger I", - "PanzerJager_I": "Panzerjäger I", - "PanzerJager_I_short": "Pz.Jäg. I", - "Paxman_Ricardo": "Paxman 12TP", - "Paxman_Ricardo_BP": "Paxman 12TP", - "Pershing": "M26 Pershing", - "Pershing_short": "Pershing", - "Pershingchassis1": "M26T80E1", - "Pershingchassis2": "M26T81", - "Pl03_PzV_Pudel": "Pudel", - "Pl03_PzV_Pudel_short": "Pudel", - "Pl05_50TP_Tyszkiewicza": "50TP Tyszkiewicza", - "Pl05_50TP_Tyszkiewicza_short": "50TP Tyszkiewicza", - "Pl06_10TP": "10TP", - "Pl06_10TP_short": "10TP", - "Pl07_14TP": "14TP", - "Pl07_14TP_short": "14TP", - "Pl08_50TP_prot": "50TP prototyp", - "Pl08_50TP_prot_Custom": "50TP prototyp Hussar", - "Pl08_50TP_prot_Custom_short": "50TP prot. Hussar", - "Pl08_50TP_prot_short": "50TP prototyp", - "Pl10_40TP_Habicha": "40TP Habicha", - "Pl10_40TP_Habicha_short": "40TP Habicha", - "Pl119_CS_52_LIS": "CS-52 LIS", - "Pl119_CS_52_LIS_short": "CS-52 LIS", - "Pl11_45TP_Habicha": "45TP Habicha", - "Pl11_45TP_Habicha_short": "45TP Habicha", - "Pl13_53TP_Markowskiego": "53TP Markowskiego", - "Pl13_53TP_Markowskiego_short": "53TP Markowskiego", - "Pl15_60TP": "60TP Lewandowskiego", - "Pl15_60TP_Custom": "60TP Krzemień", - "Pl15_60TP_Custom_short": "60TP Krzemień", - "Pl15_60TP_short": "60TP Lewandowskiego", - "Porsche_16_Cyl_Y1": "Porsche 16 Cyl. Y", - "Porsche_16_Cyl_Y2": "Porsche Typ 212 NL", - "Porsche_Deutz_Typ180_2": "Porsche Deutz Typ 180/2", - "Porsche_Deutz_Typ188": "Porsche Deutz Typ 188", - "Porsche_Typ_100": "2x Porsche Typ 100", - "Porsche_Typ_100_1": "2x Porsche Typ 100/1", - "Porsche_Typ_100_1A": "2x Porsche Typ 100/1 Ausf. A", - "Porsche_Typ_100_3": "2x Porsche Typ 100/3", - "Porsche_Typ_205_2": "Porsche Typ 205/2", - "Porsche_Typ_220": "Porsche Typ 220", - "Praga_AXK": "Praga AXK", - "Praga_AusfIV": "Praga Ausf. IV", - "Praga_C9": "Praga C9", - "Praga_EPA_2": "Praga EPA/2", - "Praga_EPA_3": "Praga EPA/3", - "Praga_NR": "Praga NR", - "Praga_NRi": "Praga NRi", - "Praga_TNPS": "Praga TNPS", - "Praga_TN_100": "Praga TN 100", - "Pro_Ag_A": "Leopard Prototyp A", - "Pro_Ag_A_short": "Leopard PT A", - "Pz-V_Shmallturm": "Pz.Kpfw. Panther Schmalturm", - "Pz-V_Shmallturm_short": "Pz.Kpfw. Panther\\nSchmalturm", - "Pz-V_Standardketten": "Pz.Kpfw. Panther Ausf. A", - "Pz-V_Standardketten_short": "Pz.Kpfw. Panther\\nAusf. A", - "Pz-V_Standardturm": "Pz.Kpfw. Panther Ausf. G", - "Pz-V_Standardturm_short": "Pz.Kpfw. Panther\\nAusf. G", - "Pz-V_Vetstaerktketten": "Pz.Kpfw. Panther Ausf. G", - "Pz-V_Vetstaerktketten_short": "Pz.Kpfw. Panther\\nAusf. G", - "Pz35t": "Pz.Kpfw. 35 (t)", - "Pz35t_Standardketten": "Pz.Kpfw. 35 (t)", - "Pz35t_Standardketten_short": "Pz.Kpfw. 35 (t)", - "Pz35t_Standardturm": "Pz.Kpfw. 35 (t)", - "Pz35t_Standardturm_short": "Pz.Kpfw. 35 (t)", - "Pz35t_Verstaerktturm": "Pz.Kpfw. 35 (t) verstärkterturm", - "Pz35t_Verstaerktturm_short": "Pz.Kpfw. 35 (t)\\nverstärkterturm", - "Pz35t_Vetstaerktketten": "Pz.Kpfw. 35 (t) verstärkteketten", - "Pz35t_short": "Pz. 35 (t)", - "Pz38_NA": "Pz.Kpfw. 38 (t) n.A.", - "Pz38_NA_short": "Pz. 38 nA", - "Pz38t": "Pz.Kpfw. 38 (t)", - "Pz38t_Ausf_G_turm": "Pz.Kpfw. 38 (t) Ausf. G", - "Pz38t_Standardketten": "Pz.Kpfw. 38 (t) Ausf. E", - "Pz38t_Vetstaerktketten": "Pz.Kpfw. 38 (t) Ausf. G", - "Pz38t_short": "Pz. 38 (t)", - "PzII": "Pz.Kpfw. II", - "PzIII": "Pz.Kpfw. III", - "PzIII-IV": "Pz.Kpfw. III/IV", - "PzIII-IV_ausf_B": "Pz.Kpfw. III/IV verstärkteketten", - "PzIII_A": "Pz.Kpfw. III Ausf. A", - "PzIII_A_short": "Pz. III A", - "PzIII_Ausf_A": "Pz.Kpfw. III Ausf. A", - "PzIII_Ausf_A_short": "Pz.Kpfw. III\\nAusf. A", - "PzIII_Ausf_C": "Pz.Kpfw. III Ausf. E", - "PzIII_Ausf_C_short": "Pz.Kpfw. III\\nAusf. E", - "PzIII_Ausf_E": "Pz.Kpfw. III Ausf. F", - "PzIII_Ausf_E_short": "Pz.Kpfw. III\\nAusf. J", - "PzIII_Ausf_M": "Pz.Kpfw. III Ausf. L", - "PzIII_Ausf_M_short": "Pz.Kpfw. III\\nAusf. L", - "PzIII_IV": "Pz.Kpfw. III/IV", - "PzIII_IV_short": "Pz. III/IV", - "PzIII_short": "Pz. III", - "PzIIIketten_Ausf_A": "Pz.Kpfw. III Ausf. A", - "PzIIIketten_Ausf_A_short": "Pz.Kpfw. III\\nAusf. A", - "PzIIIketten_Ausf_B": "Pz.Kpfw. III Ausf. B", - "PzIIIketten_Ausf_B_short": "Pz.Kpfw. III\\nAusf. B", - "PzIIIketten_Ausf_J": "Pz.Kpfw. III Ausf. J", - "PzIIIketten_Ausf_J_short": "Pz.Kpfw. III\\nAusf. J", - "PzIIIketten_Ausf_L": "Pz.Kpfw. III Ausf. L", - "PzIIIketten_Ausf_L_short": "Pz.Kpfw. III\\nAusf. L", - "PzIIL_Grosseturm": "Luchs 5 cm", - "PzIIL_Luchsturm": "Luchs 2 cm", - "PzII_J": "Pz.Kpfw. II Ausf. J", - "PzII_J_short": "Pz. II J", - "PzII_Luchs": "Pz.Kpfw. II Luchs", - "PzII_Luchs_short": "Luchs", - "PzII_Luchsturm": "Pz.Kpfw. II Ausf. F", - "PzII_Luchsturm_short": "Pz.Kpfw. II Ausf. F", - "PzII_Standartturm": "Pz.Kpfw. II Ausf. b", - "PzII_Standartturm_short": "Pz.Kpfw. II Ausf. b", - "PzII_ausf_J": "Pz.Kpfw. II Ausf. J", - "PzII_ketten_Ausf_A": "Pz.Kpfw. II Ausf. A", - "PzII_ketten_Ausf_A_short": "Pz.Kpfw. II\\nAusf. A", - "PzII_ketten_Ausf_B": "Pz.Kpfw. II Ausf. F", - "PzII_ketten_Ausf_B_short": "Pz.Kpfw. II\\nAusf. F", - "PzII_short": "Pz. II", - "PzII_tutorial_bot2": "#germany_vehicles:PzII_tutorial_bot2", - "PzII_tutorial_bot2_short": "#germany_vehicles:PzII_tutorial_bot2_short", - "PzIIturm_ausf_J": "Pz.Kpfw. II Ausf. J", - "PzIV": "Pz.Kpfw. IV Ausf. G", - "PzIV_Ausf_1940": "Pz.Kpfw. IV Ausf. G", - "PzIV_Ausf_1942": "Pz.Kpfw. IV Ausf. H", - "PzIV_Ausf_1944": "Pz. IV Gargoyle", - "PzIV_Ausf_1944_short": "Pz. IV Gargoyle", - "PzIV_Hydro": "Pz.Kpfw. IV hydrostat.", - "PzIV_Hydro_short": "Pz. IV hydr.", - "PzIV_Schmalturm_A": "Pz.Kpfw. IV Schmalturm", - "PzIV_Shmallturm": "Pz.Kpfw. IV Ausf. H", - "PzIV_Shmallturm_short": "Pz.Kpfw. IV\\nAusf. H", - "PzIV_Standart": "Pz.Kpfw. IV Ausf. G", - "PzIV_Standart_short": "Pz.Kpfw. IV\\nAusf. G", - "PzIV_ausf43": "Pz.Kpfw. IV Schmalturm", - "PzIV_ausf_F": "Pz.Kpfw. III/IV Ausf. A", - "PzIV_ausf_F_short": "Pz.Kpfw. III/IV\\nAusf. A", - "PzIV_ausf_H": "Pz.Kpfw. III/IV Ausf. B", - "PzIV_ausf_H_short": "Pz.Kpfw. III/IV\\nAusf. B", - "PzIV_schmalturm": "Pz.Kpfw. IV Schmalturm", - "PzIV_schmalturm_Custom": "Pz.Kpfw. IV (-o-)", - "PzIV_schmalturm_Custom_short": "Pz.Kpfw. IV (-o-)", - "PzIV_schmalturm_short": "Pz. IV S.", - "PzIV_short": "Pz. IV G", - "PzIVturm": "Pz.Kpfw. V/IV", - "PzJgd_ketten_I": "Panzerjäger I", - "PzJgd_ketten_II": "Panzerjäger I verstärkteketten", - "PzV": "Panther I", - "PzV-IVketten": "Pz.Kpfw. V/IV", - "PzVI": "Tiger I", - "PzVIB_Heinschel_Turm": "Tiger II Serienturm", - "PzVIB_Heinschel_Turm_short": "Tiger II\\nSerienturm", - "PzVIB_Porsche_Turm": "Tiger II Turm Nr. 1-50", - "PzVIB_Porsche_Turm_short": "Tiger II\\nTurm Nr. 1-50", - "PzVIB_Standardketten": "Tiger II Versuchs-Serie", - "PzVIB_Standardketten_short": "Tiger II\\nVersuchs-Serie", - "PzVIB_Tiger_II": "Tiger II", - "PzVIB_Tiger_II_Custom": "Tiger II Savage", - "PzVIB_Tiger_II_Custom_short": "Tiger II Savage", - "PzVIB_Tiger_II_short": "Tiger II", - "PzVIB_Vetstaerktketten": "Tiger II", - "PzVIB_Vetstaerktketten_short": "Tiger II", - "PzVI_Anfangsturm": "Pz.Kpfw. VI H Ausf. H2", - "PzVI_Anfangsturm_short": "Pz.Kpfw. VI H\\nAusf. H2", - "PzVI_AusfBP": "Icebreaker", - "PzVI_AusfBP_short": "Icebreaker", - "PzVI_AusfTU": "Tiger I", - "PzVI_AusfTU_short": "Tiger I", - "PzVI_Custom_short_v2": "Tiger I Fox", - "PzVI_Custom_short_v3": "Tiger I Baron", - "PzVI_Custom_v2": "Tiger I Fox", - "PzVI_Custom_v3": "Tiger I Baron", - "PzVI_GuP": "Tiger Kuromorimine SP", - "PzVI_GuP_short": "Tiger Kuromorimine SP", - "PzVI_Porsche_Turm": "Pz.Kpfw. Tiger Ausf. E", - "PzVI_Porsche_Turm_short": "Pz.Kpfw. Tiger\\nAusf. E", - "PzVI_Standardketten": "Pz.Kpfw. VI H Ausf. H1", - "PzVI_Tiger_P": "Tiger (P)", - "PzVI_Tiger_P_Custom": "Tiger (P) Sandsturm", - "PzVI_Tiger_P_Custom_short": "Tiger (P) Sandsturm", - "PzVI_Tiger_P_short": "Tiger (P)", - "PzVI_Vetstaerktketten": "Pz.Kpfw. Tiger Ausf. E", - "PzVI_skin_gamescom": "Tiger I Gamescom", - "PzVI_skin_gamescom_short": "Tiger I Gamescom", - "PzV_PzIV": "Pz.Kpfw. V/IV", - "PzV_PzIV_ausf_Alfa": "Pz.Kpfw. V/IV Alpha", - "PzV_PzIV_ausf_Alfa_short": "Pz. V/IV A", - "PzV_PzIV_short": "Pz. V/IV", - "PzV_short": "Panther", - "Pz_II_AusfG": "Pz.Kpfw. II Ausf. G", - "Pz_II_AusfG_short": "Pz. II G", - "Pz_IV_AusfA": "Pz.Kpfw. IV Ausf. A", - "Pz_IV_AusfA_short": "Pz. IV A", - "Pz_IV_AusfBT": "Pz. IV Gargoyle", - "Pz_IV_AusfBT_short": "Pz. IV Gargoyle", - "Pz_IV_AusfD": "Pz.Kpfw. IV Ausf. D", - "Pz_IV_AusfD_short": "Pz. IV D", - "Pz_IV_AusfH": "Panzer IV Ankou Special", - "Pz_IV_AusfH_short": "Pz. IV Ankou SP", - "Pz_Sfl_IVc": "Pz.Sfl. IVc", - "Pz_Sfl_IVc_short": "Pz.Sfl. IVc", - "Pzgr39A": "AP Shell", - "Pzgr39ta": "AP Shell", - "Pzgr40A": "APCR Shell", - "QF_2pdr_Littlejohn": "QF Mk. X-B", - "QF_57x307R": "57mm Steel Shell NF", - "QF_6_pounder_8cwt_Mk_I": "QF 8 cwt Mk. I", - "QF_6_pounder_8cwt_Mk_II": "QF 6-pdr 8 cwt Mk. II", - "QF_6_pounder_8cwt_Mk_II_AT": "QF 6-pdr 8 cwt AT Gun Mk. II", - "QF_6_pounder_8cwt_Mk_I_AT": "QF 8 cwt AT Gun Mk. I", - "QF_6_pounder_Hotchkiss": "6-pdr Hotchkiss QF", - "QF_6_pounder_Mk_III": "QF 6-pdr Mk. III", - "QF_6_pounder_Mk_IIIA": "QF 6-pdr Mk. IIIA", - "QF_6_pounder_Mk_III_AT": "QF 6-pdr AT Gun Mk. III", - "QF_6_pounder_Mk_II_AT": "QF 6-pdr AT Gun Mk. II", - "QF_6_pounder_Mk_IV_AT": "QF AT Mk. IV", - "QF_6_pounder_Mk_IV_AT_A": "QF AT Mk. IV A", - "QF_6_pounder_Mk_V": "QF 6-pdr Gun Mk. V", - "QF_6_pounder_Mk_VA": "QF Mk. V A", - "QF_6_pounder_Mk_V_AT": "QF 6-pdr AT Gun Mk. V", - "QF_6_pounder_Mk_V_HW": "QF 6-pdr AT Mk. VH", - "QF_6_pounder_Mk_V_HW_short": "QF 6-pdr AT Mk. VH", - "Quad_MkII": "Quad Mk. II", - "Quad_MkII_ATCR": "Quad Mk. II ATCR", - "R105_BT_7A": "BT-7 artillery", - "R105_BT_7A_short": "BT-7 art.", - "R107_LTB": "LTTB", - "R109_T54S": "T-54 ltwt.", - "R109_T54S_short": "T-54 ltwt.", - "R110_Object_260": "Object 260", - "R110_Object_260_Custom": "Object 260 Pike", - "R110_Object_260_Custom_short": "Object 260 Pike", - "R110_Object_260_short": "Obj. 260", - "R111_ISU130": "ISU-130", - "R111_ISU130_short": "ISU-130", - "R112_T54_45": "T-54 first prototype", - "R112_T54_45_Custom": "T-54 mod. 1 Origin", - "R112_T54_45_Custom_short": "T-54 mod. 1 Origin", - "R112_T54_45_short": "T-54 mod. 1", - "R113_Object_730": "IS-5 (Object 730)", - "R113_Object_730_Custom": "IS-5 Guardian", - "R113_Object_730_Custom_short": "IS-5 Guardian", - "R113_Object_730_short": "IS-5", - "R115_IS-3_auto": "IS-3 Defender", - "R115_IS-3_auto_short": "IS-3 Defender", - "R116_ISU122C_Berlin": "ISU-122S", - "R119_Object_777": "Object 777 Version II", - "R119_Object_777_short": "Obj. 777 II", - "R120_T22SR": "T-22 medium", - "R120_T22SR_Custom": "T-22 Uncatchable", - "R120_T22SR_Custom_short": "T-22 Uncatchable", - "R120_T22SR_short": "T-22 medium", - "R127_T44_100_P": "T-44-100", - "R127_T44_100_P_short": "T-44-100", - "R131_LTG": "LTG", - "R131_LTG_short": "LTG", - "R132_T100LT": "T-100 LT", - "R132_T100LT_Custom": "T-100 LT Interceptor", - "R132_T100LT_Custom_short": "T-100 LT Interceptor", - "R132_T100LT_short": "T-100 LT", - "R134_Object_252U": "Object 252U", - "R134_Object_252U_Custom": "Object 252U Vigilant", - "R134_Object_252U_Custom_short": "Object 252U Vigilant", - "R134_Object_252U_short": "Obj. 252U", - "R140_M4A2_Loza": "Loza's M4-A2 Sherman", - "R140_M4A2_Loza_short": "Loza's Sherman", - "R146_STG": "STG", - "R146_STG_Custom": "STG Guard", - "R146_STG_Custom_short": "STG Guard", - "R146_STG_short": "STG", - "R149_Object_268_4": "Object 268 Version 4", - "R149_Object_268_4_Custom": "Object 268/4 Chronos", - "R149_Object_268_4_Custom_short": "Object 268/4 Chronos", - "R149_Object_268_4_short": "Object 268/4", - "R154_T-34E-43": "T-34 shielded", - "R154_T-34E-43_short": "T-34 shielded", - "R158_LT_432": "LT-432", - "R158_LT_432_short": "LT-432", - "R159_SU_130PM": "SU-130PM", - "R159_SU_130PM_Custom": "SU-130PM Scarface", - "R159_SU_130PM_Custom_short": "SU-130PM Scarface", - "R159_SU_130PM_short": "SU-130PM", - "R160_object_244": "Object 244", - "R160_object_244_short": "Object 244", - "R172_Object_752": "Object 752", - "R172_Object_752_Custom": "Object 752 Stargazer", - "R172_Object_752_Custom_short": "Obj. 752 Stargazer", - "R172_Object_752_short": "Object 752", - "R175_IS_2E": "IS-2 shielded", - "R175_IS_2E_Custom": "IS-2 shld Trapper", - "R175_IS_2E_Custom_short": "IS-2 shld Trapper", - "R175_IS_2E_short": "IS-2 shielded", - "R180_Object_274A": "Object 274a", - "R180_Object_274A_Custom": "Object 274a Leshy", - "R180_Object_274A_Custom_short": "Object 274a Leshy", - "R180_Object_274A_short": "Object 274a", - "R181_Object_452K": "Object 452K", - "R181_Object_452K_short": "Object 452K", - "R43_T-70_57": "T-70/57", - "R43_T-70_57_short": "T-70/57", - "R63_ST_IBD": "T-2020", - "R63_ST_IBD_short": "T-2020", - "R71_IS_2B": "IS-2 (1945)", - "R71_IS_2B_Custom": "IS-2 Berlin", - "R71_IS_2B_Custom_short": "IS-2 Berlin", - "R98_T44_85": "T-44-85", - "R98_T44_85_Custom": "T-44-85 Bravery", - "R98_T44_85_Custom_short": "T-44-85 Bravery", - "R98_T44_85_Custom_short_v2": "T-44-85 Cosmic", - "R98_T44_85_Custom_v2": "T-44-85 Cosmic", - "R98_T44_85_short": "T-44-85", - "RX_20_1_chassis": "RX-20-1", - "RX_20_1_turret": "RX-20-1", - "R_Object_84": "Object 84", - "R_Object_84_short": "Object 84", - "Ram-II": "Ram II", - "Ram_II_D1": "Ram II D1", - "Redcell_Mk1": "Redcell Mk. 1", - "Redcell_Mk2": "Wolfheart", - "Renault": "Renault", - "RenaultFT": "Renault FT", - "RenaultFT_AC": "Renault FT AC", - "RenaultFT_AC_short": "FT AC", - "RenaultFT_short": "FT", - "RenaultR35": "Renault R35", - "RenaultR35_short": "R35", - "RenaultUE57": "Renault UE 57", - "RenaultUE57_short": "UE 57", - "Renault_ACL": "Renault ACL", - "Renault_BDR": "Renault BDR", - "Renault_EU": "Renault 85", - "Renault_M26_27": "Renault M26/27", - "Renault_Naeder-FIEUX": "Renault Naeder-FIEUX", - "Renault_Naeder-FIEUX_short": "Renault\\nNaeder-FIEUX", - "Renault_S4": "Renault S4", - "Renault_S4B": "Renault S4F", - "Renault_S6": "Renault S6", - "Renault_S6Bis": "Renault S6Bis", - "Renault_T12": "Renault T12", - "Renault_T14": "Renault T14", - "Renault_V4": "Renault V4", - "RhB_Waffentrager": "Rhm.-Borsig Waffenträger", - "RhB_Waffentrager_Custom": "Rhm.-B. WT Tarnkappe", - "RhB_Waffentrager_Custom_short": "Rhm.-B. WT Tarnkappe", - "RhB_Waffentrager_short": "Rhm.-B. WT", - "Rolls_Royce_Griffon": "Rolls-Royce Griffon", - "Rolls_Royce_Griffon-Mk2": "Rolls-Royce Griffon A", - "Rolls_Royce_Griffon_B": "Rolls-Royce Griffon B", - "Rolls_Royce_Meteor_Mark_IVB_900": "Rolls-Royce Meteor Mk. IVB (900)", - "Rolls_Royce_Meteor_MkIV_B12": "Rolls-Royce Meteor Mk. IVB/12", - "Rolls_Royce_Meteor_P": "Rolls-Royce Meteor P", - "Rolls_Royse_Meteor": "Rolls-Royce Meteor", - "Rolls_Royse_Meteor_Desert": "Ancient Spirit", - "Rolls_Royse_Meteor_M120": "Rolls-Royce Meteor M120", - "Rolls_Royse_Meteor_Mk12": "Rolls-Royce Meteor Mk. 12", - "Rolls_Royse_Meteor_MkIV": "Rolls-Royce Meteor Mk. IVB", - "Rolls_Royse_Meteor_MkIVC": "Rolls-Royce Meteor Mk. IVC", - "Rolls_Royse_Meteor_MkIVD": "Rolls-Royce Meteor M120A", - "Rolls_Royse_Meteor_MkV": "Rolls-Royce Meteor Mk. V", - "Rolls_Royse_Meteor_MkXI": "Rolls-Royce Meteor Mk. XI", - "Rolls_Royse_Meteor_Mk_XI_supercharged": "Rolls-Royce Meteor Mk. XI SC", - "Rolls_Royse_Phantom": "Rolls-Royce Phantom", - "Rolls_Royse_Phantom_II": "2x Rolls-Royce Phantom II", - "Rytla_Wernera_x2": "2x PZInż. Rytla-Wernera", - "S-35ketten": "Pz.Kpfw. S35 739 (f)", - "S-51": "S-51", - "S-51_01": "S-51", - "S-51bis": "S-51 Bis", - "S01_Frankentank": "Tankenstein", - "S01_Frankentank_short": "Tankenstein", - "S02_StrvM42": "Strv m/42", - "S02_StrvM42_short": "Strv m/42", - "S04_Lago-I": "Lago", - "S04_Lago-I_short": "Lago", - "S07_Strv74BP": "Turbo", - "S07_Strv74BP_Custom": "Turbo", - "S07_Strv74BP_Custom_short": "Turbo Firestorm", - "S07_Strv74BP_short": "Turbo", - "S07_Strv_74": "Strv 74", - "S07_Strv_74_short": "Strv 74", - "S13_Leo": "Leo", - "S13_Leo_NY": "Svear", - "S13_Leo_NY_short": "Svear", - "S13_Leo_short": "Leo", - "S16_Kranvagn": "Kranvagn", - "S16_Kranvagn_Custom": "Kranvagn Nidhögg", - "S16_Kranvagn_Custom_short": "Kranvagn Nidhögg", - "S16_Kranvagn_Custom_short_v2": "Kranvagn Nord", - "S16_Kranvagn_Custom_v2": "Kranvagn Nord", - "S16_Kranvagn_short": "Kranvagn", - "S17_EMIL_1952E2": "Emil II", - "S17_EMIL_1952E2_short": "Emil II", - "S18_EMIL_1951E1": "Emil I", - "S18_EMIL_1951E1_short": "Emil I", - "S25_EMIL_51": "EMIL 1951", - "S25_EMIL_51_Custom": "EMIL 1951 Viking", - "S25_EMIL_51_Custom_short": "EMIL 1951 Viking", - "S25_EMIL_51_short": "EMIL 1951", - "S26_Lancen_C": "Lansen C", - "S26_Lancen_C_Custom": "Lansen Moose", - "S26_Lancen_C_Custom_short": "Lansen Moose", - "S26_Lancen_C_short": "Lansen C", - "S31_Strv_K": "Strv K", - "S31_Strv_K_Custom": "Strv K Konung", - "S31_Strv_K_Custom_short": "Strv K Konung", - "S31_Strv_K_short": "Strv K", - "S32_Bofors_Tornvagn": "Bofors Tornvagn", - "S32_Bofors_Tornvagn_Custom": "Tornvagn Stegosaurus", - "S32_Bofors_Tornvagn_Custom_short": "Tornvagn Stegosaurus", - "S32_Bofors_Tornvagn_short": "Tornvagn", - "S35_captured": "Pz.Kpfw. S35 739 (f)", - "S35_captured_short": "Pz. S35", - "SA35_4": "SA35 mle. 4", - "SA38_2": "SA38 mle. 2", - "SFA_8_cyl_boxer": "SFA 8 cyl boxer", - "SFA_F12": "SFA F12", - "SN-230TR": "SN-230TR", - "SN-230TRM": "SN-230TRM", - "SOFAM-12-GSds_B": "SOFAM 12 GSds B", - "SOFAM_12GSds_A": "SOFAM 12 GSds", - "SOFAM_12_58": "SOFAM 12 58", - "SOFAM_8Gxb": "SOFAM 8 Gxb", - "SOFAM_8Gxb_A": "SOFAM 8 Gxb", - "SPA_15TB": "SPA 15TB", - "SPA_17TB": "SPA 17TB", - "SPA_242": "SPA 242", - "SPA_343": "SPA 343", - "SPA_344": "SPA 344", - "SPA_mod_15": "SPA mod. 15", - "SPA_mod_17": "SPA mod. 17", - "STA_1": "STA-1", - "STA_1_Rin": "STA-1 Rin", - "STA_1_short": "STA-1", - "ST_B1": "STB-1", - "ST_B1_Custom": "STB-1 Adamant", - "ST_B1_Custom_short": "STB-1 Adamant", - "ST_B1_Custom_short_v2": "STB-1 Spinosaurus", - "ST_B1_Custom_v2": "STB-1 Spinosaurus", - "ST_B1_short": "STB-1", - "ST_I": "ST-I", - "SU-100": "SU-100", - "SU-100_mod_1944": "SU-100", - "SU-100_mod_1945": "SU-100-60", - "SU-101": "SU-101", - "SU-101_short": "SU-101", - "SU-14": "SU-14-2", - "SU-14Bis": "SU-14-2 enhanced", - "SU-14_1": "SU-14-2", - "SU-152": "SU-152", - "SU-152_01": "SU-152", - "SU-152_Custom": "SU-152 Taiga", - "SU-152_Custom_short": "SU-152 Taiga", - "SU-152_mod_1943": "SU-152", - "SU-152_mod_1944": "SU-152M", - "SU-18": "SU-18", - "SU-18Bis": "SU-18 Bis", - "SU-26": "SU-26", - "SU-5": "SU-5", - "SU-5bis": "SU-5 Bis", - "SU-76": "SU-76", - "SU-76_1": "SU-76", - "SU-76bis": "SU-76M", - "SU-8": "SU-8", - "SU-85": "SU-85", - "SU-85A": "SU-85B", - "SU-85A1": "SU-85A", - "SU-85_01": "SU-85", - "SU-85_mod_1942": "SU-85-II", - "SU-85_mod_1942_short": "SU-85-II", - "SU-85_mod_1943": "SU-85", - "SU-85_mod_1943_short": "SU-85", - "SU-8Bis": "SU-8 Bis", - "SU100M1": "SU-100M1", - "SU100Y": "SU-100Y", - "SU122_44": "SU-122-44", - "SU122_54": "SU-122-54", - "SU122_54_short": "SU-122-54", - "SU76I": "SU-76I", - "SU76I_skin_iceclaw": "SU-76I Claw", - "SU76I_skin_iceclaw_short": "SU-76I Claw", - "SU_85I": "SU-85I", - "S_35CA": "S35 CA", - "S_35CA_short": "S35 CA", - "Saurer": "Saurer", - "Scania_Vabis1664": "Scania Vabis 1664", - "Sega_Edelweiss": "Edelweiss", - "Sega_Edelweiss_short": "Edelweiss", - "Sega_Lupus": "Lupus", - "Sega_Lupus_short": "Lupus", - "Sega_Nameless": "Nameless", - "Sega_Nameless_short": "Nameless", - "Sentinel_AC1": "AC 1 Sentinel", - "Sentinel_AC1_short": "AC 1 Sentinel", - "Sgr_m38": "Sgr m/38", - "Sherman_Jumbo": "M4A3E2 Sherman Jumbo", - "Sherman_Jumbo_short": "M4A3E2", - "Skoda_16_ADH_134": "Škoda 16 ADH 134", - "Skoda_16_ADH_140_A": "Škoda 16 ADH 140", - "Skoda_16_AH_145": "Škoda 16 AH 145", - "Skoda_16_AH_145_A": "Škoda 16 AH 145 A", - "Skoda_AHK": "Škoda AHK", - "Skoda_T-11": "Škoda T-11", - "Skoda_T-15": "Škoda T-15", - "Skoda_V12BP": "Škoda V12 B", - "Skoda_V12_19_8_liter_450": "Škoda V12 mod. 1", - "Skoda_V12_19_8_liter_500": "Škoda V12 mod. 2", - "Skoda_V16_diesel_600": "Škoda 16 cylinder diesel (600)", - "Skoda_V16_diesel_660": "Škoda 16 cylinder diesel (660)", - "Slpprj_m43": "Slpprj m/43", - "Slpprj_m49": "Slpprj m/49", - "Somua_LM": "Somua LM", - "Somua_S-40": "Somua S-40", - "Somua_S-40Bis": "Somua S-40 Bis", - "Somua_Sau_40": "Somua SAu 40", - "Somua_Sau_40_short": "SAu 40", - "Somuaturm": "Pz.Kpfw. S35 739 (f)", - "Sovam_de_150cv": "SOFAM de 150 cv", - "Strv_74A2": "Stridsvagn 74A2", - "Strv_74A2_short": "Strv 74A2", - "StuGIII": "StuG III Ausf. G", - "StuGIII_0": "StuG III", - "StuGIII_Ausf_1940": "StuG III Ausf. F", - "StuGIII_Ausf_1943": "StuG III Ausf. G", - "StuGIII_short": "StuG III G", - "Sturer_Emil": "Sturer Emil", - "Sturer_Emil_short": "St. Emil", - "SturmpanzerII": "Sturmpanzer II", - "Sturmpanzer_II": "Sturmpanzer II", - "Sturmpanzer_II_short": "St.Pz. II", - "Sturmpanzerketten_I": "Sturmpanzer I", - "Sturmpanzerketten_II": "Sturmpanzer I verstärkteketten", - "Sturmpanzerketten_ausf_A": "Sturmpanzer II", - "Sturmpanzerketten_ausf_B": "Sturmpanzer II verstärkteketten", - "Su-26A": "SU-26", - "Su-26B": "SU-26 Bis", - "Su-8": "SU-8", - "Sunbeam_Amazon": "Sunbeam Amazon", - "Swedish_Bofors": "Leichttraktor", - "T-127": "T-127", - "T-14": "T14", - "T-15": "T-15", - "T-15_short": "T-15", - "T-18": "MS-1", - "T-18Bis": "T-18", - "T-18_Standart": "MS-1 mod. 1927", - "T-18_mod": "MS-1 mod. 1930", - "T-25": "T-25", - "T-25_short": "T-25", - "T-26": "T-26", - "T-26F": "T-26F", - "T-26M": "T-26M", - "T-26_conic": "T-26 mod. 1938", - "T-26_conic_short": "T-26 mod. 1938", - "T-26_cylindrical": "T-26 mod. 1936–1937", - "T-26_cylindrical_short": "T-26 mod. 1936–1937", - "T-26_short": "#ussr_vehicles:T-26_short", - "T-28": "T-28 mod. 1940", - "T-28_0": "T-28 mod. 1940", - "T-28_A": "T-28A", - "T-28_conic": "T-28 conical", - "T-28_cylindrical": "T-28 cylindrical", - "T-34": "T-34", - "T-34-85": "T-34-85", - "T-34-85Bis": "T-34-85-60", - "T-34-85_Rudy": "T-34-85 Rudy", - "T-34-85_Victory": "T-34-85 Victory", - "T-34_mod_100": "T-34-85 extended", - "T-34_mod_1940": "T-34 mod. 1940", - "T-34_mod_1941": "T-34 mod. 1941", - "T-34_mod_1941_short": "T-34 mod. 1941", - "T-34_mod_1942": "T-34 mod. 1942", - "T-34_mod_1943": "T-34 mod. 1943", - "T-34_mod_1943_short": "T-34 mod. 1943", - "T-43": "T-43", - "T-43M": "T-43 mod. 1943", - "T-43_mod_100": "T-43 mod. 1943", - "T-43_mod_43": "T-43 mod. 1942", - "T-44": "T-44", - "T-44-100": "T-44-100", - "T-44Bis": "T-44M", - "T-46": "T-46", - "T-46-1": "T-46-1", - "T-46_mod_1": "T-46", - "T-46_mod_2": "T-46-1", - "T-50": "T-50", - "T-50_short": "T-50", - "T-54": "T-54", - "T110": "T110E5", - "T110E3": "T110E3", - "T110E3_Custom": "T110E3 Bulwark", - "T110E3_Custom_short": "T110E3 Bulwark", - "T110E4": "T110E4", - "T110E4_Custom": "T110E4 Combat Star", - "T110E4_Custom_short": "T110E4 Combat Star", - "T110_Custom": "T110E5 Rebel", - "T110_Custom_short": "T110E5 Rebel", - "T119": "T119", - "T123": "T123", - "T136": "T136", - "T14": "T14", - "T150": "T-150", - "T16E2": "T16E2", - "T18": "T18", - "T18T47": "T18T47", - "T1D5": "T1D5", - "T1D6": "T1D6", - "T1_Cunningham": "T1 Cunningham", - "T1_Cunningham_short": "T1", - "T1_E6": "T1E6", - "T1_E6_short": "T1E6", - "T1_hvy": "T1 Heavy Tank", - "T1_hvy_short": "T1 Heavy", - "T20": "T20", - "T20D1": "T20D1", - "T20D2": "T20D2", - "T21": "T21", - "T21_BP": "Ranger", - "T21_BP_Custom": "Ranger All-Terrain", - "T21_BP_Custom_short": "Ranger All-Terrain", - "T21_BP_short": "Ranger", - "T23": "T23", - "T23D51080": "T23D51080", - "T23E3": "T23E3", - "T23E3_nomad": "T23E3 Nomad", - "T23E3_nomad_short": "T23E3 Nomad", - "T23T80": "T23T80", - "T25_2": "T25/2", - "T25_2_short": "T25/2", - "T25_AT": "T25 AT", - "T25_AT_short": "T25 AT", - "T26_E4_SuperPershing": "T26E4 SuperPershing", - "T26_E4_SuperPershing_Custom": "T26E4 Predator", - "T26_E4_SuperPershing_Custom_short": "T26E4 Predator", - "T26_E4_SuperPershing_short": "T26E4", - "T28": "T28", - "T28_Prototype": "T28 Prototype", - "T28_Prototype_short": "T28 Prot.", - "T28_short": "T28", - "T29": "T29", - "T2D1": "T2D1", - "T2E1": "T2E1", - "T2E4": "T2E4", - "T2T1": "T2T1", - "T2T2": "T2T2", - "T2_lt": "T2 Light Tank", - "T2_lt_short": "T2 Light", - "T2_med": "T2 Medium Tank", - "T2_med_short": "T2 Medium", - "T30": "T30", - "T30D1": "T30D1", - "T32": "T32", - "T34T7": "T34T7", - "T34_85BP": "Triumphant", - "T34_85BP_short": "Triumphant", - "T34_85TU": "T-34-85", - "T34_85TU_short": "T-34-85", - "T34_hvy": "T34", - "T34_hvy_Custom": "T34 Falcon", - "T34_hvy_Custom_short": "T34 Falcon", - "T34_hvy_Custom_short_v2": "T34 H-Combat", - "T34_hvy_Custom_v2": "T34 H-Combat", - "T34_hvy_Freedom": "T34 Independence", - "T34_hvy_Freedom_short": "T34 (1776)", - "T37": "T37", - "T37_short": "T37", - "T40": "T40", - "T41": "T41", - "T42": "T42", - "T48": "T48", - "T49": "T67", - "T49_Fearless": "T49 Fearless", - "T49_Fearless_short": "T49 Fearless", - "T49_Rocket": "T49 ATM", - "T49_Rocket_short": "T49 A", - "T51D1": "T51D1", - "T54E1": "T54E1", - "T57": "T57", - "T57_58": "T57 Heavy Tank", - "T57_58_Custom": "T57 Heavy Tireless", - "T57_58_Custom_short": "T57 Heavy Tireless", - "T57_58_Custom_short_v2": "T57 Heavy Ammoel", - "T57_58_Custom_v2": "T57 Heavy Ammoel", - "T57_58_short": "T57 Heavy", - "T62A": "T-62A", - "T62A_Custom": "T-62A Fearless", - "T62A_Custom_short": "T-62A Fearless", - "T69": "T69", - "T69_short": "T69", - "T71": "T71", - "T72": "T72", - "T72E1": "T72E1", - "T7_Combat_Car": "T7 Combat Car", - "T7_Combat_Car_short": "T7 Car", - "T80E3": "T80E3", - "T80E3A": "T80E3A", - "T82": "T82", - "T82T47": "T82T47", - "T84E1": "T84E1", - "T84E1A": "T84E1A", - "T84E3": "T84E3", - "T84E38": "T84E38", - "T84E3A": "T84E3A", - "T85E1": "T85E1", - "T85E2": "T85E2", - "T92": "T92", - "T95": "T95", - "T95E2": "T95E2", - "T95E2_short": "T95E2", - "T95_Custom": "T95 Turtle", - "T95_Custom_short": "T95 Turtle", - "T99E2": "T99E2", - "TD-12": "TD-12", - "TD-12S": "TD-12S", - "TD-30": "M-50D", - "T_44_100_Custom": "T-44-100 Hellfire", - "T_44_100_Custom_short": "T-44-100 Hellfire", - "T_44_Custom": "T-44 Exceptional", - "T_44_Custom_short": "T-44 Exceptional", - "T_50_2": "T-50-2", - "T_50_2_short": "T-50-2", - "T_54_Custom": "T-54 Threat", - "T_54_Custom_short": "T-54 Threat", - "T_Bison": "Sturmpanzer I Bison", - "Tatra_V-103": "Tatra Typ 103", - "Tatra_V-103_R": "Tatra Typ TD 103 P", - "Tatra_V-103_R_short": "Tatra Typ\\nTD 103 P", - "Tatra_V-204": "Tatra Typ 204", - "Tetrarch_LL": "Tetrarch", - "Tetrarch_LL_short": "Tetrarch", - "Tiger_short": "Tiger I", - "Torsion_bar_T80": "T23E3", - "Tr_Optimus": "#other_vehicles:Tr_Optimus", - "Tr_Optimus_short": "#other_vehicles:Tr_Optimus_short", - "Tr_Optimus_turret_01": "#usa_vehicles:Tr_Optimus_turret_01", - "Tr_Optimus_turret_02": "#other_vehicles:Tr_Optimus_turret_02", - "Tr_Optimus_turret_02_short": "#other_vehicles:Tr_Optimus_turret_02_short", - "TwinCross-1320-16V": "Gas Guzzler", - "Twin_Cadillac_42A_series": "Twin Cadillac, Series 42A", - "Twin_Cadillac_42series": "Twin Cadillac, Series 42", - "Twin_Cadillac_42series_short": "Twin Cadillac,\\nSeries 42", - "Twin_Cadillac_series_44T4": "Twin Cadillac, Series 44T4", - "Twin_Scania_Vabis_607": "Twin Scania-Vabis 607", - "Type59_Cauldron_Custom": "Type 59 Ding", - "Type59_Cauldron_Custom_short": "Type 59 Ding", - "Type95_skin_GUP": "GuP Type 95 Ha-Go", - "Type95_skin_GUP_short": "GuP Type 95 Ha-Go", - "Type_58BP": "Ox", - "Type_58BP_short": "Ox", - "Type_58TU": "Type 58", - "Type_58TU_short": "Type 58", - "Type_61": "Type 61", - "Type_61_short": "Type 61", - "UBR-240B": "AP Shell", - "UTD_20": "UTD-20", - "V-11": "V-11", - "V-11-IS-3": "Object Defender", - "V-12": "V-12", - "V-12-5": "V-12-5", - "V-12-5A": "V-12-5", - "V-12-5U": "V-12-5U", - "V-12-5_shortened": "V-12-5 shortened", - "V-12-6": "V-12-6", - "V-12-6B": "V-12-6B", - "V-12A": "V-12 A", - "V-12P": "V-12P", - "V-12PA": "V-12PA", - "V-16": "V-16", - "V-16FN": "V-16FN", - "V-16M": "V-16M", - "V-2": "V-2", - "V-2-34": "V-2-34", - "V-2-34E": "V-2-34E", - "V-2-34M": "V-2-34M", - "V-2-34M_BP": "V-2-34M B", - "V-2-34S": "V-2-34S", - "V-2-44": "V-2-44", - "V-2-54": "V-2-54", - "V-2-54IS": "V-2-54IS", - "V-2-54IS_2": "V-2-54-IS/2", - "V-2-54IS_GuP": "V-2-54 GuP", - "V-2-54SC": "V-2-54SC", - "V-2IS": "V-2IS", - "V-2IS_B": "V-2IS", - "V-2IS_B_2": "V-2IS-2", - "V-2IS_R": "V-2-10", - "V-2IS_R1": "V-2-10", - "V-2K": "V-2K", - "V-2KA": "V-2KA", - "V-2KAB": "V-2KAB", - "V-2KT": "V-2KT", - "V-2SN": "V-2SN", - "V-2SNx2": "2x V-2K-F", - "V-4": "V-4", - "V-44": "V-44", - "V-44-2": "V-44-2", - "V-44A": "V-44A", - "V-44AD": "V-44AD", - "V-5": "V-5", - "V-54": "V-54", - "V-54-6": "V-54-6", - "V-54I": "V-54I", - "V-54K": "V-54K", - "V-54KML": "V-54K-ML", - "V-54K_TU": "V-54K", - "V-55": "V-55", - "V-55U": "V-14", - "V-5BP": "V-5B", - "V-5F": "V-5F", - "V-5U": "V-5U", - "V-64": "V-64", - "V12_cylinder_Boxer_Motor": "12 cylinder Boxer-Motor", - "VK1602": "VK 16.02 Leopard", - "VK1602_short": "Leopard", - "VK2001DB": "VK 20.01 (D)", - "VK2001DB_short": "VK 20.01 D", - "VK2801": "VK 28.01", - "VK2801_BP": "Agent", - "VK2801_BP_short": "Agent", - "VK2801_short": "VK 28.01", - "VK3001H": "VK 30.01 (H)", - "VK3001H_Eliteturm": "VK 36.01 (H)", - "VK3001H_Eliteturm_short": "VK 36.01 (H)", - "VK3001H_Standardturm": "VK 30.01 (H) 0-Serie", - "VK3001H_Standardturm_short": "VK 30.01 (H)\\n0-Serie", - "VK3001H_ausf_B": "VK 30.01 (H) verstärkteketten", - "VK3001H_ausf_B_short": "VK 30.01 (H)\\nverstärkteketten", - "VK3001H_short": "VK 30.01 H", - "VK3001P": "VK 30.01 (P)", - "VK3001P_Krupp_turm": "Porsche Typ 100", - "VK3001P_Krupp_turm_short": "Porsche Typ 100", - "VK3001P_Porsche_turm": "VK 30.01 (P) Krupp Turm", - "VK3001P_Porsche_turm_short": "VK 30.01 (P)\\nKrupp Turm", - "VK3001P_ausf_B": "VK 30.01 (P) verstärkteketten", - "VK3001P_ausf_B_short": "VK 30.01 (P)\\nverstärkteketten", - "VK3001P_short": "VK 30.01 P", - "VK3002DB": "VK 30.02 (D)", - "VK3002DB_BP": "U-Panzer", - "VK3002DB_BP_Custom": "U-Panzer Barracuda", - "VK3002DB_BP_Custom_short": "U-Panzer Barracuda", - "VK3002DB_BP_short": "U-Panzer", - "VK3002DB_Shmallturm": "VK 30.02 (D) Schmalturm", - "VK3002DB_Standardtketten": "VK 30.02 (D)", - "VK3002DB_Standardtketten_short": "VK 30.02 (D)", - "VK3002DB_V1": "VK 30.01 (D)", - "VK3002DB_V1_short": "VK 30.01 D", - "VK3002DB_Vetstaerktketten": "VK 30.02 (D) verstärkteketten", - "VK3002DB_Vetstaerktketten_short": "VK 30.02 (D)\\nverst\\xe4rkteketten", - "VK3002DB_short": "VK 30.02 D", - "VK3002DB_turm": "VK 30.02 (D)", - "VK3002M": "VK 30.02 (M)", - "VK3002M_short": "VK 30.02 M", - "VK36.01H_victory": "VK 36.01 (H) Football", - "VK36.01H_victory_short": "VK 36.01 (H) Football", - "VK3601H": "VK 36.01 (H)", - "VK3601H_Grosseturm": "Pz.Kpfw. VI H Ausf. H2", - "VK3601H_Grosseturm_short": "Pz.Kpfw. VI H\\nAusf. H2", - "VK3601H_Standardturm": "VK 36.01 (H)", - "VK3601H_Standardturm_short": "VK 36.01 (H)", - "VK3601H_henshelketten": "VK 36.01 (H)", - "VK3601H_henshelketten_ausf_B": "VK 36.01 (H) verstärkteketten", - "VK3601H_henshelketten_ausf_B_short": "VK 36.01 (H)\\nverstärkteketten", - "VK3601H_henshelketten_short": "VK 36.01 (H)", - "VK3601H_short": "VK 36.01 H", - "VK4502A": "VK 45.02 (P) Ausf. A", - "VK4502A_short": "VK 45.02 A", - "VK4502P": "VK 45.02 (P) Ausf. B", - "VK4502P_Eliteturm": "VK 45.02 (P) Ausf. B Schwerer Turm", - "VK4502P_Standardketten": "VK 45.02(P) Ausf. B", - "VK4502P_Standardturm": "VK 45.02 (P) Ausf. B", - "VK4502P_Vetstaerktketten": "VK 45.02(P) Ausf. B verstärkteketten", - "VK4502P_short": "VK 45.02 B", - "VK7201": "VK 72.01 (K)", - "VK7201_Custom": "VK 72.01 (K) Rotkopf", - "VK7201_Custom_short": "VK 72.01 Rotkopf", - "VK7201_short": "VK 72.01 K", - "VVSST41": "VVSS T41", - "VVSST48": "VVSS T48", - "VVSST49": "VVSS T49", - "VVSST51": "VVSS T51", - "VVSS_T16E1": "VVSS T16E1", - "VVSS_T16E2": "VVSS T16E2", - "VVSS_T16E3": "VVSS T16E3", - "VVSS_T36E6": "VVSS T36E6", - "VVSS_T36E8": "VVSS T36E8", - "VVSS_T48": "VVSS T48", - "VVSS_T48A": "VVSS T48A", - "VVSS_T51": "VVSS T51", - "VVSS_T55E1": "VVSS T55E1", - "VVSS_T80": "VVSS T80", - "VVSS_WE210": "VVSS WE210", - "V_12CN": "V-12CN", - "V_12_5F": "B-12-5B", - "V_23TG": "V 23 Smasher", - "V_2_34": "V-2-34", - "V_2_34M": "V-2-34M", - "V_2_34_modificato": "V-2-34 modificato", - "V_2_34_modificato_BP": "V-2-34 modificato B", - "V_2_54SCG": "V-2-54SCG", - "V_46_5": "V-46-5", - "V_54": "V-54", - "V_54_105": "V-54-105", - "ValentineMK_I": "Valentine", - "Valentine_LL": "Valentine II", - "Van_Helsing": "Helsing", - "Van_Helsing_Custom": "Helsing Raven", - "Van_Helsing_Custom_short": "Helsing Raven", - "Van_Helsing_short": "Helsing", - "Vauxhall_Bedford_Twin_Six": "Bedford Twin-Six", - "Vauxhall_Bedford_Twin_SixBP": "Bedford Twin-Six", - "Vauxhall_Bedford_Twin_Six_B": "Bedford Twin-Six B", - "Vauxhall_Bedford_Twin_Six_BP": "Bedford Twin-Six II", - "Verbesserteturm": "L.K.A. 2", - "Vickers-Armstrong": "Armstrong Siddeley V8", - "Vickers-Armstrong_MkII": "Armstrong Siddeley V8 SC", - "Volvo_A8B": "Volvo A8B", - "Volvo_A8B_HW": "Grindcore", - "Volvo_V8B": "Volvo V8B", - "W-55": "W-55", - "WC_R-975C1_BP": "Wright Continental R-975C1B", - "WC_R-975C2_BP": "Wright Continental R-975C2B", - "WZ-135G_FT": "WZ 135G FT Blaze", - "WZ-135G_FT_short": "WZ Blaze", - "W_12_5": "W12-5", - "W_12_5_B": "W12-5B", - "W_12_5_P": "W12-5P", - "Waffentrager_IV": "Waffenträger auf Pz. IV", - "Waffentrager_IV_CL": "Waffenträger Ritter", - "Waffentrager_IV_CL_short": "WT Ritter", - "Waffentrager_IV_Custom": "WT auf Pz. IV Darkwing", - "Waffentrager_IV_Custom_short": "WT auf Pz. IV Darkwing", - "Waffentrager_IV_short": "WT auf Pz. IV", - "Waterspit_1": "Waterspit", - "Wespe": "Wespe", - "Wespe_short": "Wespe", - "Wespeketten_ausf_A": "Wespe", - "Wespeketten_ausf_B": "Wespe verstärkteketten", - "Wespeturm": "Wespe", - "Wright_Continental_R-975C1": "Wright Continental R-975C1", - "Wright_Continental_R-975C1_BP": "Wright Continental R-975C1 B", - "Wright_Continental_R-975C1_GuP": "Wright Continental R-975C1 GuP", - "Wright_Continental_R-975C1_short": "Wright C. R-975C1", - "Wright_Continental_R-975C2": "Wright Continental R-975C2", - "Wright_Continental_R-975C2_short": "Wright C. R-975C2", - "Wright_Continental_R-975C3": "Wright Continental R-975C3", - "Wright_Continental_R-975C4": "Wright Continental R-975C4", - "Wright_Continental_R-975C4B": "Wright Continental R-975C4B", - "Wright_Continental_R-975C4_BD": "Wright Continental R-975C4", - "Wright_Continental_R-975C4_short": "Wright C. R-975C4", - "Wright_Continental_R-975EC2": "Wright Continental R-975EC2", - "Wright_Continental_R-975EC2_short": "Wright C. R-975EC2", - "Wright_Continental_R_975_C4_858": "Wright Continental\\nR-975-C4 (858)", - "Wright_G200_m_781C9GC1": "Wright G200 M781C9GC1", - "Wright_G200_m_781C9GC1V2_BP": "Wright G200 M781C9GC1", - "Wright_G200_m_781C9GC5": "Wright G200 M781C9GC5", - "Wright_G200_m_781C9GC5_short": "Wright G200\\nM781C9GC5", - "Wright_G200_m_795C9GC1": "Wright G200 M795C9GC1", - "Wright_G2XBP": "Wright G2X", - "Wright_G2X_m_781C9GC1": "Wright G2X M781C9GC1", - "Wright_R-975EC2": "Wright R-975EC2", - "chassisE1": "Stuart E1", - "chassisE1_LL": "Stuart E1", - "chassisE2": "Stuart E2", - "chassis_IS-2Sh": "IS-2Sh", - "chinaNY": "#other_vehicles:chinaNY", - "chinaNY_short": "#other_vehicles:chinaNY_short", - "skin_121B_Gold_dragon_Custom": "121B Invincible", - "skin_121B_Gold_dragon_Custom_short": "121B Invincible", - "turret_IS-2Sh": "IS-2Sh" - } -} \ No newline at end of file +{"status": "ok", "meta": {"count": 612}, "data": {"1": {"tank_id": 1, "name": "T-34", "nation": 0, "type": "mediumTank", "tier": 5, "is_premium": false}, "17": {"tank_id": 17, "name": "Pz.Kpfw. IV Ausf. G", "nation": 1, "type": "mediumTank", "tier": 5, "is_premium": false}, "33": {"tank_id": 33, "name": "T14", "nation": 2, "type": "heavyTank", "tier": 5, "is_premium": true}, "49": {"tank_id": 49, "name": "Type 59", "nation": 3, "type": "mediumTank", "tier": 8, "is_premium": true}, "81": {"tank_id": 81, "name": "Vickers Medium Mk. I", "nation": 5, "type": "mediumTank", "tier": 1, "is_premium": false}, "113": {"tank_id": 113, "name": "Vindicator Ultramarines", "nation": 7, "type": "AT-SPG", "tier": 7, "is_premium": true}, "257": {"tank_id": 257, "name": "SU-85", "nation": 0, "type": "AT-SPG", "tier": 5, "is_premium": false}, "289": {"tank_id": 289, "name": "M3 Stuart", "nation": 2, "type": "lightTank", "tier": 2, "is_premium": false}, "321": {"tank_id": 321, "name": "D2", "nation": 4, "type": "heavyTank", "tier": 3, "is_premium": false}, "337": {"tank_id": 337, "name": "Vickers Medium Mk. II", "nation": 5, "type": "mediumTank", "tier": 2, "is_premium": true}, "353": {"tank_id": 353, "name": "Chi-Ni", "nation": 6, "type": "mediumTank", "tier": 2, "is_premium": false}, "385": {"tank_id": 385, "name": "Progetto M40 mod. 65", "nation": 8, "type": "mediumTank", "tier": 10, "is_premium": false}, "513": {"tank_id": 513, "name": "IS", "nation": 0, "type": "heavyTank", "tier": 7, "is_premium": false}, "529": {"tank_id": 529, "name": "Tiger I", "nation": 1, "type": "heavyTank", "tier": 7, "is_premium": false}, "545": {"tank_id": 545, "name": "T1 Cunningham", "nation": 2, "type": "lightTank", "tier": 1, "is_premium": false}, "577": {"tank_id": 577, "name": "Renault FT", "nation": 4, "type": "lightTank", "tier": 1, "is_premium": false}, "593": {"tank_id": 593, "name": "Sherman Firefly", "nation": 5, "type": "mediumTank", "tier": 6, "is_premium": false}, "609": {"tank_id": 609, "name": "Renault Otsu", "nation": 6, "type": "lightTank", "tier": 1, "is_premium": false}, "625": {"tank_id": 625, "name": "Stridsvagn 74A2", "nation": 7, "type": "mediumTank", "tier": 6, "is_premium": true}, "641": {"tank_id": 641, "name": "Prototipo Standard B", "nation": 8, "type": "mediumTank", "tier": 9, "is_premium": false}, "769": {"tank_id": 769, "name": "BT-7", "nation": 0, "type": "lightTank", "tier": 3, "is_premium": false}, "785": {"tank_id": 785, "name": "Pz.Kpfw. 35 (t)", "nation": 1, "type": "lightTank", "tier": 2, "is_premium": false}, "801": {"tank_id": 801, "name": "M6", "nation": 2, "type": "heavyTank", "tier": 6, "is_premium": false}, "817": {"tank_id": 817, "name": "WZ-111", "nation": 3, "type": "heavyTank", "tier": 8, "is_premium": true}, "849": {"tank_id": 849, "name": "Matilda", "nation": 5, "type": "heavyTank", "tier": 4, "is_premium": false}, "865": {"tank_id": 865, "name": "Type 95 Ha-Go", "nation": 6, "type": "lightTank", "tier": 1, "is_premium": false}, "881": {"tank_id": 881, "name": "Edelweiss", "nation": 7, "type": "mediumTank", "tier": 7, "is_premium": true}, "897": {"tank_id": 897, "name": "P.44 Pantera", "nation": 8, "type": "mediumTank", "tier": 8, "is_premium": false}, "1025": {"tank_id": 1025, "name": "BT-2", "nation": 0, "type": "lightTank", "tier": 2, "is_premium": false}, "1041": {"tank_id": 1041, "name": "StuG III Ausf. G", "nation": 1, "type": "AT-SPG", "tier": 5, "is_premium": false}, "1057": {"tank_id": 1057, "name": "M4 Sherman", "nation": 2, "type": "mediumTank", "tier": 5, "is_premium": false}, "1073": {"tank_id": 1073, "name": "T-34-1", "nation": 3, "type": "mediumTank", "tier": 7, "is_premium": false}, "1089": {"tank_id": 1089, "name": "B1", "nation": 4, "type": "heavyTank", "tier": 4, "is_premium": false}, "1105": {"tank_id": 1105, "name": "Cromwell", "nation": 5, "type": "mediumTank", "tier": 6, "is_premium": false}, "1121": {"tank_id": 1121, "name": "Type 5 Chi-Ri", "nation": 6, "type": "mediumTank", "tier": 7, "is_premium": false}, "1137": {"tank_id": 1137, "name": "Predator Ultramarines", "nation": 7, "type": "heavyTank", "tier": 7, "is_premium": true}, "1153": {"tank_id": 1153, "name": "P.43 ter", "nation": 8, "type": "mediumTank", "tier": 7, "is_premium": false}, "1297": {"tank_id": 1297, "name": "Panther I", "nation": 1, "type": "mediumTank", "tier": 7, "is_premium": false}, "1313": {"tank_id": 1313, "name": "M4A3E8 Sherman", "nation": 2, "type": "mediumTank", "tier": 6, "is_premium": false}, "1329": {"tank_id": 1329, "name": "Renault NC-31", "nation": 3, "type": "lightTank", "tier": 1, "is_premium": false}, "1361": {"tank_id": 1361, "name": "Churchill Mk. VI", "nation": 5, "type": "heavyTank", "tier": 6, "is_premium": true}, "1377": {"tank_id": 1377, "name": "Type 3 Chi-Nu", "nation": 6, "type": "mediumTank", "tier": 5, "is_premium": false}, "1393": {"tank_id": 1393, "name": "Nameless", "nation": 7, "type": "heavyTank", "tier": 7, "is_premium": true}, "1409": {"tank_id": 1409, "name": "P.43 bis", "nation": 8, "type": "mediumTank", "tier": 6, "is_premium": false}, "1537": {"tank_id": 1537, "name": "T-28 mod. 1940", "nation": 0, "type": "mediumTank", "tier": 4, "is_premium": true}, "1553": {"tank_id": 1553, "name": "Jagdpanzer IV", "nation": 1, "type": "AT-SPG", "tier": 6, "is_premium": false}, "1569": {"tank_id": 1569, "name": "T20", "nation": 2, "type": "mediumTank", "tier": 7, "is_premium": false}, "1585": {"tank_id": 1585, "name": "T-34-2", "nation": 3, "type": "mediumTank", "tier": 8, "is_premium": false}, "1601": {"tank_id": 1601, "name": "D1", "nation": 4, "type": "lightTank", "tier": 2, "is_premium": true}, "1617": {"tank_id": 1617, "name": "Sherman V", "nation": 5, "type": "mediumTank", "tier": 5, "is_premium": true}, "1633": {"tank_id": 1633, "name": "Type 1 Chi-He", "nation": 6, "type": "mediumTank", "tier": 4, "is_premium": false}, "1649": {"tank_id": 1649, "name": "Helsing", "nation": 7, "type": "AT-SPG", "tier": 7, "is_premium": true}, "1665": {"tank_id": 1665, "name": "Lago", "nation": 8, "type": "mediumTank", "tier": 4, "is_premium": false}, "1809": {"tank_id": 1809, "name": "Hetzer", "nation": 1, "type": "AT-SPG", "tier": 4, "is_premium": false}, "1825": {"tank_id": 1825, "name": "M2 Light Tank", "nation": 2, "type": "lightTank", "tier": 1, "is_premium": false}, "1841": {"tank_id": 1841, "name": "WZ-120", "nation": 3, "type": "mediumTank", "tier": 9, "is_premium": false}, "1857": {"tank_id": 1857, "name": "Bat.-Ch\u00e2tillon 25 t AP", "nation": 4, "type": "lightTank", "tier": 9, "is_premium": false}, "1889": {"tank_id": 1889, "name": "Type 4 Chi-To", "nation": 6, "type": "mediumTank", "tier": 6, "is_premium": false}, "1905": {"tank_id": 1905, "name": "O-47", "nation": 7, "type": "mediumTank", "tier": 8, "is_premium": true}, "1921": {"tank_id": 1921, "name": "Strv m/42", "nation": 8, "type": "mediumTank", "tier": 5, "is_premium": false}, "2049": {"tank_id": 2049, "name": "A-20", "nation": 0, "type": "lightTank", "tier": 4, "is_premium": false}, "2065": {"tank_id": 2065, "name": "Pz.Kpfw. II", "nation": 1, "type": "lightTank", "tier": 1, "is_premium": false}, "2097": {"tank_id": 2097, "name": "WZ-111 model 1-4", "nation": 3, "type": "heavyTank", "tier": 9, "is_premium": false}, "2129": {"tank_id": 2129, "name": "Crusader", "nation": 5, "type": "lightTank", "tier": 5, "is_premium": false}, "2145": {"tank_id": 2145, "name": "Type 97 Chi-Ha", "nation": 6, "type": "mediumTank", "tier": 3, "is_premium": false}, "2161": {"tank_id": 2161, "name": "WZ 135G FT Blaze", "nation": 7, "type": "AT-SPG", "tier": 7, "is_premium": true}, "2177": {"tank_id": 2177, "name": "14TP", "nation": 8, "type": "lightTank", "tier": 3, "is_premium": false}, "2305": {"tank_id": 2305, "name": "SU-152", "nation": 0, "type": "AT-SPG", "tier": 7, "is_premium": false}, "2321": {"tank_id": 2321, "name": "VK 36.01 (H)", "nation": 1, "type": "heavyTank", "tier": 6, "is_premium": false}, "2353": {"tank_id": 2353, "name": "Vickers Mk. E Type B", "nation": 3, "type": "lightTank", "tier": 1, "is_premium": false}, "2369": {"tank_id": 2369, "name": "FCM 36 Pak 40", "nation": 4, "type": "AT-SPG", "tier": 3, "is_premium": true}, "2385": {"tank_id": 2385, "name": "Vickers Medium Mk. III", "nation": 5, "type": "mediumTank", "tier": 3, "is_premium": true}, "2401": {"tank_id": 2401, "name": "Type 98 Ke-Ni", "nation": 6, "type": "lightTank", "tier": 3, "is_premium": true}, "2433": {"tank_id": 2433, "name": "10TP", "nation": 8, "type": "lightTank", "tier": 2, "is_premium": false}, "2561": {"tank_id": 2561, "name": "T-34-85", "nation": 0, "type": "mediumTank", "tier": 6, "is_premium": false}, "2577": {"tank_id": 2577, "name": "VK 30.01 (H)", "nation": 1, "type": "heavyTank", "tier": 5, "is_premium": true}, "2593": {"tank_id": 2593, "name": "T30", "nation": 2, "type": "AT-SPG", "tier": 9, "is_premium": false}, "2609": {"tank_id": 2609, "name": "Type 64", "nation": 3, "type": "lightTank", "tier": 6, "is_premium": true}, "2625": {"tank_id": 2625, "name": "ARL 44", "nation": 4, "type": "heavyTank", "tier": 6, "is_premium": false}, "2657": {"tank_id": 2657, "name": "STA-1", "nation": 6, "type": "mediumTank", "tier": 8, "is_premium": false}, "2689": {"tank_id": 2689, "name": "Vickers Mk. F", "nation": 8, "type": "lightTank", "tier": 1, "is_premium": false}, "2817": {"tank_id": 2817, "name": "KV-1S", "nation": 0, "type": "heavyTank", "tier": 6, "is_premium": false}, "2849": {"tank_id": 2849, "name": "T34", "nation": 2, "type": "heavyTank", "tier": 8, "is_premium": true}, "2865": {"tank_id": 2865, "name": "WZ-110", "nation": 3, "type": "heavyTank", "tier": 8, "is_premium": false}, "2881": {"tank_id": 2881, "name": "AMX 40", "nation": 4, "type": "mediumTank", "tier": 4, "is_premium": true}, "2897": {"tank_id": 2897, "name": "Churchill I", "nation": 5, "type": "heavyTank", "tier": 5, "is_premium": false}, "2913": {"tank_id": 2913, "name": "Type 5 Ke-Ho", "nation": 6, "type": "lightTank", "tier": 4, "is_premium": true}, "2945": {"tank_id": 2945, "name": "Progetto M35 mod. 46", "nation": 8, "type": "mediumTank", "tier": 8, "is_premium": true}, "3073": {"tank_id": 3073, "name": "T-46", "nation": 0, "type": "lightTank", "tier": 3, "is_premium": true}, "3089": {"tank_id": 3089, "name": "Leichttraktor", "nation": 1, "type": "lightTank", "tier": 1, "is_premium": false}, "3105": {"tank_id": 3105, "name": "M3 Lee", "nation": 2, "type": "mediumTank", "tier": 4, "is_premium": true}, "3121": {"tank_id": 3121, "name": "M5A1 Stuart", "nation": 3, "type": "lightTank", "tier": 4, "is_premium": false}, "3137": {"tank_id": 3137, "name": "AMX 50 100", "nation": 4, "type": "heavyTank", "tier": 8, "is_premium": false}, "3153": {"tank_id": 3153, "name": "Black Prince", "nation": 5, "type": "heavyTank", "tier": 7, "is_premium": false}, "3201": {"tank_id": 3201, "name": "50TP prototyp", "nation": 8, "type": "heavyTank", "tier": 9, "is_premium": true}, "3329": {"tank_id": 3329, "name": "MS-1 mod. 1", "nation": 0, "type": "lightTank", "tier": 1, "is_premium": false}, "3345": {"tank_id": 3345, "name": "Pz.Kpfw. 38 (t)", "nation": 1, "type": "lightTank", "tier": 3, "is_premium": true}, "3361": {"tank_id": 3361, "name": "T1 Heavy Tank", "nation": 2, "type": "heavyTank", "tier": 5, "is_premium": false}, "3377": {"tank_id": 3377, "name": "WZ-131", "nation": 3, "type": "lightTank", "tier": 7, "is_premium": false}, "3425": {"tank_id": 3425, "name": "Type 61", "nation": 6, "type": "mediumTank", "tier": 9, "is_premium": false}, "3457": {"tank_id": 3457, "name": "Emil I", "nation": 8, "type": "heavyTank", "tier": 8, "is_premium": false}, "3585": {"tank_id": 3585, "name": "SU-100", "nation": 0, "type": "AT-SPG", "tier": 6, "is_premium": false}, "3601": {"tank_id": 3601, "name": "Panzerj\u00e4ger I", "nation": 1, "type": "AT-SPG", "tier": 2, "is_premium": true}, "3633": {"tank_id": 3633, "name": "IS-2", "nation": 3, "type": "heavyTank", "tier": 7, "is_premium": false}, "3649": {"tank_id": 3649, "name": "Bat.-Ch\u00e2tillon 25 t", "nation": 4, "type": "lightTank", "tier": 10, "is_premium": false}, "3681": {"tank_id": 3681, "name": "STB-1", "nation": 6, "type": "mediumTank", "tier": 10, "is_premium": false}, "3697": {"tank_id": 3697, "name": "Lupus", "nation": 7, "type": "heavyTank", "tier": 7, "is_premium": true}, "3713": {"tank_id": 3713, "name": "Strv 74", "nation": 8, "type": "mediumTank", "tier": 6, "is_premium": false}, "3857": {"tank_id": 3857, "name": "Jagdpanther", "nation": 1, "type": "AT-SPG", "tier": 7, "is_premium": false}, "3873": {"tank_id": 3873, "name": "T29", "nation": 2, "type": "heavyTank", "tier": 7, "is_premium": false}, "3889": {"tank_id": 3889, "name": "WZ-132", "nation": 3, "type": "lightTank", "tier": 8, "is_premium": false}, "3905": {"tank_id": 3905, "name": "AMX 50 120", "nation": 4, "type": "heavyTank", "tier": 9, "is_premium": false}, "3921": {"tank_id": 3921, "name": "Caernarvon", "nation": 5, "type": "heavyTank", "tier": 8, "is_premium": false}, "3937": {"tank_id": 3937, "name": "Ho-Ri Type III", "nation": 6, "type": "AT-SPG", "tier": 10, "is_premium": false}, "3953": {"tank_id": 3953, "name": "T 55A", "nation": 7, "type": "mediumTank", "tier": 9, "is_premium": true}, "3969": {"tank_id": 3969, "name": "Leo", "nation": 8, "type": "mediumTank", "tier": 7, "is_premium": false}, "4113": {"tank_id": 4113, "name": "VK 30.02 (D)", "nation": 1, "type": "mediumTank", "tier": 7, "is_premium": false}, "4145": {"tank_id": 4145, "name": "WZ-121", "nation": 3, "type": "mediumTank", "tier": 10, "is_premium": false}, "4193": {"tank_id": 4193, "name": "Ho-Ri Type II", "nation": 6, "type": "AT-SPG", "tier": 9, "is_premium": false}, "4209": {"tank_id": 4209, "name": "WarDuck", "nation": 7, "type": "mediumTank", "tier": 1, "is_premium": false}, "4225": {"tank_id": 4225, "name": "Emil II", "nation": 8, "type": "heavyTank", "tier": 9, "is_premium": false}, "4353": {"tank_id": 4353, "name": "T-44", "nation": 0, "type": "mediumTank", "tier": 8, "is_premium": false}, "4369": {"tank_id": 4369, "name": "Pz.Kpfw. III", "nation": 1, "type": "mediumTank", "tier": 3, "is_premium": false}, "4385": {"tank_id": 4385, "name": "T32", "nation": 2, "type": "heavyTank", "tier": 8, "is_premium": false}, "4401": {"tank_id": 4401, "name": "Type 2597 Chi-Ha", "nation": 3, "type": "mediumTank", "tier": 3, "is_premium": false}, "4417": {"tank_id": 4417, "name": "AMX M4 mle. 54", "nation": 4, "type": "heavyTank", "tier": 10, "is_premium": true}, "4433": {"tank_id": 4433, "name": "Conqueror", "nation": 5, "type": "heavyTank", "tier": 9, "is_premium": false}, "4449": {"tank_id": 4449, "name": "IS-2 Pravda SP", "nation": 6, "type": "heavyTank", "tier": 7, "is_premium": true}, "4465": {"tank_id": 4465, "name": "Hafen", "nation": 7, "type": "mediumTank", "tier": 7, "is_premium": true}, "4481": {"tank_id": 4481, "name": "Kranvagn", "nation": 8, "type": "heavyTank", "tier": 10, "is_premium": false}, "4609": {"tank_id": 4609, "name": "T-26", "nation": 0, "type": "lightTank", "tier": 1, "is_premium": false}, "4657": {"tank_id": 4657, "name": "Type T-34", "nation": 3, "type": "mediumTank", "tier": 5, "is_premium": false}, "4689": {"tank_id": 4689, "name": "Churchill VII", "nation": 5, "type": "heavyTank", "tier": 6, "is_premium": false}, "4705": {"tank_id": 4705, "name": "Firefly Saunders SP", "nation": 6, "type": "mediumTank", "tier": 6, "is_premium": true}, "4721": {"tank_id": 4721, "name": "Gravedigger", "nation": 7, "type": "heavyTank", "tier": 7, "is_premium": true}, "4737": {"tank_id": 4737, "name": "EMIL 1951", "nation": 8, "type": "heavyTank", "tier": 8, "is_premium": true}, "4881": {"tank_id": 4881, "name": "Pz.Kpfw. III Ausf. A", "nation": 1, "type": "lightTank", "tier": 3, "is_premium": true}, "4897": {"tank_id": 4897, "name": "M2 Medium Tank", "nation": 2, "type": "mediumTank", "tier": 3, "is_premium": true}, "4913": {"tank_id": 4913, "name": "59-16", "nation": 3, "type": "lightTank", "tier": 6, "is_premium": false}, "4929": {"tank_id": 4929, "name": "AMX 13 90", "nation": 4, "type": "lightTank", "tier": 8, "is_premium": false}, "4945": {"tank_id": 4945, "name": "Valentine Mk. IX", "nation": 5, "type": "lightTank", "tier": 4, "is_premium": true}, "4961": {"tank_id": 4961, "name": "Ho-Ri Type I", "nation": 6, "type": "AT-SPG", "tier": 8, "is_premium": false}, "4977": {"tank_id": 4977, "name": "Scavenger", "nation": 7, "type": "mediumTank", "tier": 5, "is_premium": true}, "4993": {"tank_id": 4993, "name": "P.43/06 anniversario", "nation": 8, "type": "mediumTank", "tier": 6, "is_premium": true}, "5121": {"tank_id": 5121, "name": "AT-1", "nation": 0, "type": "AT-SPG", "tier": 2, "is_premium": true}, "5137": {"tank_id": 5137, "name": "Tiger II", "nation": 1, "type": "heavyTank", "tier": 8, "is_premium": false}, "5153": {"tank_id": 5153, "name": "M5 Stuart", "nation": 2, "type": "lightTank", "tier": 3, "is_premium": false}, "5169": {"tank_id": 5169, "name": "Type 58", "nation": 3, "type": "mediumTank", "tier": 6, "is_premium": false}, "5185": {"tank_id": 5185, "name": "AMX 13 75", "nation": 4, "type": "lightTank", "tier": 7, "is_premium": false}, "5201": {"tank_id": 5201, "name": "Cruiser Mk. I", "nation": 5, "type": "lightTank", "tier": 2, "is_premium": true}, "5217": {"tank_id": 5217, "name": "Chi-To SPG", "nation": 6, "type": "AT-SPG", "tier": 7, "is_premium": false}, "5233": {"tank_id": 5233, "name": "Smasher", "nation": 7, "type": "heavyTank", "tier": 7, "is_premium": true}, "5249": {"tank_id": 5249, "name": "Pudel", "nation": 8, "type": "mediumTank", "tier": 6, "is_premium": true}, "5377": {"tank_id": 5377, "name": "IS-3", "nation": 0, "type": "heavyTank", "tier": 8, "is_premium": false}, "5393": {"tank_id": 5393, "name": "VK 16.02 Leopard", "nation": 1, "type": "lightTank", "tier": 5, "is_premium": false}, "5409": {"tank_id": 5409, "name": "M7", "nation": 2, "type": "mediumTank", "tier": 4, "is_premium": false}, "5425": {"tank_id": 5425, "name": "WZ-113", "nation": 3, "type": "heavyTank", "tier": 10, "is_premium": false}, "5441": {"tank_id": 5441, "name": "AMX 30 1er prototype", "nation": 4, "type": "mediumTank", "tier": 9, "is_premium": true}, "5457": {"tank_id": 5457, "name": "Comet", "nation": 5, "type": "mediumTank", "tier": 7, "is_premium": false}, "5473": {"tank_id": 5473, "name": "Mitsu 108", "nation": 6, "type": "heavyTank", "tier": 5, "is_premium": false}, "5489": {"tank_id": 5489, "name": "Y5 T-34", "nation": 7, "type": "mediumTank", "tier": 5, "is_premium": true}, "5505": {"tank_id": 5505, "name": "TVP T 50/51", "nation": 8, "type": "mediumTank", "tier": 10, "is_premium": false}, "5665": {"tank_id": 5665, "name": "T2 Medium Tank", "nation": 2, "type": "mediumTank", "tier": 2, "is_premium": true}, "5681": {"tank_id": 5681, "name": "121B", "nation": 3, "type": "mediumTank", "tier": 10, "is_premium": true}, "5713": {"tank_id": 5713, "name": "Centurion Mk. 7/1", "nation": 5, "type": "mediumTank", "tier": 9, "is_premium": false}, "5729": {"tank_id": 5729, "name": "Ju-Nu", "nation": 6, "type": "heavyTank", "tier": 6, "is_premium": false}, "5745": {"tank_id": 5745, "name": "Y5 Firefly", "nation": 7, "type": "mediumTank", "tier": 6, "is_premium": true}, "5761": {"tank_id": 5761, "name": "\u0160koda T 50", "nation": 8, "type": "mediumTank", "tier": 9, "is_premium": false}, "5889": {"tank_id": 5889, "name": "KV-3", "nation": 0, "type": "heavyTank", "tier": 7, "is_premium": false}, "5921": {"tank_id": 5921, "name": "M26 Pershing", "nation": 2, "type": "mediumTank", "tier": 8, "is_premium": false}, "5937": {"tank_id": 5937, "name": "59-Patton", "nation": 3, "type": "mediumTank", "tier": 8, "is_premium": true}, "5953": {"tank_id": 5953, "name": "AMX 38", "nation": 4, "type": "mediumTank", "tier": 2, "is_premium": false}, "5969": {"tank_id": 5969, "name": "Centurion Mk. I", "nation": 5, "type": "mediumTank", "tier": 8, "is_premium": false}, "5985": {"tank_id": 5985, "name": "Ju-To", "nation": 6, "type": "heavyTank", "tier": 7, "is_premium": false}, "6001": {"tank_id": 6001, "name": "Y5 ELC bis", "nation": 7, "type": "mediumTank", "tier": 7, "is_premium": true}, "6017": {"tank_id": 6017, "name": "TVP VTU Koncept", "nation": 8, "type": "mediumTank", "tier": 8, "is_premium": false}, "6145": {"tank_id": 6145, "name": "IS-4", "nation": 0, "type": "heavyTank", "tier": 10, "is_premium": false}, "6161": {"tank_id": 6161, "name": "Pz.Kpfw. II Luchs", "nation": 1, "type": "lightTank", "tier": 4, "is_premium": true}, "6177": {"tank_id": 6177, "name": "T18", "nation": 2, "type": "AT-SPG", "tier": 2, "is_premium": true}, "6193": {"tank_id": 6193, "name": "T-34-3", "nation": 3, "type": "mediumTank", "tier": 8, "is_premium": true}, "6209": {"tank_id": 6209, "name": "AMX 50 B", "nation": 4, "type": "heavyTank", "tier": 10, "is_premium": false}, "6225": {"tank_id": 6225, "name": "FV215b", "nation": 5, "type": "heavyTank", "tier": 10, "is_premium": false}, "6241": {"tank_id": 6241, "name": "Chi-Se", "nation": 6, "type": "heavyTank", "tier": 8, "is_premium": false}, "6257": {"tank_id": 6257, "name": "M4/FL10", "nation": 7, "type": "mediumTank", "tier": 7, "is_premium": true}, "6273": {"tank_id": 6273, "name": "Kon\u0161trukta T-34/100", "nation": 8, "type": "mediumTank", "tier": 7, "is_premium": false}, "6401": {"tank_id": 6401, "name": "SU-76", "nation": 0, "type": "AT-SPG", "tier": 3, "is_premium": true}, "6417": {"tank_id": 6417, "name": "Pz.Kpfw. III/IV", "nation": 1, "type": "mediumTank", "tier": 5, "is_premium": true}, "6433": {"tank_id": 6433, "name": "T82", "nation": 2, "type": "AT-SPG", "tier": 3, "is_premium": true}, "6449": {"tank_id": 6449, "name": "WZ-113G FT", "nation": 3, "type": "AT-SPG", "tier": 10, "is_premium": false}, "6465": {"tank_id": 6465, "name": "AMX 12 t", "nation": 4, "type": "lightTank", "tier": 6, "is_premium": false}, "6481": {"tank_id": 6481, "name": "Covenanter", "nation": 5, "type": "lightTank", "tier": 4, "is_premium": true}, "6497": {"tank_id": 6497, "name": "Type 68", "nation": 6, "type": "heavyTank", "tier": 9, "is_premium": false}, "6529": {"tank_id": 6529, "name": "\u0160koda T 25", "nation": 8, "type": "mediumTank", "tier": 6, "is_premium": false}, "6657": {"tank_id": 6657, "name": "T-43", "nation": 0, "type": "mediumTank", "tier": 7, "is_premium": false}, "6673": {"tank_id": 6673, "name": "Marder II", "nation": 1, "type": "AT-SPG", "tier": 3, "is_premium": true}, "6689": {"tank_id": 6689, "name": "T49 ATM", "nation": 2, "type": "lightTank", "tier": 7, "is_premium": true}, "6705": {"tank_id": 6705, "name": "LT vz. 38", "nation": 3, "type": "lightTank", "tier": 2, "is_premium": false}, "6721": {"tank_id": 6721, "name": "BDR G1 B", "nation": 4, "type": "heavyTank", "tier": 5, "is_premium": false}, "6753": {"tank_id": 6753, "name": "Type 71", "nation": 6, "type": "heavyTank", "tier": 10, "is_premium": false}, "6785": {"tank_id": 6785, "name": "\u0160koda T 27", "nation": 8, "type": "mediumTank", "tier": 8, "is_premium": true}, "6913": {"tank_id": 6913, "name": "SU-85B", "nation": 0, "type": "AT-SPG", "tier": 4, "is_premium": false}, "6929": {"tank_id": 6929, "name": "Maus", "nation": 1, "type": "heavyTank", "tier": 10, "is_premium": false}, "6945": {"tank_id": 6945, "name": "M10 Wolverine", "nation": 2, "type": "AT-SPG", "tier": 5, "is_premium": false}, "6961": {"tank_id": 6961, "name": "WZ-120-1G FT", "nation": 3, "type": "AT-SPG", "tier": 8, "is_premium": true}, "6977": {"tank_id": 6977, "name": "AMX M4 mle. 45", "nation": 4, "type": "heavyTank", "tier": 7, "is_premium": false}, "6993": {"tank_id": 6993, "name": "Cruiser Mk. II", "nation": 5, "type": "lightTank", "tier": 1, "is_premium": false}, "7009": {"tank_id": 7009, "name": "Type 57", "nation": 6, "type": "heavyTank", "tier": 8, "is_premium": true}, "7025": {"tank_id": 7025, "name": "Vulcan", "nation": 7, "type": "heavyTank", "tier": 7, "is_premium": true}, "7041": {"tank_id": 7041, "name": "Turbo", "nation": 8, "type": "mediumTank", "tier": 6, "is_premium": true}, "7169": {"tank_id": 7169, "name": "IS-7", "nation": 0, "type": "heavyTank", "tier": 10, "is_premium": false}, "7185": {"tank_id": 7185, "name": "VK 30.01 (P)", "nation": 1, "type": "mediumTank", "tier": 6, "is_premium": false}, "7201": {"tank_id": 7201, "name": "M36 Jackson", "nation": 2, "type": "AT-SPG", "tier": 6, "is_premium": false}, "7217": {"tank_id": 7217, "name": "WZ-112-2", "nation": 3, "type": "heavyTank", "tier": 8, "is_premium": true}, "7249": {"tank_id": 7249, "name": "FV4202", "nation": 5, "type": "mediumTank", "tier": 10, "is_premium": false}, "7265": {"tank_id": 7265, "name": "Ferrum", "nation": 6, "type": "heavyTank", "tier": 6, "is_premium": true}, "7281": {"tank_id": 7281, "name": "Lycan", "nation": 7, "type": "heavyTank", "tier": 7, "is_premium": true}, "7297": {"tank_id": 7297, "name": "60TP Lewandowskiego", "nation": 8, "type": "heavyTank", "tier": 10, "is_premium": false}, "7425": {"tank_id": 7425, "name": "ISU-152", "nation": 0, "type": "AT-SPG", "tier": 8, "is_premium": false}, "7441": {"tank_id": 7441, "name": "VK 45.02 (P) Ausf. B", "nation": 1, "type": "heavyTank", "tier": 9, "is_premium": false}, "7473": {"tank_id": 7473, "name": "T-34-2G FT", "nation": 3, "type": "AT-SPG", "tier": 7, "is_premium": false}, "7505": {"tank_id": 7505, "name": "Cruiser Mk. IV", "nation": 5, "type": "mediumTank", "tier": 3, "is_premium": false}, "7537": {"tank_id": 7537, "name": "Nightmare", "nation": 7, "type": "AT-SPG", "tier": 5, "is_premium": true}, "7553": {"tank_id": 7553, "name": "50TP Tyszkiewicza", "nation": 8, "type": "heavyTank", "tier": 9, "is_premium": false}, "7697": {"tank_id": 7697, "name": "Ferdinand", "nation": 1, "type": "AT-SPG", "tier": 8, "is_premium": false}, "7713": {"tank_id": 7713, "name": "T40", "nation": 2, "type": "AT-SPG", "tier": 4, "is_premium": true}, "7729": {"tank_id": 7729, "name": "WZ-131G FT", "nation": 3, "type": "AT-SPG", "tier": 6, "is_premium": false}, "7745": {"tank_id": 7745, "name": "Renault FT AC", "nation": 4, "type": "AT-SPG", "tier": 2, "is_premium": true}, "7761": {"tank_id": 7761, "name": "Cruiser Mk. III", "nation": 5, "type": "mediumTank", "tier": 2, "is_premium": false}, "7793": {"tank_id": 7793, "name": "Annihilator", "nation": 7, "type": "heavyTank", "tier": 7, "is_premium": true}, "7809": {"tank_id": 7809, "name": "53TP Markowskiego", "nation": 8, "type": "heavyTank", "tier": 8, "is_premium": false}, "7937": {"tank_id": 7937, "name": "T-54", "nation": 0, "type": "mediumTank", "tier": 9, "is_premium": false}, "7953": {"tank_id": 7953, "name": "Jagdtiger", "nation": 1, "type": "AT-SPG", "tier": 9, "is_premium": false}, "7985": {"tank_id": 7985, "name": "WZ-111-1G FT", "nation": 3, "type": "AT-SPG", "tier": 8, "is_premium": false}, "8001": {"tank_id": 8001, "name": "Lorraine 40 t", "nation": 4, "type": "mediumTank", "tier": 8, "is_premium": true}, "8017": {"tank_id": 8017, "name": "Valentine AT", "nation": 5, "type": "AT-SPG", "tier": 3, "is_premium": true}, "8049": {"tank_id": 8049, "name": "Spike", "nation": 7, "type": "mediumTank", "tier": 5, "is_premium": true}, "8065": {"tank_id": 8065, "name": "40TP Habicha", "nation": 8, "type": "heavyTank", "tier": 6, "is_premium": false}, "8193": {"tank_id": 8193, "name": "Object 704", "nation": 0, "type": "AT-SPG", "tier": 9, "is_premium": false}, "8209": {"tank_id": 8209, "name": "Pz.Kpfw. 38 (t) n.A.", "nation": 1, "type": "lightTank", "tier": 4, "is_premium": true}, "8225": {"tank_id": 8225, "name": "T28", "nation": 2, "type": "AT-SPG", "tier": 8, "is_premium": false}, "8241": {"tank_id": 8241, "name": "WZ-111G FT", "nation": 3, "type": "AT-SPG", "tier": 9, "is_premium": false}, "8257": {"tank_id": 8257, "name": "Renault UE 57", "nation": 4, "type": "AT-SPG", "tier": 3, "is_premium": true}, "8273": {"tank_id": 8273, "name": "Universal Carrier 2-pdr", "nation": 5, "type": "AT-SPG", "tier": 2, "is_premium": true}, "8305": {"tank_id": 8305, "name": "Titan H-Nd", "nation": 7, "type": "heavyTank", "tier": 7, "is_premium": true}, "8321": {"tank_id": 8321, "name": "45TP Habicha", "nation": 8, "type": "heavyTank", "tier": 7, "is_premium": false}, "8465": {"tank_id": 8465, "name": "Panther II", "nation": 1, "type": "mediumTank", "tier": 8, "is_premium": false}, "8497": {"tank_id": 8497, "name": "WZ-111 model 5A", "nation": 3, "type": "heavyTank", "tier": 10, "is_premium": true}, "8513": {"tank_id": 8513, "name": "AMX 30 B", "nation": 4, "type": "mediumTank", "tier": 10, "is_premium": true}, "8529": {"tank_id": 8529, "name": "AT 15", "nation": 5, "type": "AT-SPG", "tier": 8, "is_premium": false}, "8561": {"tank_id": 8561, "name": "Titan T24 57", "nation": 7, "type": "lightTank", "tier": 6, "is_premium": true}, "8577": {"tank_id": 8577, "name": "Lansen C", "nation": 8, "type": "mediumTank", "tier": 8, "is_premium": true}, "8737": {"tank_id": 8737, "name": "T95", "nation": 2, "type": "AT-SPG", "tier": 9, "is_premium": false}, "8753": {"tank_id": 8753, "name": "M41D", "nation": 3, "type": "lightTank", "tier": 7, "is_premium": true}, "8785": {"tank_id": 8785, "name": "AT 2", "nation": 5, "type": "AT-SPG", "tier": 5, "is_premium": false}, "8817": {"tank_id": 8817, "name": "Titan Mk. I", "nation": 7, "type": "lightTank", "tier": 5, "is_premium": true}, "8833": {"tank_id": 8833, "name": "Spark", "nation": 8, "type": "mediumTank", "tier": 6, "is_premium": true}, "8961": {"tank_id": 8961, "name": "KV-13", "nation": 0, "type": "mediumTank", "tier": 7, "is_premium": true}, "8993": {"tank_id": 8993, "name": "M46 Patton", "nation": 2, "type": "mediumTank", "tier": 9, "is_premium": false}, "9009": {"tank_id": 9009, "name": "Ox", "nation": 3, "type": "mediumTank", "tier": 6, "is_premium": true}, "9041": {"tank_id": 9041, "name": "Alecto", "nation": 5, "type": "AT-SPG", "tier": 4, "is_premium": false}, "9073": {"tank_id": 9073, "name": "Titan-54d", "nation": 7, "type": "mediumTank", "tier": 8, "is_premium": true}, "9089": {"tank_id": 9089, "name": "\u0160koda T 56", "nation": 8, "type": "heavyTank", "tier": 8, "is_premium": true}, "9217": {"tank_id": 9217, "name": "IS-6", "nation": 0, "type": "heavyTank", "tier": 8, "is_premium": true}, "9249": {"tank_id": 9249, "name": "T25 AT", "nation": 2, "type": "AT-SPG", "tier": 7, "is_premium": false}, "9265": {"tank_id": 9265, "name": "Type 58", "nation": 3, "type": "mediumTank", "tier": 6, "is_premium": true}, "9297": {"tank_id": 9297, "name": "FV215b (183)", "nation": 5, "type": "AT-SPG", "tier": 10, "is_premium": false}, "9329": {"tank_id": 9329, "name": "Titan-150", "nation": 7, "type": "heavyTank", "tier": 6, "is_premium": true}, "9345": {"tank_id": 9345, "name": "Svear", "nation": 8, "type": "mediumTank", "tier": 7, "is_premium": true}, "9489": {"tank_id": 9489, "name": "E 100", "nation": 1, "type": "heavyTank", "tier": 10, "is_premium": false}, "9505": {"tank_id": 9505, "name": "M103", "nation": 2, "type": "heavyTank", "tier": 9, "is_premium": false}, "9521": {"tank_id": 9521, "name": "WZ-122 TM", "nation": 3, "type": "mediumTank", "tier": 8, "is_premium": true}, "9553": {"tank_id": 9553, "name": "AT 8", "nation": 5, "type": "AT-SPG", "tier": 6, "is_premium": false}, "9601": {"tank_id": 9601, "name": "CS-52 LIS", "nation": 8, "type": "mediumTank", "tier": 7, "is_premium": true}, "9745": {"tank_id": 9745, "name": "E 75", "nation": 1, "type": "heavyTank", "tier": 9, "is_premium": false}, "9761": {"tank_id": 9761, "name": "M24 Chaffee", "nation": 2, "type": "lightTank", "tier": 6, "is_premium": false}, "9777": {"tank_id": 9777, "name": "WZ-114", "nation": 3, "type": "heavyTank", "tier": 9, "is_premium": true}, "9793": {"tank_id": 9793, "name": "Somua SAu 40", "nation": 4, "type": "AT-SPG", "tier": 4, "is_premium": false}, "9809": {"tank_id": 9809, "name": "Churchill Gun Carrier", "nation": 5, "type": "AT-SPG", "tier": 6, "is_premium": true}, "9841": {"tank_id": 9841, "name": "Rover", "nation": 7, "type": "lightTank", "tier": 6, "is_premium": true}, "9857": {"tank_id": 9857, "name": "\u0160koda T 45", "nation": 8, "type": "heavyTank", "tier": 7, "is_premium": true}, "9985": {"tank_id": 9985, "name": "SU-101", "nation": 0, "type": "AT-SPG", "tier": 8, "is_premium": false}, "10001": {"tank_id": 10001, "name": "VK 28.01", "nation": 1, "type": "lightTank", "tier": 6, "is_premium": false}, "10017": {"tank_id": 10017, "name": "M4A3E2 Sherman Jumbo", "nation": 2, "type": "mediumTank", "tier": 6, "is_premium": true}, "10033": {"tank_id": 10033, "name": "WZ-132A", "nation": 3, "type": "lightTank", "tier": 9, "is_premium": false}, "10049": {"tank_id": 10049, "name": "S35 CA", "nation": 4, "type": "AT-SPG", "tier": 5, "is_premium": false}, "10065": {"tank_id": 10065, "name": "AT 7", "nation": 5, "type": "AT-SPG", "tier": 7, "is_premium": false}, "10097": {"tank_id": 10097, "name": "Medjay", "nation": 7, "type": "heavyTank", "tier": 5, "is_premium": true}, "10113": {"tank_id": 10113, "name": "Carro da Combattimento 45t", "nation": 8, "type": "mediumTank", "tier": 10, "is_premium": true}, "10241": {"tank_id": 10241, "name": "SU-100M1", "nation": 0, "type": "AT-SPG", "tier": 7, "is_premium": false}, "10257": {"tank_id": 10257, "name": "E 50", "nation": 1, "type": "mediumTank", "tier": 9, "is_premium": false}, "10273": {"tank_id": 10273, "name": "M8A1", "nation": 2, "type": "AT-SPG", "tier": 4, "is_premium": false}, "10289": {"tank_id": 10289, "name": "WZ-132-1", "nation": 3, "type": "lightTank", "tier": 10, "is_premium": false}, "10353": {"tank_id": 10353, "name": "Pharaoh", "nation": 7, "type": "heavyTank", "tier": 7, "is_premium": true}, "10369": {"tank_id": 10369, "name": "Controcarro 3 Minotauro", "nation": 8, "type": "AT-SPG", "tier": 10, "is_premium": false}, "10497": {"tank_id": 10497, "name": "KV-2", "nation": 0, "type": "heavyTank", "tier": 6, "is_premium": false}, "10513": {"tank_id": 10513, "name": "VK 45.02 (P) Ausf. A", "nation": 1, "type": "heavyTank", "tier": 8, "is_premium": false}, "10529": {"tank_id": 10529, "name": "T67", "nation": 2, "type": "AT-SPG", "tier": 5, "is_premium": true}, "10545": {"tank_id": 10545, "name": "Wind", "nation": 3, "type": "lightTank", "tier": 6, "is_premium": true}, "10609": {"tank_id": 10609, "name": "Magnate", "nation": 7, "type": "mediumTank", "tier": 7, "is_premium": true}, "10625": {"tank_id": 10625, "name": "Controcarro 1 Mk. 2", "nation": 8, "type": "AT-SPG", "tier": 9, "is_premium": false}, "10753": {"tank_id": 10753, "name": "ST-I", "nation": 0, "type": "heavyTank", "tier": 9, "is_premium": false}, "10769": {"tank_id": 10769, "name": "Tiger (P)", "nation": 1, "type": "heavyTank", "tier": 7, "is_premium": false}, "10785": {"tank_id": 10785, "name": "T110E5", "nation": 2, "type": "heavyTank", "tier": 10, "is_premium": false}, "10801": {"tank_id": 10801, "name": "Panlong", "nation": 3, "type": "AT-SPG", "tier": 6, "is_premium": true}, "10817": {"tank_id": 10817, "name": "AMX AC mle. 46", "nation": 4, "type": "AT-SPG", "tier": 7, "is_premium": false}, "10865": {"tank_id": 10865, "name": "Fixer", "nation": 7, "type": "mediumTank", "tier": 8, "is_premium": true}, "10881": {"tank_id": 10881, "name": "Semovente Controcarro mod. 1964", "nation": 8, "type": "AT-SPG", "tier": 8, "is_premium": false}, "11009": {"tank_id": 11009, "name": "KV-4", "nation": 0, "type": "heavyTank", "tier": 8, "is_premium": false}, "11025": {"tank_id": 11025, "name": "Sturer Emil", "nation": 1, "type": "AT-SPG", "tier": 7, "is_premium": false}, "11041": {"tank_id": 11041, "name": "T25/2", "nation": 2, "type": "AT-SPG", "tier": 7, "is_premium": false}, "11057": {"tank_id": 11057, "name": "114 SP2", "nation": 3, "type": "heavyTank", "tier": 10, "is_premium": true}, "11073": {"tank_id": 11073, "name": "AMX 50 Foch", "nation": 4, "type": "AT-SPG", "tier": 9, "is_premium": false}, "11121": {"tank_id": 11121, "name": "Regressor", "nation": 7, "type": "heavyTank", "tier": 9, "is_premium": true}, "11137": {"tank_id": 11137, "name": "Semovente Controcarro mod. 1956", "nation": 8, "type": "AT-SPG", "tier": 7, "is_premium": false}, "11265": {"tank_id": 11265, "name": "T-150", "nation": 0, "type": "heavyTank", "tier": 6, "is_premium": true}, "11281": {"tank_id": 11281, "name": "Kampfpanzer 70", "nation": 1, "type": "heavyTank", "tier": 9, "is_premium": true}, "11297": {"tank_id": 11297, "name": "T28 Prototype", "nation": 2, "type": "AT-SPG", "tier": 8, "is_premium": false}, "11393": {"tank_id": 11393, "name": "Semovente M43 Bassotto", "nation": 8, "type": "AT-SPG", "tier": 6, "is_premium": false}, "11521": {"tank_id": 11521, "name": "IS-8", "nation": 0, "type": "heavyTank", "tier": 9, "is_premium": false}, "11537": {"tank_id": 11537, "name": "Jagdpanther II", "nation": 1, "type": "AT-SPG", "tier": 8, "is_premium": false}, "11553": {"tank_id": 11553, "name": "M18 Hellcat", "nation": 2, "type": "AT-SPG", "tier": 6, "is_premium": false}, "11585": {"tank_id": 11585, "name": "ARL V39", "nation": 4, "type": "AT-SPG", "tier": 6, "is_premium": false}, "11649": {"tank_id": 11649, "name": "Semovente M41", "nation": 8, "type": "AT-SPG", "tier": 5, "is_premium": false}, "11777": {"tank_id": 11777, "name": "KV-1", "nation": 0, "type": "heavyTank", "tier": 5, "is_premium": false}, "11793": {"tank_id": 11793, "name": "Nashorn", "nation": 1, "type": "AT-SPG", "tier": 6, "is_premium": false}, "11809": {"tank_id": 11809, "name": "T23E3", "nation": 2, "type": "mediumTank", "tier": 7, "is_premium": true}, "11905": {"tank_id": 11905, "name": "Shadowhunter", "nation": 8, "type": "mediumTank", "tier": 6, "is_premium": true}, "12033": {"tank_id": 12033, "name": "SU-122-54", "nation": 0, "type": "AT-SPG", "tier": 9, "is_premium": false}, "12049": {"tank_id": 12049, "name": "Jagdpanzer E 100", "nation": 1, "type": "AT-SPG", "tier": 10, "is_premium": false}, "12065": {"tank_id": 12065, "name": "T95E2", "nation": 2, "type": "mediumTank", "tier": 8, "is_premium": true}, "12097": {"tank_id": 12097, "name": "AMX AC mle. 48", "nation": 4, "type": "AT-SPG", "tier": 8, "is_premium": false}, "12161": {"tank_id": 12161, "name": "Strv K", "nation": 8, "type": "heavyTank", "tier": 10, "is_premium": true}, "12305": {"tank_id": 12305, "name": "E 50 Ausf. M", "nation": 1, "type": "mediumTank", "tier": 10, "is_premium": false}, "12321": {"tank_id": 12321, "name": "T6E1 Grizzly", "nation": 2, "type": "mediumTank", "tier": 4, "is_premium": true}, "12417": {"tank_id": 12417, "name": "Bisonte C45", "nation": 8, "type": "heavyTank", "tier": 8, "is_premium": true}, "12545": {"tank_id": 12545, "name": "K-91", "nation": 0, "type": "heavyTank", "tier": 9, "is_premium": true}, "12673": {"tank_id": 12673, "name": "Bofors Tornvagn", "nation": 8, "type": "heavyTank", "tier": 8, "is_premium": true}, "12929": {"tank_id": 12929, "name": "TNH T Vz. 51", "nation": 8, "type": "heavyTank", "tier": 9, "is_premium": false}, "13073": {"tank_id": 13073, "name": "Pz.Kpfw. II Ausf. G", "nation": 1, "type": "lightTank", "tier": 3, "is_premium": true}, "13089": {"tank_id": 13089, "name": "T110E4", "nation": 2, "type": "AT-SPG", "tier": 10, "is_premium": false}, "13185": {"tank_id": 13185, "name": "Vz. 55", "nation": 8, "type": "heavyTank", "tier": 10, "is_premium": false}, "13329": {"tank_id": 13329, "name": "Durchbruchswagen 2", "nation": 1, "type": "heavyTank", "tier": 4, "is_premium": true}, "13345": {"tank_id": 13345, "name": "T26E4 SuperPershing", "nation": 2, "type": "mediumTank", "tier": 8, "is_premium": true}, "13441": {"tank_id": 13441, "name": "Aeonix", "nation": 8, "type": "mediumTank", "tier": 6, "is_premium": true}, "13569": {"tank_id": 13569, "name": "Object 268", "nation": 0, "type": "AT-SPG", "tier": 10, "is_premium": false}, "13697": {"tank_id": 13697, "name": "TNH 105/1000", "nation": 8, "type": "heavyTank", "tier": 8, "is_premium": false}, "13825": {"tank_id": 13825, "name": "T-62A", "nation": 0, "type": "mediumTank", "tier": 10, "is_premium": false}, "13841": {"tank_id": 13841, "name": "Indien-Panzer", "nation": 1, "type": "mediumTank", "tier": 8, "is_premium": false}, "13857": {"tank_id": 13857, "name": "T110E3", "nation": 2, "type": "AT-SPG", "tier": 10, "is_premium": false}, "13889": {"tank_id": 13889, "name": "AMX 50 Foch (155)", "nation": 4, "type": "AT-SPG", "tier": 10, "is_premium": false}, "13953": {"tank_id": 13953, "name": "Vz. 44-1", "nation": 8, "type": "heavyTank", "tier": 7, "is_premium": false}, "14097": {"tank_id": 14097, "name": "VK 30.01 (D)", "nation": 1, "type": "mediumTank", "tier": 6, "is_premium": false}, "14113": {"tank_id": 14113, "name": "M48 Patton", "nation": 2, "type": "mediumTank", "tier": 10, "is_premium": false}, "14145": {"tank_id": 14145, "name": "AMX ELC bis", "nation": 4, "type": "lightTank", "tier": 5, "is_premium": false}, "14209": {"tank_id": 14209, "name": "\u0160koda P-JS", "nation": 8, "type": "heavyTank", "tier": 6, "is_premium": false}, "14337": {"tank_id": 14337, "name": "Object 263", "nation": 0, "type": "AT-SPG", "tier": 10, "is_premium": false}, "14609": {"tank_id": 14609, "name": "Leopard 1", "nation": 1, "type": "mediumTank", "tier": 10, "is_premium": false}, "14625": {"tank_id": 14625, "name": "T69", "nation": 2, "type": "mediumTank", "tier": 8, "is_premium": false}, "14721": {"tank_id": 14721, "name": "Strv 81", "nation": 8, "type": "mediumTank", "tier": 8, "is_premium": true}, "14865": {"tank_id": 14865, "name": "Leopard Prototyp A", "nation": 1, "type": "mediumTank", "tier": 9, "is_premium": false}, "14881": {"tank_id": 14881, "name": "T57 Heavy Tank", "nation": 2, "type": "heavyTank", "tier": 10, "is_premium": false}, "14977": {"tank_id": 14977, "name": "CS-63", "nation": 8, "type": "mediumTank", "tier": 10, "is_premium": true}, "15137": {"tank_id": 15137, "name": "T21", "nation": 2, "type": "lightTank", "tier": 6, "is_premium": true}, "15393": {"tank_id": 15393, "name": "T54E1", "nation": 2, "type": "mediumTank", "tier": 9, "is_premium": false}, "15441": {"tank_id": 15441, "name": "Chieftain/T95", "nation": 5, "type": "heavyTank", "tier": 8, "is_premium": true}, "15617": {"tank_id": 15617, "name": "Object 907", "nation": 0, "type": "mediumTank", "tier": 10, "is_premium": true}, "15649": {"tank_id": 15649, "name": "T71", "nation": 2, "type": "lightTank", "tier": 7, "is_premium": false}, "15697": {"tank_id": 15697, "name": "Chieftain Mk. 6", "nation": 5, "type": "heavyTank", "tier": 10, "is_premium": true}, "15889": {"tank_id": 15889, "name": "VK 30.02 (M)", "nation": 1, "type": "mediumTank", "tier": 6, "is_premium": true}, "15905": {"tank_id": 15905, "name": "M60", "nation": 2, "type": "mediumTank", "tier": 10, "is_premium": true}, "15937": {"tank_id": 15937, "name": "Renault R35", "nation": 4, "type": "mediumTank", "tier": 1, "is_premium": false}, "15953": {"tank_id": 15953, "name": "FV201 (A45)", "nation": 5, "type": "heavyTank", "tier": 7, "is_premium": true}, "16145": {"tank_id": 16145, "name": "Pz.Sfl. IVc", "nation": 1, "type": "AT-SPG", "tier": 5, "is_premium": true}, "16193": {"tank_id": 16193, "name": "M4A1 Revaloris\u00e9", "nation": 4, "type": "mediumTank", "tier": 8, "is_premium": true}, "16209": {"tank_id": 16209, "name": "Cruiser Mk. I", "nation": 5, "type": "lightTank", "tier": 1, "is_premium": false}, "16257": {"tank_id": 16257, "name": "Outcast", "nation": 8, "type": "heavyTank", "tier": 6, "is_premium": true}, "16401": {"tank_id": 16401, "name": "Waffentr\u00e4ger auf Pz. IV", "nation": 1, "type": "AT-SPG", "tier": 9, "is_premium": false}, "16449": {"tank_id": 16449, "name": "AMX 13 57", "nation": 4, "type": "lightTank", "tier": 7, "is_premium": true}, "16465": {"tank_id": 16465, "name": "Cruiser Mk. I", "nation": 5, "type": "lightTank", "tier": 1, "is_premium": false}, "16641": {"tank_id": 16641, "name": "MT-25", "nation": 0, "type": "lightTank", "tier": 6, "is_premium": false}, "16657": {"tank_id": 16657, "name": "Rhm.-Borsig Waffentr\u00e4ger", "nation": 1, "type": "AT-SPG", "tier": 8, "is_premium": false}, "16673": {"tank_id": 16673, "name": "T37", "nation": 2, "type": "lightTank", "tier": 6, "is_premium": false}, "16705": {"tank_id": 16705, "name": "AMX M4 mle. 49", "nation": 4, "type": "heavyTank", "tier": 8, "is_premium": true}, "16897": {"tank_id": 16897, "name": "Object 140", "nation": 0, "type": "mediumTank", "tier": 10, "is_premium": false}, "17169": {"tank_id": 17169, "name": "Pz.Kpfw. IV Ausf. A", "nation": 1, "type": "mediumTank", "tier": 3, "is_premium": true}, "17217": {"tank_id": 17217, "name": "Eraser BP44", "nation": 4, "type": "heavyTank", "tier": 6, "is_premium": true}, "17233": {"tank_id": 17233, "name": "FV4004 Conway", "nation": 5, "type": "AT-SPG", "tier": 9, "is_premium": false}, "17425": {"tank_id": 17425, "name": "Pz.Kpfw. IV Ausf. D", "nation": 1, "type": "mediumTank", "tier": 4, "is_premium": false}, "17473": {"tank_id": 17473, "name": "AMX Defender", "nation": 4, "type": "lightTank", "tier": 8, "is_premium": true}, "17489": {"tank_id": 17489, "name": "Charioteer", "nation": 5, "type": "AT-SPG", "tier": 8, "is_premium": false}, "17729": {"tank_id": 17729, "name": "Somua SM", "nation": 4, "type": "heavyTank", "tier": 8, "is_premium": true}, "17745": {"tank_id": 17745, "name": "FV217 Badger", "nation": 5, "type": "AT-SPG", "tier": 10, "is_premium": true}, "17953": {"tank_id": 17953, "name": "M41 Walker Bulldog", "nation": 2, "type": "lightTank", "tier": 7, "is_premium": false}, "17985": {"tank_id": 17985, "name": "Bretagne Panther", "nation": 4, "type": "mediumTank", "tier": 6, "is_premium": true}, "18001": {"tank_id": 18001, "name": "FV4005", "nation": 5, "type": "AT-SPG", "tier": 10, "is_premium": false}, "18177": {"tank_id": 18177, "name": "T-54 ltwt.", "nation": 0, "type": "lightTank", "tier": 8, "is_premium": false}, "18209": {"tank_id": 18209, "name": "T49", "nation": 2, "type": "lightTank", "tier": 8, "is_premium": false}, "18241": {"tank_id": 18241, "name": "Bat.-Ch\u00e2tillon Bourrasque", "nation": 4, "type": "mediumTank", "tier": 8, "is_premium": true}, "18257": {"tank_id": 18257, "name": "Challenger", "nation": 5, "type": "AT-SPG", "tier": 7, "is_premium": false}, "18433": {"tank_id": 18433, "name": "LTTB", "nation": 0, "type": "lightTank", "tier": 7, "is_premium": false}, "18449": {"tank_id": 18449, "name": "Sp\u00e4hpanzer Ru 251", "nation": 1, "type": "lightTank", "tier": 8, "is_premium": false}, "18465": {"tank_id": 18465, "name": "M2 Light Tank", "nation": 2, "type": "lightTank", "tier": 1, "is_premium": false}, "18497": {"tank_id": 18497, "name": "Lorraine 40 t Fearless", "nation": 4, "type": "mediumTank", "tier": 8, "is_premium": true}, "18513": {"tank_id": 18513, "name": "Chimera", "nation": 5, "type": "mediumTank", "tier": 8, "is_premium": true}, "18689": {"tank_id": 18689, "name": "T-70/57", "nation": 0, "type": "lightTank", "tier": 3, "is_premium": true}, "18721": {"tank_id": 18721, "name": "M2 Light Tank", "nation": 2, "type": "lightTank", "tier": 1, "is_premium": false}, "18753": {"tank_id": 18753, "name": "AMX Canon d'assaut 105", "nation": 4, "type": "AT-SPG", "tier": 8, "is_premium": true}, "18769": {"tank_id": 18769, "name": "Caernarvon Action X", "nation": 5, "type": "heavyTank", "tier": 8, "is_premium": true}, "18945": {"tank_id": 18945, "name": "ISU-130", "nation": 0, "type": "AT-SPG", "tier": 8, "is_premium": true}, "18961": {"tank_id": 18961, "name": "Sp\u00e4hpanzer SP I C", "nation": 1, "type": "lightTank", "tier": 7, "is_premium": false}, "18977": {"tank_id": 18977, "name": "T95E6", "nation": 2, "type": "heavyTank", "tier": 10, "is_premium": true}, "19009": {"tank_id": 19009, "name": "AMXmas", "nation": 4, "type": "lightTank", "tier": 6, "is_premium": true}, "19025": {"tank_id": 19025, "name": "Defender Mk. 1", "nation": 5, "type": "mediumTank", "tier": 8, "is_premium": true}, "19201": {"tank_id": 19201, "name": "T-26", "nation": 0, "type": "lightTank", "tier": 1, "is_premium": false}, "19217": {"tank_id": 19217, "name": "Grille 15", "nation": 1, "type": "AT-SPG", "tier": 10, "is_premium": false}, "19233": {"tank_id": 19233, "name": "Chrysler K", "nation": 2, "type": "heavyTank", "tier": 8, "is_premium": true}, "19265": {"tank_id": 19265, "name": "Charles", "nation": 4, "type": "AT-SPG", "tier": 6, "is_premium": true}, "19281": {"tank_id": 19281, "name": "Super Conqueror", "nation": 5, "type": "heavyTank", "tier": 10, "is_premium": true}, "19457": {"tank_id": 19457, "name": "T-26", "nation": 0, "type": "lightTank", "tier": 1, "is_premium": false}, "19473": {"tank_id": 19473, "name": "Krupp-38(D)", "nation": 1, "type": "mediumTank", "tier": 5, "is_premium": true}, "19489": {"tank_id": 19489, "name": "T28 Defender", "nation": 2, "type": "AT-SPG", "tier": 8, "is_premium": true}, "19521": {"tank_id": 19521, "name": "#france_vehicles:Bat-Chat-25CL", "nation": 4, "type": "lightTank", "tier": 9, "is_premium": true}, "19537": {"tank_id": 19537, "name": "Vickers Light 105", "nation": 5, "type": "lightTank", "tier": 10, "is_premium": false}, "19713": {"tank_id": 19713, "name": "Loza's M4-A2 Sherman", "nation": 0, "type": "mediumTank", "tier": 6, "is_premium": true}, "19729": {"tank_id": 19729, "name": "Tiger 131", "nation": 1, "type": "heavyTank", "tier": 6, "is_premium": true}, "19745": {"tank_id": 19745, "name": "T26E5", "nation": 2, "type": "heavyTank", "tier": 8, "is_premium": true}, "19777": {"tank_id": 19777, "name": "AltProto AMX 30", "nation": 4, "type": "mediumTank", "tier": 8, "is_premium": true}, "19793": {"tank_id": 19793, "name": "Vickers Cruiser", "nation": 5, "type": "lightTank", "tier": 9, "is_premium": false}, "19969": {"tank_id": 19969, "name": "T-22 medium", "nation": 0, "type": "mediumTank", "tier": 10, "is_premium": true}, "19985": {"tank_id": 19985, "name": "Rheinmetall Skorpion G", "nation": 1, "type": "AT-SPG", "tier": 8, "is_premium": true}, "20001": {"tank_id": 20001, "name": "T92E1", "nation": 2, "type": "lightTank", "tier": 9, "is_premium": false}, "20033": {"tank_id": 20033, "name": "Char Futur 4", "nation": 4, "type": "mediumTank", "tier": 9, "is_premium": true}, "20049": {"tank_id": 20049, "name": "FV301", "nation": 5, "type": "lightTank", "tier": 8, "is_premium": false}, "20241": {"tank_id": 20241, "name": "Pz.Kpfw. II", "nation": 1, "type": "lightTank", "tier": 1, "is_premium": false}, "20257": {"tank_id": 20257, "name": "XM551 Sheridan", "nation": 2, "type": "lightTank", "tier": 10, "is_premium": false}, "20289": {"tank_id": 20289, "name": "Pirate", "nation": 4, "type": "AT-SPG", "tier": 6, "is_premium": true}, "20305": {"tank_id": 20305, "name": "Centurion Mk. 5/1 RAAC", "nation": 5, "type": "mediumTank", "tier": 8, "is_premium": true}, "20481": {"tank_id": 20481, "name": "Object 252U", "nation": 0, "type": "heavyTank", "tier": 8, "is_premium": true}, "20497": {"tank_id": 20497, "name": "VK 100.01 (P)", "nation": 1, "type": "heavyTank", "tier": 8, "is_premium": false}, "20513": {"tank_id": 20513, "name": "T54E2", "nation": 2, "type": "heavyTank", "tier": 8, "is_premium": true}, "20545": {"tank_id": 20545, "name": "Renault G1", "nation": 4, "type": "heavyTank", "tier": 5, "is_premium": true}, "20561": {"tank_id": 20561, "name": "Turtle Mk. I", "nation": 5, "type": "AT-SPG", "tier": 8, "is_premium": true}, "20737": {"tank_id": 20737, "name": "SU-130PM", "nation": 0, "type": "AT-SPG", "tier": 8, "is_premium": true}, "20753": {"tank_id": 20753, "name": "M\u00e4uschen", "nation": 1, "type": "heavyTank", "tier": 9, "is_premium": false}, "20769": {"tank_id": 20769, "name": "T25 Pilot Number 1", "nation": 2, "type": "mediumTank", "tier": 8, "is_premium": true}, "20817": {"tank_id": 20817, "name": "Explorer", "nation": 5, "type": "mediumTank", "tier": 6, "is_premium": true}, "20993": {"tank_id": 20993, "name": "T-2020", "nation": 0, "type": "heavyTank", "tier": 8, "is_premium": true}, "21009": {"tank_id": 21009, "name": "Panzer 58", "nation": 1, "type": "mediumTank", "tier": 8, "is_premium": true}, "21025": {"tank_id": 21025, "name": "T26E3 Eagle 7", "nation": 2, "type": "mediumTank", "tier": 7, "is_premium": true}, "21073": {"tank_id": 21073, "name": "Dreadnought", "nation": 5, "type": "AT-SPG", "tier": 6, "is_premium": true}, "21249": {"tank_id": 21249, "name": "Thunder", "nation": 0, "type": "heavyTank", "tier": 6, "is_premium": true}, "21265": {"tank_id": 21265, "name": "VK 168.01 (P)", "nation": 1, "type": "heavyTank", "tier": 8, "is_premium": true}, "21281": {"tank_id": 21281, "name": "Rudolph", "nation": 2, "type": "mediumTank", "tier": 6, "is_premium": true}, "21329": {"tank_id": 21329, "name": "GSOR 1008", "nation": 5, "type": "AT-SPG", "tier": 8, "is_premium": true}, "21505": {"tank_id": 21505, "name": "T-44-85", "nation": 0, "type": "mediumTank", "tier": 7, "is_premium": true}, "21521": {"tank_id": 21521, "name": "E 75 TS", "nation": 1, "type": "heavyTank", "tier": 8, "is_premium": true}, "21537": {"tank_id": 21537, "name": "Sherman Easy 8", "nation": 2, "type": "mediumTank", "tier": 6, "is_premium": true}, "21585": {"tank_id": 21585, "name": "Blasteroid", "nation": 5, "type": "heavyTank", "tier": 6, "is_premium": true}, "21761": {"tank_id": 21761, "name": "STG", "nation": 0, "type": "mediumTank", "tier": 8, "is_premium": true}, "21777": {"tank_id": 21777, "name": "VK 90.01 (P)", "nation": 1, "type": "heavyTank", "tier": 10, "is_premium": true}, "21793": {"tank_id": 21793, "name": "XM551 Sheridan Missile", "nation": 2, "type": "lightTank", "tier": 10, "is_premium": true}, "21841": {"tank_id": 21841, "name": "Caliban", "nation": 5, "type": "heavyTank", "tier": 8, "is_premium": true}, "22017": {"tank_id": 22017, "name": "T-34-85", "nation": 0, "type": "mediumTank", "tier": 6, "is_premium": true}, "22033": {"tank_id": 22033, "name": "Agent", "nation": 1, "type": "lightTank", "tier": 6, "is_premium": true}, "22049": {"tank_id": 22049, "name": "Magnus", "nation": 2, "type": "heavyTank", "tier": 6, "is_premium": true}, "22097": {"tank_id": 22097, "name": "Churchill VIII", "nation": 5, "type": "heavyTank", "tier": 6, "is_premium": true}, "22273": {"tank_id": 22273, "name": "Object 260", "nation": 0, "type": "heavyTank", "tier": 10, "is_premium": true}, "22289": {"tank_id": 22289, "name": "Tiger I", "nation": 1, "type": "heavyTank", "tier": 6, "is_premium": true}, "22305": {"tank_id": 22305, "name": "AE Phase I", "nation": 2, "type": "heavyTank", "tier": 9, "is_premium": true}, "22353": {"tank_id": 22353, "name": "Churchill W", "nation": 5, "type": "heavyTank", "tier": 6, "is_premium": true}, "22529": {"tank_id": 22529, "name": "LT-432", "nation": 0, "type": "lightTank", "tier": 8, "is_premium": true}, "22545": {"tank_id": 22545, "name": "Kanonenjagdpanzer 105", "nation": 1, "type": "AT-SPG", "tier": 8, "is_premium": true}, "22561": {"tank_id": 22561, "name": "TS-5", "nation": 2, "type": "AT-SPG", "tier": 8, "is_premium": true}, "22609": {"tank_id": 22609, "name": "Caernarvon Defender", "nation": 5, "type": "heavyTank", "tier": 8, "is_premium": true}, "22785": {"tank_id": 22785, "name": "Triumphant", "nation": 0, "type": "mediumTank", "tier": 6, "is_premium": true}, "22801": {"tank_id": 22801, "name": "Icebreaker", "nation": 1, "type": "heavyTank", "tier": 6, "is_premium": true}, "22817": {"tank_id": 22817, "name": "M-VI-Yoh", "nation": 2, "type": "heavyTank", "tier": 10, "is_premium": false}, "22865": {"tank_id": 22865, "name": "Charlemagne", "nation": 5, "type": "heavyTank", "tier": 8, "is_premium": true}, "23041": {"tank_id": 23041, "name": "T-34 shielded", "nation": 0, "type": "mediumTank", "tier": 5, "is_premium": true}, "23057": {"tank_id": 23057, "name": "Kunze Panzer", "nation": 1, "type": "lightTank", "tier": 7, "is_premium": true}, "23073": {"tank_id": 23073, "name": "M-V-Yoh", "nation": 2, "type": "heavyTank", "tier": 9, "is_premium": false}, "23121": {"tank_id": 23121, "name": "Cobra", "nation": 5, "type": "mediumTank", "tier": 9, "is_premium": true}, "23297": {"tank_id": 23297, "name": "Object 244", "nation": 0, "type": "heavyTank", "tier": 6, "is_premium": true}, "23313": {"tank_id": 23313, "name": "Kampfpanzer 50 t", "nation": 1, "type": "mediumTank", "tier": 10, "is_premium": true}, "23329": {"tank_id": 23329, "name": "M-III-Yoh", "nation": 2, "type": "heavyTank", "tier": 8, "is_premium": false}, "23553": {"tank_id": 23553, "name": "MS-1", "nation": 0, "type": "lightTank", "tier": 2, "is_premium": true}, "23569": {"tank_id": 23569, "name": "Pz. IV Gargoyle", "nation": 1, "type": "mediumTank", "tier": 5, "is_premium": true}, "23585": {"tank_id": 23585, "name": "M-VII-Yoh", "nation": 2, "type": "heavyTank", "tier": 7, "is_premium": false}, "23809": {"tank_id": 23809, "name": "Object 84", "nation": 0, "type": "lightTank", "tier": 9, "is_premium": false}, "23825": {"tank_id": 23825, "name": "Krupp-Steyr Waffentr\u00e4ger", "nation": 1, "type": "AT-SPG", "tier": 7, "is_premium": true}, "23841": {"tank_id": 23841, "name": "Super Hellcat", "nation": 2, "type": "AT-SPG", "tier": 7, "is_premium": true}, "24065": {"tank_id": 24065, "name": "LTG", "nation": 0, "type": "lightTank", "tier": 7, "is_premium": false}, "24081": {"tank_id": 24081, "name": "U-Panzer", "nation": 1, "type": "mediumTank", "tier": 6, "is_premium": true}, "24097": {"tank_id": 24097, "name": "BLTZ9000", "nation": 2, "type": "mediumTank", "tier": 6, "is_premium": true}, "24321": {"tank_id": 24321, "name": "T-100 LT", "nation": 0, "type": "lightTank", "tier": 10, "is_premium": false}, "24337": {"tank_id": 24337, "name": "M48A2 R\u00e4umpanzer", "nation": 1, "type": "mediumTank", "tier": 8, "is_premium": true}, "24577": {"tank_id": 24577, "name": "Object 268 Version 4", "nation": 0, "type": "AT-SPG", "tier": 10, "is_premium": true}, "24593": {"tank_id": 24593, "name": "Keiler", "nation": 1, "type": "heavyTank", "tier": 8, "is_premium": true}, "24609": {"tank_id": 24609, "name": "Concept 1B", "nation": 2, "type": "heavyTank", "tier": 10, "is_premium": true}, "24849": {"tank_id": 24849, "name": "Kryos", "nation": 1, "type": "AT-SPG", "tier": 6, "is_premium": true}, "24865": {"tank_id": 24865, "name": "Scepter", "nation": 2, "type": "heavyTank", "tier": 8, "is_premium": true}, "25089": {"tank_id": 25089, "name": "Object 752", "nation": 0, "type": "heavyTank", "tier": 9, "is_premium": true}, "25105": {"tank_id": 25105, "name": "Barkhan", "nation": 1, "type": "heavyTank", "tier": 6, "is_premium": true}, "25345": {"tank_id": 25345, "name": "Object 274a", "nation": 0, "type": "mediumTank", "tier": 8, "is_premium": true}, "25361": {"tank_id": 25361, "name": "Waffentr\u00e4ger Ritter", "nation": 1, "type": "AT-SPG", "tier": 9, "is_premium": true}, "25377": {"tank_id": 25377, "name": "T77", "nation": 2, "type": "heavyTank", "tier": 8, "is_premium": true}, "25601": {"tank_id": 25601, "name": "IS-2 shielded", "nation": 0, "type": "heavyTank", "tier": 7, "is_premium": true}, "25617": {"tank_id": 25617, "name": "HWK 30", "nation": 1, "type": "lightTank", "tier": 8, "is_premium": true}, "25633": {"tank_id": 25633, "name": "M-IV-Y", "nation": 2, "type": "heavyTank", "tier": 8, "is_premium": true}, "25857": {"tank_id": 25857, "name": "Object 777 Version II", "nation": 0, "type": "heavyTank", "tier": 10, "is_premium": true}, "25889": {"tank_id": 25889, "name": "Ranger", "nation": 2, "type": "lightTank", "tier": 6, "is_premium": true}, "26113": {"tank_id": 26113, "name": "Object 452K", "nation": 0, "type": "heavyTank", "tier": 9, "is_premium": true}, "26129": {"tank_id": 26129, "name": "Epsilon", "nation": 1, "type": "AT-SPG", "tier": 6, "is_premium": true}, "26145": {"tank_id": 26145, "name": "High Score", "nation": 2, "type": "mediumTank", "tier": 5, "is_premium": true}, "26401": {"tank_id": 26401, "name": "Enforcer", "nation": 2, "type": "AT-SPG", "tier": 6, "is_premium": true}, "26641": {"tank_id": 26641, "name": "Kampfpanzer 07 RH", "nation": 1, "type": "mediumTank", "tier": 8, "is_premium": true}, "26657": {"tank_id": 26657, "name": "ASTRON Rex 105 mm", "nation": 2, "type": "mediumTank", "tier": 8, "is_premium": true}, "26913": {"tank_id": 26913, "name": "Frosty", "nation": 2, "type": "lightTank", "tier": 6, "is_premium": true}, "27169": {"tank_id": 27169, "name": "Cyborg", "nation": 2, "type": "heavyTank", "tier": 6, "is_premium": true}, "27425": {"tank_id": 27425, "name": "TL-7-120", "nation": 2, "type": "heavyTank", "tier": 9, "is_premium": false}, "27681": {"tank_id": 27681, "name": "Pawlack Tank", "nation": 2, "type": "heavyTank", "tier": 6, "is_premium": true}, "27937": {"tank_id": 27937, "name": "T49 Fearless", "nation": 2, "type": "lightTank", "tier": 8, "is_premium": true}, "28193": {"tank_id": 28193, "name": "TS-60", "nation": 2, "type": "AT-SPG", "tier": 8, "is_premium": true}, "28449": {"tank_id": 28449, "name": "T42", "nation": 2, "type": "mediumTank", "tier": 7, "is_premium": true}, "51201": {"tank_id": 51201, "name": "KV-220 Beta-Test", "nation": 0, "type": "heavyTank", "tier": 5, "is_premium": true}, "51457": {"tank_id": 51457, "name": "Matilda IV", "nation": 0, "type": "mediumTank", "tier": 5, "is_premium": true}, "51473": {"tank_id": 51473, "name": "Pz.Kpfw. V/IV", "nation": 1, "type": "mediumTank", "tier": 5, "is_premium": true}, "51489": {"tank_id": 51489, "name": "T2 Light Tank", "nation": 2, "type": "lightTank", "tier": 2, "is_premium": true}, "51713": {"tank_id": 51713, "name": "Churchill III", "nation": 0, "type": "heavyTank", "tier": 5, "is_premium": true}, "51729": {"tank_id": 51729, "name": "Pz.Kpfw. II Ausf. J", "nation": 1, "type": "lightTank", "tier": 3, "is_premium": true}, "51745": {"tank_id": 51745, "name": "Ram II", "nation": 2, "type": "mediumTank", "tier": 5, "is_premium": true}, "51809": {"tank_id": 51809, "name": "Type 98 Ke-Ni Otsu", "nation": 6, "type": "lightTank", "tier": 3, "is_premium": true}, "51985": {"tank_id": 51985, "name": "Pz.Kpfw. S35 739 (f)", "nation": 1, "type": "mediumTank", "tier": 3, "is_premium": true}, "52065": {"tank_id": 52065, "name": "Hetzer Kame SP", "nation": 6, "type": "AT-SPG", "tier": 4, "is_premium": true}, "52225": {"tank_id": 52225, "name": "BT-SV", "nation": 0, "type": "lightTank", "tier": 3, "is_premium": true}, "52241": {"tank_id": 52241, "name": "Pz.Kpfw. B2 740 (f)", "nation": 1, "type": "heavyTank", "tier": 4, "is_premium": true}, "52257": {"tank_id": 52257, "name": "M4A2E4 Sherman", "nation": 2, "type": "mediumTank", "tier": 5, "is_premium": true}, "52481": {"tank_id": 52481, "name": "Valentine II", "nation": 0, "type": "lightTank", "tier": 4, "is_premium": true}, "52497": {"tank_id": 52497, "name": "Pz.Kpfw. 38H 735 (f)", "nation": 1, "type": "lightTank", "tier": 2, "is_premium": true}, "52513": {"tank_id": 52513, "name": "M6A2E1", "nation": 2, "type": "heavyTank", "tier": 7, "is_premium": true}, "52561": {"tank_id": 52561, "name": "Tortoise", "nation": 5, "type": "AT-SPG", "tier": 9, "is_premium": false}, "52737": {"tank_id": 52737, "name": "M3 Light", "nation": 0, "type": "lightTank", "tier": 3, "is_premium": true}, "52769": {"tank_id": 52769, "name": "M22 Locust", "nation": 2, "type": "lightTank", "tier": 3, "is_premium": true}, "52993": {"tank_id": 52993, "name": "A-32", "nation": 0, "type": "mediumTank", "tier": 4, "is_premium": true}, "53025": {"tank_id": 53025, "name": "M6A2E1 EXP", "nation": 2, "type": "heavyTank", "tier": 8, "is_premium": true}, "53249": {"tank_id": 53249, "name": "KV-5", "nation": 0, "type": "heavyTank", "tier": 8, "is_premium": true}, "53505": {"tank_id": 53505, "name": "T-127", "nation": 0, "type": "lightTank", "tier": 3, "is_premium": true}, "53537": {"tank_id": 53537, "name": "T1E6", "nation": 2, "type": "lightTank", "tier": 2, "is_premium": true}, "53585": {"tank_id": 53585, "name": "Matilda Black Prince", "nation": 5, "type": "mediumTank", "tier": 5, "is_premium": true}, "53761": {"tank_id": 53761, "name": "SU-85I", "nation": 0, "type": "AT-SPG", "tier": 5, "is_premium": true}, "53841": {"tank_id": 53841, "name": "TOG II*", "nation": 5, "type": "heavyTank", "tier": 6, "is_premium": true}, "54097": {"tank_id": 54097, "name": "AT 15A", "nation": 5, "type": "AT-SPG", "tier": 7, "is_premium": true}, "54273": {"tank_id": 54273, "name": "SU-76I", "nation": 0, "type": "AT-SPG", "tier": 3, "is_premium": true}, "54289": {"tank_id": 54289, "name": "L\u00f6we", "nation": 1, "type": "heavyTank", "tier": 8, "is_premium": true}, "54353": {"tank_id": 54353, "name": "Excelsior", "nation": 5, "type": "heavyTank", "tier": 5, "is_premium": true}, "54529": {"tank_id": 54529, "name": "Tetrarch", "nation": 0, "type": "lightTank", "tier": 2, "is_premium": true}, "54545": {"tank_id": 54545, "name": "T-25", "nation": 1, "type": "mediumTank", "tier": 5, "is_premium": true}, "54785": {"tank_id": 54785, "name": "SU-100Y", "nation": 0, "type": "AT-SPG", "tier": 6, "is_premium": true}, "54801": {"tank_id": 54801, "name": "T-15", "nation": 1, "type": "lightTank", "tier": 3, "is_premium": true}, "54865": {"tank_id": 54865, "name": "Light Mk. VIC", "nation": 5, "type": "lightTank", "tier": 2, "is_premium": true}, "55057": {"tank_id": 55057, "name": "Pz.Kpfw. IV hydrostat.", "nation": 1, "type": "mediumTank", "tier": 5, "is_premium": true}, "55073": {"tank_id": 55073, "name": "T7 Combat Car", "nation": 2, "type": "lightTank", "tier": 2, "is_premium": true}, "55297": {"tank_id": 55297, "name": "SU-122-44", "nation": 0, "type": "AT-SPG", "tier": 7, "is_premium": true}, "55313": {"tank_id": 55313, "name": "8,8 cm Pak 43 Jagdtiger", "nation": 1, "type": "AT-SPG", "tier": 8, "is_premium": true}, "55889": {"tank_id": 55889, "name": "Cromwell B", "nation": 5, "type": "mediumTank", "tier": 6, "is_premium": true}, "56097": {"tank_id": 56097, "name": "M4A3E8 Fury", "nation": 2, "type": "mediumTank", "tier": 6, "is_premium": true}, "56577": {"tank_id": 56577, "name": "LTP", "nation": 0, "type": "lightTank", "tier": 3, "is_premium": true}, "56609": {"tank_id": 56609, "name": "T28 Concept", "nation": 2, "type": "AT-SPG", "tier": 7, "is_premium": true}, "57105": {"tank_id": 57105, "name": "Dicker Max", "nation": 1, "type": "AT-SPG", "tier": 6, "is_premium": true}, "57361": {"tank_id": 57361, "name": "Pz.Kpfw. IV Schmalturm", "nation": 1, "type": "mediumTank", "tier": 6, "is_premium": true}, "57617": {"tank_id": 57617, "name": "Panther/M10", "nation": 1, "type": "mediumTank", "tier": 7, "is_premium": true}, "58641": {"tank_id": 58641, "name": "VK 72.01 (K)", "nation": 1, "type": "heavyTank", "tier": 10, "is_premium": false}, "58881": {"tank_id": 58881, "name": "IS-5 (Object 730)", "nation": 0, "type": "heavyTank", "tier": 8, "is_premium": true}, "59137": {"tank_id": 59137, "name": "IS-2 (1945)", "nation": 0, "type": "heavyTank", "tier": 7, "is_premium": true}, "59649": {"tank_id": 59649, "name": "ISU-122S", "nation": 0, "type": "AT-SPG", "tier": 7, "is_premium": true}, "59665": {"tank_id": 59665, "name": "Gro\u00dftraktor - Krupp", "nation": 1, "type": "heavyTank", "tier": 3, "is_premium": true}, "59905": {"tank_id": 59905, "name": "T-54 first prototype", "nation": 0, "type": "mediumTank", "tier": 8, "is_premium": true}, "60161": {"tank_id": 60161, "name": "IS-2Sh", "nation": 0, "type": "heavyTank", "tier": 8, "is_premium": true}, "60177": {"tank_id": 60177, "name": "Panther mit 8,8 cm L/71", "nation": 1, "type": "mediumTank", "tier": 8, "is_premium": true}, "60417": {"tank_id": 60417, "name": "IS-3 Defender", "nation": 0, "type": "heavyTank", "tier": 8, "is_premium": true}, "60929": {"tank_id": 60929, "name": "BT-7 artillery", "nation": 0, "type": "lightTank", "tier": 3, "is_premium": true}, "62737": {"tank_id": 62737, "name": "leKpz M 41 90 mm", "nation": 1, "type": "lightTank", "tier": 8, "is_premium": true}, "62977": {"tank_id": 62977, "name": "T-44-100", "nation": 0, "type": "mediumTank", "tier": 8, "is_premium": true}, "62993": {"tank_id": 62993, "name": "VK 45.03", "nation": 1, "type": "heavyTank", "tier": 7, "is_premium": true}, "63553": {"tank_id": 63553, "name": "AMX Chasseur de chars", "nation": 4, "type": "mediumTank", "tier": 8, "is_premium": true}, "63585": {"tank_id": 63585, "name": "Tiger Kuromorimine SP", "nation": 6, "type": "heavyTank", "tier": 6, "is_premium": true}, "63601": {"tank_id": 63601, "name": "Dracula", "nation": 7, "type": "mediumTank", "tier": 7, "is_premium": true}, "63841": {"tank_id": 63841, "name": "Panzer IV Ankou Special", "nation": 6, "type": "mediumTank", "tier": 5, "is_premium": true}, "64001": {"tank_id": 64001, "name": "T-34-85 Rudy", "nation": 0, "type": "mediumTank", "tier": 7, "is_premium": true}, "64017": {"tank_id": 64017, "name": "Tankenstein", "nation": 1, "type": "heavyTank", "tier": 7, "is_premium": true}, "64065": {"tank_id": 64065, "name": "FCM 50 t", "nation": 4, "type": "mediumTank", "tier": 8, "is_premium": true}, "64081": {"tank_id": 64081, "name": "Mk I* Heavy Tank", "nation": 5, "type": "heavyTank", "tier": 1, "is_premium": true}, "64257": {"tank_id": 64257, "name": "T-34-85 Victory", "nation": 0, "type": "mediumTank", "tier": 6, "is_premium": true}, "64273": {"tank_id": 64273, "name": "Snowstorm Jagdtiger 8.8", "nation": 1, "type": "AT-SPG", "tier": 8, "is_premium": true}, "64337": {"tank_id": 64337, "name": "AC IV Sentinel", "nation": 5, "type": "mediumTank", "tier": 6, "is_premium": true}, "64529": {"tank_id": 64529, "name": "E 25", "nation": 1, "type": "AT-SPG", "tier": 7, "is_premium": true}, "64561": {"tank_id": 64561, "name": "112 Glacial", "nation": 3, "type": "heavyTank", "tier": 8, "is_premium": true}, "64593": {"tank_id": 64593, "name": "Angry Connor", "nation": 5, "type": "AT-SPG", "tier": 5, "is_premium": true}, "64769": {"tank_id": 64769, "name": "IS-6 Fearless", "nation": 0, "type": "heavyTank", "tier": 8, "is_premium": true}, "64785": {"tank_id": 64785, "name": "Pz.Kpfw. II", "nation": 1, "type": "lightTank", "tier": 1, "is_premium": false}, "64801": {"tank_id": 64801, "name": "T34 Independence", "nation": 2, "type": "heavyTank", "tier": 8, "is_premium": true}, "64849": {"tank_id": 64849, "name": "AC 1 Sentinel", "nation": 5, "type": "mediumTank", "tier": 4, "is_premium": true}, "65025": {"tank_id": 65025, "name": "MS-1 mod. 1", "nation": 0, "type": "lightTank", "tier": 1, "is_premium": false}, "65041": {"tank_id": 65041, "name": "Leichttraktor", "nation": 1, "type": "lightTank", "tier": 1, "is_premium": false}, "65057": {"tank_id": 65057, "name": "T1 Cunningham", "nation": 2, "type": "lightTank", "tier": 1, "is_premium": false}, "65105": {"tank_id": 65105, "name": "Vickers Medium Mk. I", "nation": 5, "type": "mediumTank", "tier": 1, "is_premium": false}, "65281": {"tank_id": 65281, "name": "MS-1 mod. 1", "nation": 0, "type": "lightTank", "tier": 1, "is_premium": false}, "65297": {"tank_id": 65297, "name": "Leichttraktor", "nation": 1, "type": "lightTank", "tier": 1, "is_premium": false}, "65313": {"tank_id": 65313, "name": "T1 Cunningham", "nation": 2, "type": "lightTank", "tier": 1, "is_premium": false}, "65329": {"tank_id": 65329, "name": "Type 62", "nation": 3, "type": "lightTank", "tier": 7, "is_premium": true}, "65361": {"tank_id": 65361, "name": "Vickers Medium Mk. I", "nation": 5, "type": "mediumTank", "tier": 1, "is_premium": false}, "65377": {"tank_id": 65377, "name": "Type 3 Chi-Nu Kai", "nation": 6, "type": "mediumTank", "tier": 5, "is_premium": true}}} \ No newline at end of file From b001bb44be9deb83b300d27806ac0abddfa791eb Mon Sep 17 00:00:00 2001 From: jylpah Date: Sun, 10 Sep 2023 20:02:59 +0300 Subject: [PATCH 5/5] remove legacy scripts, use Jylpah/blitz-replays --- extract_tankopedia.py | 248 ------------------------------------------ get_tankopedia.py | 91 ---------------- 2 files changed, 339 deletions(-) delete mode 100755 extract_tankopedia.py delete mode 100755 get_tankopedia.py diff --git a/extract_tankopedia.py b/extract_tankopedia.py deleted file mode 100755 index 01330b3..0000000 --- a/extract_tankopedia.py +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/env python3 -## PYTHON VERSION MUST BE 3.7 OR HIGHER - -# Extract tankopedia data in WG API JSON format from Blitz app files (Android APK unzipped) - -from os import path -import aiofiles # type: ignore -import xmltodict # type: ignore -from asyncio import set_event_loop_policy, run, create_task, get_event_loop_policy -import sys, argparse, json, os, inspect, asyncio, re, logging, time, configparser -from collections import OrderedDict -import blitzutils as bu -from blitzutils import WG - -logging.getLogger("asyncio").setLevel(logging.DEBUG) - -FILE_CONFIG = 'blitzstats.ini' - -# BLITZAPP_STRINGS='/assets/Data/Strings/en.yaml' -# BLITZAPP_VEHICLES_DIR='/assets/Data/XML/item_defs/vehicles/' -BLITZAPP_STRINGS='Data/Strings/en.yaml' -BLITZAPP_VEHICLES_DIR='Data/XML/item_defs/vehicles/' -BLITZAPP_VEHICLE_FILE='list.xml' - -wg : WG | None = None - -## main() ------------------------------------------------------------- -async def main() -> None: - global wg - # set the directory for the script - os.chdir(os.path.dirname(sys.argv[0])) - - ## Read config - BLITZAPP_FOLDER = '.' - try: - if os.path.isfile(FILE_CONFIG): - config = configparser.ConfigParser() - config.read(FILE_CONFIG) - - configOptions = config['EXTRACT_TANKOPEDIA'] - BLITZAPP_FOLDER = configOptions.get('blitz_app_dir', BLITZAPP_FOLDER) - except: - pass - - parser = argparse.ArgumentParser(description='Extract Tankopedia data from Blitz game files') - parser.add_argument('blitz_app_base', type=str, nargs='?', metavar="BLITZAPP_FOLDER", default=BLITZAPP_FOLDER, help='Base dir of the Blitz App files') - parser.add_argument('tanks', type=str, default='tanks.json', nargs='?', metavar="TANKS_FILE", help='File to write Tankopedia') - parser.add_argument('maps', type=str, default='maps.json', nargs='?', metavar='MAPS_FILE', help='File to write map names') - arggroup = parser.add_mutually_exclusive_group() - arggroup.add_argument('-d', '--debug', action='store_true', default=False, help='Debug mode') - arggroup.add_argument('-v', '--verbose', action='store_true', default=False, help='Verbose mode') - arggroup.add_argument('-s', '--silent', action='store_true', default=False, help='Silent mode') - - args = parser.parse_args() - bu.set_log_level(args.silent, args.verbose, args.debug) - - wg = WG() - - tasks = [] - for nation in wg.NATION: - tasks.append(asyncio.create_task(extract_tanks(args.blitz_app_base, nation))) - - tanklist = [] - for tanklist_tmp in await asyncio.gather(*tasks): - tanklist.extend(tanklist_tmp) - - tank_strs, map_strs = await read_user_strs(args.blitz_app_base) - - json_data = None - userStrs = {} - tanks = {} - if os.path.exists(args.tanks): - try: - async with aiofiles.open(args.tanks) as infile: - json_data = json.loads(await infile.read()) - userStrs = json_data['userStr'] - tanks = json_data['data'] - except Exception as err: - bu.error('Unexpected error when reading file: ' + args.tanks + ' : ' + str(err)) - - async with aiofiles.open(args.tanks, 'w', encoding="utf8") as outfile: - new_tanks, new_userStrs = await convert_tank_names(tanklist, tank_strs) - # merge old and new tankopedia - tanks.update(new_tanks) - userStrs.update(new_userStrs) - tankopedia : OrderedDict[str, str| int | dict ]= OrderedDict() - tankopedia['status'] = 'ok' - tankopedia['meta'] = { "count": len(tanks) } - tankopedia['data'] = bu.sort_dict(tanks, number=True) - tankopedia['userStr'] = bu.sort_dict(userStrs) - bu.verbose_std('New tankopedia \'' + args.tanks + '\' contains ' + str(len(tanks)) + ' tanks') - bu.verbose_std('New tankopedia \'' + args.tanks + '\' contains ' + str(len(userStrs)) + ' tank strings') - await outfile.write(json.dumps(tankopedia, ensure_ascii=False, indent=4, sort_keys=False)) - - if args.maps is not None: - maps = {} - if os.path.exists(args.maps): - try: - async with aiofiles.open(args.maps) as infile: - maps = json.loads(await infile.read()) - except Exception as err: - bu.error('Unexpected error when reading file: ' + args.maps + ' : ' + str(err)) - # merge old and new map data - maps.update(map_strs) - async with aiofiles.open(args.maps, 'w', encoding="utf8") as outfile: - bu.verbose_std('New maps file \'' + args.maps + '\' contains ' + str(len(maps)) + ' maps') - await outfile.write(json.dumps(maps, ensure_ascii=False, indent=4, sort_keys=True)) - - return None - -async def extract_tanks(blitz_app_base : str, nation: str): - - tanks = [] - # WG has changed the location of Data directory - at least in steam client - if path.isdir(path.join(blitz_app_base, 'assets')): - blitz_app_base = path.join(blitz_app_base, 'assets') - list_xml_file = path.join(blitz_app_base, BLITZAPP_VEHICLES_DIR ,nation ,BLITZAPP_VEHICLE_FILE) - if not path.isfile(list_xml_file): - print('ERROR: cannot open ' + list_xml_file) - return None - bu.debug('Opening file: ' + list_xml_file + ' (Nation: ' + nation + ')') - async with aiofiles.open(list_xml_file, 'r', encoding="utf8") as f: - try: - tankList = xmltodict.parse(await f.read()) - for data in tankList['root'].keys(): - tank_xml = tankList['root'][data] - tank : dict[str, bool | int | str]= dict() - tank['is_premium'] = issubclass(type(tank_xml['price']), dict) - tank['nation'] = nation - tank['tank_id'] = get_tank_id(nation, int(tank_xml['id'])) - tank['tier'] = int(tank_xml['level']) - tank['type'] = get_tank_type(tank_xml['tags']) - tank['userStr'] = tank_xml['userString'] - #bu.debug('Reading tank string: ' + tank['userStr'], force=True) - tanks.append(tank) - except Exception as err: - bu.error(err) - sys.exit(2) - return tanks - -async def read_user_strs(blitz_app_base : str) -> tuple[dict, dict]: - """Read user strings to convert map and tank names""" - tank_strs = {} - map_strs = {} - filename = path.join(blitz_app_base, BLITZAPP_STRINGS) - bu.debug('Opening file: ' + filename + ' for reading UserStrings') - try: - async with aiofiles.open(filename, 'r', encoding="utf8") as f: - p_tank = re.compile('^"(#\\w+?_vehicles:.+?)": "(.+)"$') - p_map = re.compile('^"#maps:(.+?):.+?: "(.+?)"$') - - async for l in f: - m = p_tank.match(l) - if m is not None: - tank_strs[m.group(1)] = m.group(2) - - m = p_map.match(l) - if m is not None and m.group(2) != 'Macragge': - map_strs[m.group(1)] = m.group(2) - - except Exception as err: - bu.error(err) - sys.exit(1) - - return tank_strs, map_strs - -async def convert_tank_names(tanklist : list, tank_strs: dict) -> tuple[dict, dict]: - """Convert tank names for Tankopedia""" - tankopedia = {} - userStrs = {} - - bu.debug(f'tank_strs:') - for key, value in tank_strs.items(): - bu.debug(f'{key}: {value}') - bu.debug('---------') - try: - for tank in tanklist: - try: - bu.debug(f'tank: {tank}') - if tank['userStr'] in tank_strs: - tank['name'] = tank_strs[tank['userStr']] - else: - tank['name'] = tank['userStr'].split(':')[1] - tank.pop('userStr', None) - tank_tmp = OrderedDict() - for key in sorted(tank.keys()): - tank_tmp[key] = tank[key] - tankopedia[str(tank['tank_id'])] = tank_tmp - except: - bu.error(f'Could not process tank: {tank}') - - for tank_str in tank_strs: - skip = False - key = tank_str.split(':')[1] - bu.debug('Tank string: ' + key + ' = ' + tank_strs[tank_str]) - re_strs = [r'^Chassis_', r'^Turret_', r'^_', r'_short$' ] - for re_str in re_strs: - p = re.compile(re_str) - if p.match(key): - skip = True - break - if skip: - continue - - userStrs[key] = tank_strs[tank_str] - - # sorting - tankopedia_sorted = OrderedDict() - for tank_id in sorted(tankopedia.keys(), key=int): - tankopedia_sorted[str(tank_id)] = tankopedia[str(tank_id)] - - userStrs_sorted = OrderedDict() - for userStr in sorted(userStrs.keys()): - userStrs_sorted[userStr] = userStrs[userStr] - # bu.debug('Tank strings: ' + str(len(userStrs_sorted))) - - except Exception as err: - bu.error(err) - sys.exit(1) - - return tankopedia_sorted, userStrs_sorted - - -def get_tank_id(nation: str, tank_id : int) -> int: - assert wg is not None, "wg has to be set" - return (tank_id << 8) + (wg.NATION_ID[nation] << 4) + 1 - - -def get_tank_type(tagstr : str): - assert wg is not None, "wg has to be set" - tags = tagstr.split(' ') - for t_type in wg.TANK_TYPE: - if tags[0] == t_type: - return t_type - return None - -### main() -if __name__ == "__main__": - # To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp - if sys.platform.startswith("win") and sys.version_info >= (3, 8): - try: - from asyncio import WindowsSelectorEventLoopPolicy - except ImportError: - pass - else: - if not isinstance(get_event_loop_policy(), WindowsSelectorEventLoopPolicy): - set_event_loop_policy(WindowsSelectorEventLoopPolicy()) - run(main()) diff --git a/get_tankopedia.py b/get_tankopedia.py deleted file mode 100755 index 2065217..0000000 --- a/get_tankopedia.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python3 - -# Get Tankpedia from WoTinspector.com - -import sys -import argparse -import json -import os -import inspect -import aiohttp -import asyncio -import re -import logging -from pathlib import Path -import blitzutils as bu -from blitzutils import WG -from blitzutils import WoTinspector - -logging.getLogger("asyncio").setLevel(logging.DEBUG) - -VERBOSE = False -DEBUG = False - -tank_type = ['lightTank', 'mediumTank', 'heavyTank', 'AT-SPG'] - -# @asyncio.coroutine -async def get_tankopedia(filename: str): - url = "https://wotinspector.com/static/armorinspector/tank_db_blitz.js" - async with aiohttp.ClientSession() as client: - async with client.get(url) as r: - if r.status == 200: - WI_tank_db = await r.text() - WI_tank_db = WI_tank_db.split("\n") - else: - print('Error: Could not get valid HTTPS response. HTTP: ' + str(r.status) ) - sys.exit(1) - tanks = {} - n = 0 - p = re.compile('\\s*(\\d+):\\s{"en":"([^"]+)",.*?"tier":(\\d+), "type":(\\d), "premium":(\\d).*') - for line in WI_tank_db[1:-1]: - m = p.match(line) - tank = {} - tank['tank_id'] = int(m.group(1)) - tank['name'] = m.group(2) - tank['tier'] = int(m.group(3)) - tank['type'] = tank_type[int(m.group(4))] - tank['is_premium'] = (int(m.group(5)) == 1) - tanks[str(m.group(1))] = tank - n += 1 - - tankopedia = {} - tankopedia['status'] = "ok" - tankopedia['meta'] = {"count" : n} - tankopedia['data'] = tanks - print("Tankopedia has " + str(n) + " tanks in: " + filename) - with open(filename,'w') as outfile: - outfile.write(json.dumps(tankopedia, ensure_ascii=False, indent=4, sort_keys=True)) - return None - - -async def main(argv): - # set the directory for the script - os.chdir(os.path.dirname(sys.argv[0])) - - parser = argparse.ArgumentParser(description='Retrieve Tankopedia from WoTinspector.com') - parser.add_argument('--file', dest="outfile", help='Write Tankopedia to file') - parser.add_argument('-d', '--debug', action='store_true', default=False, help='Debug mode') - parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Verbose mode') - parser.add_argument('-s', '--silent', action='store_true', default=False, help='Silent mode') - - args = parser.parse_args() - bu.set_log_level(args.silent, args.verbose, args.debug) - - wi = WoTinspector() - await wi.get_tankopedia(args.outfile) - - await wi.close() - - -# main() -if __name__ == "__main__": - # To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp - if sys.platform.startswith("win") and sys.version_info >= (3, 8): - try: - from asyncio import WindowsSelectorEventLoopPolicy - except ImportError: - pass - else: - if not isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy): - asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) - asyncio.run(main(sys.argv[1:]), debug=False)