mirror of
https://github.com/Jylpah/blitz-tools.git
synced 2026-10-24 11:39:52 +02:00
Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
858a38092d | ||
|
|
7a1272c4eb | ||
|
|
9601177ff6 | ||
|
|
ff4396a3b1 | ||
|
|
1aaa197fac | ||
|
|
abf15c2b35 | ||
|
|
c8e219ab45 | ||
|
|
c640747a0a | ||
|
|
daf0db7382 | ||
|
|
7d46ce46a0 | ||
|
|
44d1d6deb9 | ||
|
|
64a520a845 | ||
|
|
f4f0afec08 | ||
|
|
9e05683f98 | ||
|
|
4ea29439b0 | ||
|
|
40c313d408 | ||
|
|
3364834f3e | ||
|
|
175150b901 | ||
|
|
c18a23258c | ||
|
|
257ba190c2 | ||
|
|
970c24072d | ||
|
|
111c1801e6 | ||
|
|
9f91ef85b6 | ||
|
|
d5cd5f5b12 | ||
|
|
2e1aeea9b8 | ||
|
|
be0e43ad1c | ||
|
|
19bc93405b | ||
|
|
e567be2b93 | ||
|
|
ef657ca6cb | ||
|
|
1f293872e2 | ||
|
|
3e27bd995e | ||
|
|
c9745fcf5c | ||
|
|
18609cd361 | ||
|
|
4196e0f395 | ||
|
|
f1799ff346 | ||
|
|
383e4c1dac | ||
|
|
058d3ad88a | ||
|
|
5c19f4f5a6 | ||
|
|
31d9a32de2 | ||
|
|
8adc0aa561 | ||
|
|
90f2844749 | ||
|
|
fda015abdb |
No files matched your search
+431
-96
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python3.8
|
||||
#!/usr/bin/env python3
|
||||
## PYTHON VERSION MUST BE 3.7 OR HIGHER
|
||||
|
||||
# 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
|
||||
import motor.motor_asyncio, configparser, ssl, re, logging, time, xmltodict, collections, csv
|
||||
from datetime import datetime, timedelta, date
|
||||
import blitzutils as bu
|
||||
from blitzutils import WG
|
||||
@@ -29,14 +30,65 @@ BATTLE_TIME_BUCKET = 3600*24*14
|
||||
|
||||
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' ],
|
||||
'tank': [ 'get_stat_id_tank', 'get_db_tank_stats', 'get_wg_tank_stats' ],
|
||||
'player': [ 'get_stat_id_player', 'get_db_player_stats', 'get_wg_player_stats' ]
|
||||
'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'
|
||||
|
||||
_stat_func = _DEFAULT
|
||||
|
||||
@classmethod
|
||||
def get_default(cls):
|
||||
return cls._DEFAULT
|
||||
|
||||
|
||||
@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 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_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]
|
||||
|
||||
|
||||
replay_summary_flds = [
|
||||
'battle_result',
|
||||
'battle_type',
|
||||
@@ -79,7 +131,7 @@ replay_details_flds = [
|
||||
## 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], 100, '{:2.0f}%' ],
|
||||
'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
|
||||
}
|
||||
@@ -110,7 +162,7 @@ class BattleCategorizationList():
|
||||
|
||||
_battle_modes = {
|
||||
'Any' : 0,
|
||||
'Random' : 1,
|
||||
'Regular' : 1,
|
||||
'Training Room' : 2,
|
||||
'Tournament' : 4,
|
||||
'Rating' : 7,
|
||||
@@ -144,10 +196,17 @@ class BattleCategorizationList():
|
||||
'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 ], '%' ],
|
||||
'player_name' : [ 'Player', 'string' ],
|
||||
'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], '%' ],
|
||||
'enemies_wins' : [ 'Enemies WR', 'bucket', [ 0, .35, .45, .50, .55, .65], '%' ],
|
||||
'player_name' : [ 'Player', 'string', 25 ],
|
||||
'protagonist' : [ 'account_id', 'number' ],
|
||||
'tank_name' : [ 'Tank', 'string' ],
|
||||
'map_name' : [ 'Map', 'string' ],
|
||||
'tank_name' : [ 'Tank', 'string', 25],
|
||||
'map_name' : [ 'Map', 'string', 20],
|
||||
'battle' : [ 'Battle', 'string', 40],
|
||||
'battle_i' : [ 'Battle #', 'number' ]
|
||||
}
|
||||
|
||||
@@ -229,7 +288,7 @@ class BattleCategorizationList():
|
||||
if ctype == 'number':
|
||||
cat_tmp = BattleNumberCategorization(cat, ctitle)
|
||||
elif ctype == 'string':
|
||||
cat_tmp = BattleStringCategorization(cat, ctitle)
|
||||
cat_tmp = BattleStringCategorization(cat, ctitle, cparams)
|
||||
elif ctype == 'category':
|
||||
cat_tmp = BattleClassCategorization(cat, ctitle, cparams[0])
|
||||
elif ctype == 'bucket':
|
||||
@@ -281,7 +340,7 @@ class BattleCategorizationList():
|
||||
elif ctype == 'number':
|
||||
self.categorizations[cat] = BattleNumberCategorization(cat, ctitle)
|
||||
elif ctype == 'string':
|
||||
self.categorizations[cat] = BattleStringCategorization(cat, ctitle)
|
||||
self.categorizations[cat] = BattleStringCategorization(cat, ctitle, cparams)
|
||||
elif ctype == 'category':
|
||||
self.categorizations[cat] = BattleClassCategorization(cat, ctitle, cparams[0])
|
||||
elif ctype == 'bucket':
|
||||
@@ -379,9 +438,28 @@ class BattleCategorizationList():
|
||||
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 BattleCategorization():
|
||||
|
||||
RESULT_CAT_FRMT = '{:>20s}'
|
||||
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
|
||||
@@ -390,6 +468,15 @@ class BattleCategorization():
|
||||
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)
|
||||
@@ -413,7 +500,7 @@ class BattleCategorization():
|
||||
print(FORMAT.format(self.category_key, self.title), end=' ')
|
||||
|
||||
|
||||
def record_result(self, result: dict):
|
||||
def record_result(self, result: dict) -> bool:
|
||||
try:
|
||||
cat = self.get_category(result)
|
||||
self.categories[cat].record_result(result)
|
||||
@@ -434,12 +521,38 @@ class BattleCategorization():
|
||||
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()
|
||||
BattleCategory.print_headers(self.title)
|
||||
self.print_headers()
|
||||
for cat in self.get_categories():
|
||||
print(self.RESULT_CAT_FRMT.format(cat), end=' ')
|
||||
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)
|
||||
@@ -448,7 +561,7 @@ class BattleCategorization():
|
||||
return None
|
||||
|
||||
|
||||
def get_results_json(self):
|
||||
def get_results_json(self) -> dict:
|
||||
"""Get results in JSON format"""
|
||||
try:
|
||||
results = dict()
|
||||
@@ -463,6 +576,19 @@ class BattleCategorization():
|
||||
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'
|
||||
|
||||
@@ -546,6 +672,17 @@ class BattleClassCategorization(BattleCategorization):
|
||||
class BattleStringCategorization(BattleCategorization):
|
||||
"""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 get_category(self, result: dict) -> str:
|
||||
"""Get category"""
|
||||
try:
|
||||
@@ -557,6 +694,21 @@ class BattleStringCategorization(BattleCategorization):
|
||||
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)
|
||||
|
||||
except Exception as err:
|
||||
bu.error('Category: ' + self.category_key, exception=err)
|
||||
return None
|
||||
|
||||
def get_filter_categories(self, filters: list) -> list:
|
||||
try:
|
||||
return filters
|
||||
@@ -747,7 +899,10 @@ class BattleCategory():
|
||||
'hit_rate' : [ 'Hit rate', 'Shots hit / all shots made', 8, '{:8.1%}' ],
|
||||
'pen_rate' : [ 'Pen rate', 'Shots pen / shots hit', 8, '{:8.1%}' ],
|
||||
'survived' : [ 'Surv%', 'Survival rate', 6, '{:6.1%}' ],
|
||||
'time_alive%' : [ 'T alive%', 'Percentage of time being alive in a battle', 8, '{:8.0%}' ],
|
||||
'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%}' ],
|
||||
@@ -764,10 +919,16 @@ class BattleCategory():
|
||||
|
||||
# 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'],
|
||||
'default' : [ 'battles', 'win','damage_made', 'enemies_destroyed', 'enemies_spotted',
|
||||
'top_tier', 'DR', 'survived', 'allies_wins', 'enemies_wins'],
|
||||
'team' : [ 'battles', 'win','player_wins','allies_wins','enemies_wins'],
|
||||
'team_extended' : [ 'battles', 'battles%', 'win','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','damage_made', 'enemies_destroyed', 'enemies_spotted', 'top_tier', 'DR', 'KDR', 'hit_rate', 'pen_rate', 'survived', 'time_alive%', 'player_wins', 'allies_wins', 'enemies_wins', MISSING_STATS ]
|
||||
'team_extended' : [ 'battles', 'battles%', 'win','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','damage_made', 'enemies_destroyed', 'enemies_spotted',
|
||||
'top_tier', 'DR', 'KDR', 'hit_rate', 'pen_rate', 'survived', 'alive',
|
||||
'player_wins', 'allies_wins', 'enemies_wins', MISSING_STATS ],
|
||||
'all' : [ cat for cat in _result_fields.keys() ]
|
||||
}
|
||||
|
||||
_team_fields = [ 'wins', 'battles', 'damage_dealt' ]
|
||||
@@ -792,7 +953,6 @@ class BattleCategory():
|
||||
avg_fields = set()
|
||||
ratio_fields = set()
|
||||
|
||||
RESULT_CAT_HEADER_FRMT = '{:_<20s}'
|
||||
|
||||
@classmethod
|
||||
def get_modes(cls) -> list:
|
||||
@@ -831,6 +991,10 @@ class BattleCategory():
|
||||
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:
|
||||
@@ -889,9 +1053,9 @@ class BattleCategory():
|
||||
|
||||
|
||||
@classmethod
|
||||
def print_headers(cls, cat_name: str):
|
||||
def print_headers(cls, header: str):
|
||||
try:
|
||||
headers = [ cls.RESULT_CAT_HEADER_FRMT.format(cat_name) ]
|
||||
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)))
|
||||
@@ -903,6 +1067,20 @@ class BattleCategory():
|
||||
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
|
||||
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
|
||||
@@ -986,6 +1164,22 @@ class BattleCategory():
|
||||
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()')
|
||||
@@ -1003,16 +1197,20 @@ class BattleCategory():
|
||||
|
||||
|
||||
class PlayerHistogram():
|
||||
def __init__(self, field: str, name: str, fields : list, factor: float, format: str ):
|
||||
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 = format
|
||||
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):
|
||||
@@ -1042,7 +1240,7 @@ class PlayerHistogram():
|
||||
return self.cat_format.format(float(val * self.cat_factor))
|
||||
|
||||
|
||||
def print(self, ret_json: bool = False):
|
||||
def calc_results(self):
|
||||
N_enemies = 0
|
||||
N_allies = 0
|
||||
res = dict()
|
||||
@@ -1052,10 +1250,7 @@ class PlayerHistogram():
|
||||
N_allies += self.allies[cat]
|
||||
N_total = N_allies + N_enemies
|
||||
|
||||
print("\n{:12s} | {:13s} | {:13s} | {:13s}".format(self.name, "Allies", "Enemies", "TOTAL"))
|
||||
for cat in range(0, self.ncat):
|
||||
print("{:12s} | {:5d} ({:4.1f}%) | {:5d} ({:4.1f}%) | {:5d} ({:4.1f}%)".format(self.get_category_name(cat), self.allies[cat], self.allies[cat]/N_allies*100, self.enemies[cat], self.enemies[cat]/N_enemies*100, self.allies[cat] + self.enemies[cat], (self.allies[cat] + self.enemies[cat])/N_total*100 ))
|
||||
if ret_json:
|
||||
stat = dict()
|
||||
stat['allies'] = self.allies[cat]
|
||||
stat['allies%'] = stat['allies'] / N_allies
|
||||
@@ -1063,11 +1258,51 @@ class PlayerHistogram():
|
||||
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 ErrorCatchingArgumentParser(argparse.ArgumentParser):
|
||||
def exit(self, status=0, message=None):
|
||||
if status:
|
||||
@@ -1079,10 +1314,6 @@ class ErrorCatchingArgumentParser(argparse.ArgumentParser):
|
||||
|
||||
## main() -------------------------------------------------------------
|
||||
|
||||
OPT_MODE_DEFAULT = 'default'
|
||||
OPT_MODE_HELP = 'help'
|
||||
OPT_MODES = BattleCategory.get_modes() + [OPT_MODE_HELP]
|
||||
|
||||
async def main(argv):
|
||||
global wg, wi, WG_APP_ID, OPT_MODE_DEFAULT
|
||||
# set the directory for the script
|
||||
@@ -1090,11 +1321,15 @@ async def main(argv):
|
||||
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 = 'player'
|
||||
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
|
||||
@@ -1128,6 +1363,8 @@ async def main(argv):
|
||||
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(' ','')
|
||||
@@ -1170,21 +1407,22 @@ async def main(argv):
|
||||
|
||||
parser = ErrorCatchingArgumentParser(description='Analyze Blitz replay JSON files from WoTinspector.com. Use \'upload_wotb_replays.py\' to upload the replay files first.')
|
||||
|
||||
parser.add_argument('--mode', default=OPT_MODE_DEFAULT, choices=OPT_MODES, help='Select stats mode. Options: ' + ', '.join(OPT_MODES))
|
||||
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('--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 nameto analyze. Format: ACCOUNT_NAME@SERVER')
|
||||
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('--stat_func', default=OPT_STAT_FUNC, choices=STAT_FUNC.keys(), help='Select how to calculate for ally/enemy performance: tank-tier stats, global player stats')
|
||||
# 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('-j', '--json', action='store_true', default=False, help='Export data in 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('-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: : [ {"tier" : [8,9,20] }, { "player_wins" : 5 }]')
|
||||
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: : [ {"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')
|
||||
@@ -1197,6 +1435,7 @@ async def main(argv):
|
||||
try:
|
||||
args = parser.parse_args()
|
||||
except Exception as err:
|
||||
bu.error('Invalid arguments', exception=err)
|
||||
sys.exit(0)
|
||||
|
||||
# res_categories = BattleCategorization.get_categorizations(OPT_CATEGORIZATIONS, args)
|
||||
@@ -1233,6 +1472,8 @@ async def main(argv):
|
||||
|
||||
if args.log:
|
||||
await bu.set_file_logging('analyze_wotb_replays', add_timestamp=True)
|
||||
|
||||
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)
|
||||
|
||||
@@ -1283,23 +1524,52 @@ async def main(argv):
|
||||
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()
|
||||
hist = None
|
||||
|
||||
histograms = None
|
||||
if args.hist:
|
||||
hist = process_player_dist(results, player_stats, stat_id_map, args.json)
|
||||
histograms = process_player_dist(results, player_stats, stat_id_map)
|
||||
print_player_dist(histograms)
|
||||
|
||||
if args.json:
|
||||
res = blt_cat_list.get_results_json()
|
||||
res['histograms'] = hist
|
||||
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 == '-':
|
||||
args.outfile = 'export.json'
|
||||
await bu.save_JSON(args.outfile, res, pretty=False) ## Excel does not understand pretty JSON
|
||||
bu.verbose_std('Data exported to ' + 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)
|
||||
|
||||
if args.csv:
|
||||
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)
|
||||
|
||||
bu.debug('Finished. Cleaning up..................')
|
||||
except Exception as err:
|
||||
bu.error(exception=err)
|
||||
except UserWarning as err:
|
||||
bu.warning(str(err))
|
||||
except asyncio.exceptions.CancelledError:
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as err:
|
||||
bu.error(exception=err)
|
||||
@@ -1350,6 +1620,14 @@ async def help_extended(db : motor.motor_asyncio.AsyncIOMotorDatabase = None, pa
|
||||
print("\tExample: --filter '{ \"room_type\" :1 }'")
|
||||
print('')
|
||||
BattleCategorizationList.help()
|
||||
print('\n-------------------------------------------------------------------')
|
||||
print('| Rounding errors |')
|
||||
print('-------------------------------------------------------------------')
|
||||
print('')
|
||||
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.')
|
||||
|
||||
|
||||
## move the class PlayerHistogram?
|
||||
@@ -1399,7 +1677,7 @@ def filter_min_replays_by_player(results: list, min_replays: int) -> list:
|
||||
return None
|
||||
|
||||
|
||||
def process_player_dist(results: list, player_stats: dict, stat_id_map: dict, res_json: bool = False) -> dict:
|
||||
def process_player_dist(results: list, player_stats: dict, stat_id_map: dict) -> dict:
|
||||
"""Process player distribution"""
|
||||
hist_stats = dict()
|
||||
try:
|
||||
@@ -1407,7 +1685,7 @@ def process_player_dist(results: list, player_stats: dict, stat_id_map: dict, re
|
||||
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']: # uniton of Sets
|
||||
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']:
|
||||
@@ -1417,19 +1695,23 @@ def process_player_dist(results: list, player_stats: dict, stat_id_map: dict, re
|
||||
for stat_field in hist_stats:
|
||||
hist_stats[stat_field].record_enemy(player_stats[player_remapped][stat_field])
|
||||
|
||||
print('\nPlayer Histograms______', end='', flush=True)
|
||||
|
||||
res = dict()
|
||||
for stat_field in hist_stats:
|
||||
res[stat_field] = hist_stats[stat_field].print(res_json)
|
||||
hist_stats[stat_field].calc_results()
|
||||
|
||||
return res
|
||||
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()
|
||||
|
||||
def process_battle_results(results: dict, args : argparse.Namespace):
|
||||
"""Process replay battle results"""
|
||||
try:
|
||||
@@ -1505,10 +1787,10 @@ async def process_player_stats(players, N_workers: int, args : argparse.Namespac
|
||||
try:
|
||||
statsQ = asyncio.Queue()
|
||||
bu.debug('Create player stats queue: ' + str(len(players)) + ' players')
|
||||
stat_id_map = {}
|
||||
stat_id_map = dict()
|
||||
stat_ids = set()
|
||||
|
||||
stat_id_map_func = globals()[STAT_FUNC[args.stat_func][0]]
|
||||
stat_id_map_func = globals()[StatFunc.get_stat_id_func()]
|
||||
|
||||
for player in players:
|
||||
stat_id_map[player] = stat_id_map_func(player)
|
||||
@@ -1531,8 +1813,8 @@ async def process_player_stats(players, N_workers: int, args : argparse.Namespac
|
||||
bu.debug('Cancelling stats workers')
|
||||
for task in stats_tasks:
|
||||
task.cancel()
|
||||
player_stats = {}
|
||||
stat_id_remap = {}
|
||||
player_stats = dict()
|
||||
stat_id_remap = dict()
|
||||
|
||||
bu.debug('Gathering stats worker outputs')
|
||||
for (stats, id_remap) in await asyncio.gather(*stats_tasks):
|
||||
@@ -1639,11 +1921,11 @@ def calc_team_stats(results: list, player_stats : dict, stat_id_map : dict, arg
|
||||
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 = {}
|
||||
stat_id_remap = {}
|
||||
stats = dict()
|
||||
stat_id_remap = dict()
|
||||
|
||||
stat_db_func = globals()[STAT_FUNC[args.stat_func][1]]
|
||||
stat_wg_func = globals()[STAT_FUNC[args.stat_func][2]]
|
||||
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:
|
||||
@@ -1689,7 +1971,7 @@ async def stat_worker(queue : asyncio.Queue, workerID: int, args : argparse.Name
|
||||
bu.error('Unexpected error', err, id=workerID)
|
||||
queue.task_done()
|
||||
|
||||
except (asyncio.CancelledError, concurrent.futures._base.CancelledError):
|
||||
except (asyncio.CancelledError, concurrent.futures.CancelledError):
|
||||
bu.debug('Stats queue is empty', id=workerID)
|
||||
except Exception as err:
|
||||
bu.error(exception=err, id=workerID)
|
||||
@@ -1789,6 +2071,29 @@ async def get_wg_tank_tier_stats(stat_id_str: str, cache_only = False) -> dict:
|
||||
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)
|
||||
|
||||
# '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))
|
||||
|
||||
return await 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:
|
||||
@@ -1815,6 +2120,32 @@ async def get_db_tank_tier_stats(db : motor.motor_asyncio.AsyncIOMotorDatabase,
|
||||
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:
|
||||
dbc = db[DB_C_TANK_STATS]
|
||||
( account_id, battletime ) = str2ints(stat_id_str)
|
||||
tier_tanks = wg.get_tanks_by_tier(10)
|
||||
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)
|
||||
|
||||
return await tank_stats_helper(await cursor.to_list(1000))
|
||||
|
||||
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)"""
|
||||
@@ -1867,7 +2198,7 @@ async def tank_stats_helper(stat_list: list):
|
||||
try:
|
||||
if stat_list == None:
|
||||
return None
|
||||
stats = {}
|
||||
stats = dict()
|
||||
hist_fields = histogram_fields.keys()
|
||||
if 'battles' not in hist_fields:
|
||||
bu.error('\'battles\' must be defined in \'histogram_fields\'')
|
||||
@@ -1899,16 +2230,12 @@ async def player_stats_helper(player_stats: dict):
|
||||
try:
|
||||
if player_stats == None:
|
||||
return None
|
||||
stats = {}
|
||||
stats = dict()
|
||||
hist_fields = histogram_fields.keys()
|
||||
if 'battles' not in hist_fields:
|
||||
bu.error('\'battles\' must be defined in \'histogram_fields\'')
|
||||
return None
|
||||
|
||||
# NEEDED 2020-10-25?
|
||||
#for field in hist_fields:
|
||||
# stats[field] = 0
|
||||
|
||||
for field in hist_fields:
|
||||
stats[field] = max(0, player_stats['statistics']['all'][field])
|
||||
|
||||
@@ -1944,7 +2271,7 @@ async def mk_replayQ(queue : asyncio.Queue, args : argparse.Namespace, db : moto
|
||||
cursor = dbc.find(filters)
|
||||
else:
|
||||
# select all
|
||||
cursor = dbc.find({})
|
||||
cursor = dbc.find(dict())
|
||||
bu.debug('Reading replays...')
|
||||
async for replay_json in cursor:
|
||||
_id = replay_json['_id']
|
||||
@@ -1999,9 +2326,12 @@ async def mk_readerQ_item(replay_json, filename : str = None) -> list:
|
||||
global REPLAY_N
|
||||
REPLAY_N +=1
|
||||
try:
|
||||
if wi.chk_JSON_replay(replay_json):
|
||||
if not '_id' in replay_json:
|
||||
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:
|
||||
@@ -2025,9 +2355,9 @@ async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Nam
|
||||
replay_file = item[2]
|
||||
|
||||
try:
|
||||
msg_str = 'Replay[' + str(replayID) + ']: '
|
||||
# msg_str = 'Replay[' + str(replayID) + ']: '
|
||||
if replay_json == None:
|
||||
bu.warning(msg_str + 'Invalid replay. Skipping: ' + (replay_file if replay_file != None else '') )
|
||||
bu.warning('Invalid replay. Skipping: ' + (replay_file if replay_file != None else '') )
|
||||
queue.task_done()
|
||||
continue
|
||||
|
||||
@@ -2036,7 +2366,7 @@ async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Nam
|
||||
result = await read_replay_JSON(replay_json, args)
|
||||
bu.print_progress()
|
||||
if result == None:
|
||||
bu.warning(msg_str + 'Invalid replay ' + (replay_file if replay_file != None else '') )
|
||||
bu.warning('Invalid replay ' + (replay_file if replay_file != None else '') )
|
||||
queue.task_done()
|
||||
continue
|
||||
|
||||
@@ -2045,7 +2375,7 @@ async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Nam
|
||||
playerstanks.update(set([result['player']]))
|
||||
|
||||
results.append(result)
|
||||
bu.debug('Marking task ' + str(replayID) + ' done')
|
||||
bu.debug('Marking replay [' + str(replayID) + '] done', id=readerID)
|
||||
|
||||
except Exception as err:
|
||||
bu.error(exception=err)
|
||||
@@ -2062,7 +2392,7 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
|
||||
account_id = args.account_id
|
||||
url = args.url
|
||||
#db = args.db
|
||||
result = {}
|
||||
result = dict()
|
||||
try:
|
||||
# bu.debug(str(replay_json))
|
||||
if not wi.chk_JSON_replay(replay_json):
|
||||
@@ -2070,9 +2400,10 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
|
||||
return None
|
||||
|
||||
result['_id'] = replay_json['_id']
|
||||
result['battle_start_timestamp'] = int(replay_json['data']['summary']['battle_start_timestamp'])
|
||||
replay_summary = replay_json['data']['summary']
|
||||
result['battle_start_timestamp'] = int(replay_summary['battle_start_timestamp'])
|
||||
# TBD...
|
||||
protagonist = int(replay_json['data']['summary']['protagonist'])
|
||||
protagonist = int(replay_summary['protagonist'])
|
||||
|
||||
# For filtering replays per submitter
|
||||
result['protagonist'] = protagonist
|
||||
@@ -2080,17 +2411,17 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
|
||||
if account_id == None:
|
||||
account_id = protagonist
|
||||
elif protagonist != account_id:
|
||||
if account_id in replay_json['data']['summary']['enemies']:
|
||||
if account_id in replay_summary['enemies']:
|
||||
# switch the teams...
|
||||
if replay_json['data']['summary']['battle_result'] != 2:
|
||||
if replay_json['data']['summary']['battle_result'] == 0:
|
||||
replay_json['data']['summary']['battle_result'] = 1
|
||||
if replay_summary['battle_result'] != 2:
|
||||
if replay_summary['battle_result'] == 0:
|
||||
replay_summary['battle_result'] = 1
|
||||
else:
|
||||
replay_json['data']['summary']['battle_result'] = 0
|
||||
tmp = replay_json['data']['summary']['enemies']
|
||||
replay_json['data']['summary']['enemies'] = replay_json['data']['summary']['allies']
|
||||
replay_json['data']['summary']['allies'] = tmp
|
||||
elif account_id in replay_json['data']['summary']['allies']:
|
||||
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
|
||||
@@ -2099,7 +2430,7 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
|
||||
result['url'] = replay_json['data']['view_url']
|
||||
|
||||
for key in replay_summary_flds:
|
||||
result[key] = replay_json['data']['summary'][key]
|
||||
result[key] = replay_summary[key]
|
||||
|
||||
except Exception as err:
|
||||
bu.error(exception=err)
|
||||
@@ -2112,18 +2443,20 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
|
||||
result['enemies_survived'] = 0 # for steamroller stats
|
||||
btl_duration = 0
|
||||
btl_tier = 0
|
||||
btl_tier_low = 10
|
||||
protagonist_tank = None
|
||||
for player in replay_json['data']['summary']['details']:
|
||||
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)
|
||||
btl_tier_low = min(btl_tier_low, player_tank_tier)
|
||||
|
||||
if (protagonist != None) and (player['dbid'] == protagonist):
|
||||
protagonist_tank = player['vehicle_descr']
|
||||
|
||||
if player['dbid'] == account_id:
|
||||
# player itself is not put in results['allies']
|
||||
tmp = {}
|
||||
tmp = dict()
|
||||
tmp['account_id'] = account_id
|
||||
tmp['tank_id'] = player['vehicle_descr']
|
||||
tmp['tank_tier'] = player_tank_tier
|
||||
@@ -2153,7 +2486,7 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
|
||||
else:
|
||||
survived = False
|
||||
|
||||
if player['dbid'] in replay_json['data']['summary']['allies']:
|
||||
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
|
||||
@@ -2162,14 +2495,15 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
|
||||
if survived:
|
||||
result['enemies_survived'] += 1
|
||||
|
||||
result['battle_duration'] = btl_duration
|
||||
## Rather use 'player' than incomprehensible 'protagonist'...
|
||||
result['player'] = get_stat_id(protagonist, protagonist_tank, 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_json['data']['summary']['details']:
|
||||
for player in replay_summary['details']:
|
||||
bu.debug(str(player))
|
||||
if (player['squad_index'] == result['squad_index']) and (player['dbid'] in replay_json['data']['summary']['allies']) and (player['dbid'] != account_id):
|
||||
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']
|
||||
@@ -2181,12 +2515,13 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
|
||||
for ratio in BattleCategory.get_result_fields_ratio(all=True):
|
||||
result[ratio] = BattleCategory.calc_ratio(ratio, result)
|
||||
|
||||
result['time_alive%'] = result['time_alive'] / btl_duration
|
||||
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:
|
||||
|
||||
+65
-51
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3.8
|
||||
#!/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
|
||||
@@ -21,7 +22,7 @@ _log_level = NORMAL
|
||||
LOG = False
|
||||
LOGGER = None
|
||||
|
||||
## Progress display
|
||||
# Progress display
|
||||
_progress_N = 100
|
||||
_progress_i = 0
|
||||
_progress_id = None
|
||||
@@ -30,9 +31,10 @@ _progress_obj = None
|
||||
UMASK = os.umask(0)
|
||||
os.umask(UMASK)
|
||||
|
||||
## -----------------------------------------------------------
|
||||
#### Class ThrottledClientSession(aiohttp.ClientSession)
|
||||
## -----------------------------------------------------------
|
||||
# -----------------------------------------------------------
|
||||
# Class ThrottledClientSession(aiohttp.ClientSession)
|
||||
# -----------------------------------------------------------
|
||||
|
||||
|
||||
class ThrottledClientSession(aiohttp.ClientSession):
|
||||
"""Rate-throttled client session class inherited from aiohttp.ClientSession)"""
|
||||
@@ -54,31 +56,28 @@ class ThrottledClientSession(aiohttp.ClientSession):
|
||||
self._queue = asyncio.Queue(min(2, int(rate_limit)+1))
|
||||
self._fillerTask = asyncio.create_task(self._filler())
|
||||
|
||||
|
||||
|
||||
def _get_sleep(self) -> list:
|
||||
if self.rate_limit == None:
|
||||
return None
|
||||
else:
|
||||
return 1/self.rate_limit
|
||||
|
||||
|
||||
def get_rate(self) -> float:
|
||||
"""Return rate of requests"""
|
||||
return self._count / (time.time() - self._start_time)
|
||||
|
||||
|
||||
def get_stats(self):
|
||||
"""Get session statistics"""
|
||||
res = {'rate' : self.get_rate(), 'rate_limit': self.rate_limit, 'count' : self._count }
|
||||
res = {'rate': self.get_rate(), 'rate_limit': self.rate_limit,
|
||||
'count': self._count}
|
||||
return res
|
||||
|
||||
|
||||
def get_stats_str(self):
|
||||
"""Print session statistics"""
|
||||
return 'rate limit: ' + str(self.rate_limit if self.rate_limit != None else '-') + \
|
||||
' rate: ' + "{0:.1f}".format(self.get_rate()) + ' requests: ' + str(self._count)
|
||||
|
||||
' rate: ' + \
|
||||
"{0:.1f}".format(self.get_rate()) + \
|
||||
' requests: ' + str(self._count)
|
||||
|
||||
def reset_counters(self):
|
||||
"""Reset rate counters and return current results"""
|
||||
@@ -87,14 +86,12 @@ class ThrottledClientSession(aiohttp.ClientSession):
|
||||
self._count = 0
|
||||
return res
|
||||
|
||||
|
||||
def set_rate_limit(self, rate_limit: float = 1):
|
||||
if rate_limit >= 0:
|
||||
self.rate_limit = rate_limit
|
||||
return self.rate_limit
|
||||
return None
|
||||
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close rate-limiter's "bucket filler" task"""
|
||||
# DEBUG
|
||||
@@ -103,11 +100,10 @@ class ThrottledClientSession(aiohttp.ClientSession):
|
||||
if self._fillerTask != None:
|
||||
self._fillerTask.cancel()
|
||||
await asyncio.wait_for(self._fillerTask, timeout=0.5)
|
||||
except (asyncio.exceptions.CancelledError, asyncio.TimeoutError):
|
||||
except (asyncio.CancelledError, asyncio.TimeoutError):
|
||||
pass
|
||||
await super().close()
|
||||
|
||||
|
||||
async def _filler_simple(self):
|
||||
"""Filler task to fill the leaky bucket algo"""
|
||||
try:
|
||||
@@ -122,7 +118,6 @@ class ThrottledClientSession(aiohttp.ClientSession):
|
||||
except Exception as err:
|
||||
error(exception=err)
|
||||
|
||||
|
||||
async def _filler(self):
|
||||
"""Filler task to fill the leaky bucket algo"""
|
||||
try:
|
||||
@@ -141,18 +136,18 @@ class ThrottledClientSession(aiohttp.ClientSession):
|
||||
increment = self.rate_limit * (now - updated_at)
|
||||
fraction += increment % 1
|
||||
extra_increment = fraction // 1
|
||||
items_2_add = int(min(self._queue.maxsize - self._queue.qsize(), int(increment) + extra_increment))
|
||||
items_2_add = int(
|
||||
min(self._queue.maxsize - self._queue.qsize(), int(increment) + extra_increment))
|
||||
fraction = fraction % 1
|
||||
for i in range(0, items_2_add):
|
||||
self._queue.put_nowait(i)
|
||||
updated_at = now
|
||||
await asyncio.sleep(sleep)
|
||||
except (asyncio.exceptions.CancelledError, asyncio.CancelledError):
|
||||
except asyncio.CancelledError:
|
||||
debug('Cancelled')
|
||||
except Exception as err:
|
||||
error(exception=err)
|
||||
|
||||
|
||||
async def _request(self, *args, **kwargs):
|
||||
"""Throttled _request()"""
|
||||
if self._queue != None:
|
||||
@@ -164,12 +159,12 @@ class ThrottledClientSession(aiohttp.ClientSession):
|
||||
|
||||
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 == None:
|
||||
@@ -183,7 +178,6 @@ class AsyncLogger():
|
||||
self._file = None
|
||||
return False
|
||||
|
||||
|
||||
async def logger(self):
|
||||
"""Async file logger"""
|
||||
if self._file == None:
|
||||
@@ -199,14 +193,12 @@ class AsyncLogger():
|
||||
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
|
||||
# empty queue & close
|
||||
await self._queue.join()
|
||||
self._task.cancel()
|
||||
self._file.close()
|
||||
@@ -214,9 +206,10 @@ class AsyncLogger():
|
||||
error('Error closing log file', err)
|
||||
return None
|
||||
|
||||
## -----------------------------------------------------------
|
||||
#### Utils
|
||||
## -----------------------------------------------------------
|
||||
# -----------------------------------------------------------
|
||||
# Utils
|
||||
# -----------------------------------------------------------
|
||||
|
||||
|
||||
def set_debug(debug: bool):
|
||||
global _log_level
|
||||
@@ -300,6 +293,7 @@ async def close_file_logging():
|
||||
await LOGGER.close()
|
||||
LOGGER = None
|
||||
|
||||
|
||||
def _randomword(length):
|
||||
letters = string.ascii_lowercase
|
||||
return ''.join(random.choice(letters) for i in range(length))
|
||||
@@ -317,7 +311,7 @@ def verbose_std(msg = "", id = None) -> bool:
|
||||
|
||||
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)) )
|
||||
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:
|
||||
@@ -337,6 +331,21 @@ def log(msg = "", id = None, exception = None) -> bool:
|
||||
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 = 'LOG', msg = '', exception = None, id = None, print_msg : bool = True):
|
||||
# Use empty prefix to determine standard verbose messages
|
||||
if not (print_msg or LOG):
|
||||
@@ -345,8 +354,13 @@ def _print_log_msg(prefix = 'LOG', msg = '', exception = None, id = None, print_
|
||||
if prefix != '':
|
||||
curframe = inspect.currentframe()
|
||||
calframe = inspect.getouterframes(curframe)
|
||||
class_name = get_class_from_frame(curframe)
|
||||
caller = calframe[2].function
|
||||
prefix = prefix + ': ' + caller + '(): '
|
||||
if class_name != None:
|
||||
prefix = prefix + ': ' + class_name + '.' + caller
|
||||
else:
|
||||
prefix = prefix + ': ' + caller
|
||||
prefix = prefix + '(): '
|
||||
|
||||
if id != None:
|
||||
prefix = prefix + '[' + str(id) + ']: '
|
||||
@@ -534,7 +548,7 @@ async def get_url_JSON(session: aiohttp.ClientSession, url: str, chk_JSON_func =
|
||||
if url == None:
|
||||
return None
|
||||
|
||||
## To avoid excessive use of servers
|
||||
# To avoid excessive use of servers
|
||||
for retry in range(1,max_tries+1):
|
||||
try:
|
||||
async with session.get(url) as resp:
|
||||
@@ -612,9 +626,9 @@ def get_JSON_value(json, key : str = None, keys : list = None, keypath = None):
|
||||
raise KeyError('Key not found: ' + get_JSON_keypath(keypath, keys[0]))
|
||||
|
||||
|
||||
## -----------------------------------------------------------
|
||||
#### Class SlowBar
|
||||
## -----------------------------------------------------------
|
||||
# -----------------------------------------------------------
|
||||
# Class SlowBar
|
||||
# -----------------------------------------------------------
|
||||
|
||||
class SlowBar(IncrementalBar):
|
||||
suffix = '%(index)d/%(max)d %(percent)d%% ETA %(remaining_hours).0f h %(remaining_mins).0f mins'
|
||||
@@ -627,9 +641,9 @@ class SlowBar(IncrementalBar):
|
||||
return (self.eta - (self.eta // 3600)*3600) // 60
|
||||
|
||||
|
||||
## -----------------------------------------------------------
|
||||
#### Class StatsNotFound
|
||||
## -----------------------------------------------------------
|
||||
# -----------------------------------------------------------
|
||||
# Class StatsNotFound
|
||||
# -----------------------------------------------------------
|
||||
|
||||
class StatsNotFound(Exception):
|
||||
pass
|
||||
@@ -637,9 +651,9 @@ class StatsNotFound(Exception):
|
||||
|
||||
|
||||
|
||||
## -----------------------------------------------------------
|
||||
#### Class WG
|
||||
## -----------------------------------------------------------
|
||||
# -----------------------------------------------------------
|
||||
# Class WG
|
||||
# -----------------------------------------------------------
|
||||
|
||||
class WG:
|
||||
|
||||
@@ -718,7 +732,7 @@ class WG:
|
||||
|
||||
SQL_PRUNE_CACHE = """DELETE from {} WHERE update_time < {}"""
|
||||
|
||||
## Default data. Please use the latest maps.json
|
||||
# Default data. Please use the latest maps.json
|
||||
|
||||
maps = {
|
||||
"Random": "Random map",
|
||||
@@ -867,7 +881,7 @@ class WG:
|
||||
error(exception=err)
|
||||
|
||||
|
||||
## Class methods ----------------------------------------------------------
|
||||
# Class methods ----------------------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def get_server(cls, account_id: int) -> str:
|
||||
@@ -1052,7 +1066,7 @@ class WG:
|
||||
return False
|
||||
|
||||
|
||||
## Methods --------------------------------------------------
|
||||
# Methods --------------------------------------------------
|
||||
def load_tanks(self, tankopedia_fn: str):
|
||||
"""Load tanks from tankopedia JSON"""
|
||||
if tankopedia_fn == None:
|
||||
@@ -1443,7 +1457,7 @@ class WG:
|
||||
return None
|
||||
try:
|
||||
self.cache = await aiosqlite.connect(WG.CACHE_DB_FILE)
|
||||
## Create cache tables table
|
||||
# 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)
|
||||
@@ -1474,7 +1488,7 @@ class WG:
|
||||
else:
|
||||
error('Function to saves stats type \'' + stats_type + '\' is not implemented yet')
|
||||
|
||||
except (asyncio.CancelledError):
|
||||
except asyncio.CancelledError:
|
||||
# this is an eternal loop that will wait until cancelled
|
||||
return None
|
||||
|
||||
@@ -1654,9 +1668,9 @@ class WG:
|
||||
return None
|
||||
|
||||
|
||||
## -----------------------------------------------------------
|
||||
#### Class WoTinspector
|
||||
## -----------------------------------------------------------
|
||||
# -----------------------------------------------------------
|
||||
# Class WoTinspector
|
||||
# -----------------------------------------------------------
|
||||
|
||||
class WoTinspector:
|
||||
URL_WI = 'https://replays.wotinspector.com'
|
||||
@@ -1739,7 +1753,7 @@ class WoTinspector:
|
||||
hash.update(data)
|
||||
replay_id = hash.hexdigest()
|
||||
|
||||
## Testing if the replay has already been posted
|
||||
# Testing if the replay has already been posted
|
||||
json_resp = await self.get_replay_JSON(replay_id)
|
||||
if json_resp != None:
|
||||
debug('Already uploaded: ' + title, id=N)
|
||||
@@ -1838,7 +1852,7 @@ class WoTinspector:
|
||||
# FIX The replay can have allies/enemies i.e. be useful for
|
||||
# team composition analysis, but does not have player contribution
|
||||
if ('status' in json_resp) and json_resp['status'] == 'ok' and \
|
||||
(get_JSON_value(json_resp, key='data.summary.allies') != None) :
|
||||
(get_JSON_value(json_resp, key='data.summary.winner_team') != None) :
|
||||
debug("JSON check OK")
|
||||
return True
|
||||
except KeyError as err:
|
||||
|
||||
+22
-3
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3.8
|
||||
#!/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)
|
||||
|
||||
@@ -81,6 +82,7 @@ async def main(argv):
|
||||
tankopedia['data'] = tanks
|
||||
tankopedia['userStr'] = 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 != None:
|
||||
@@ -112,7 +114,7 @@ async def extract_tanks(blitz_app_base : str, nation: str):
|
||||
tankList = xmltodict.parse(await f.read())
|
||||
for data in tankList['root'].keys():
|
||||
tank_xml = tankList['root'][data]
|
||||
tank = {}
|
||||
tank = dict()
|
||||
tank['tank_id'] = await get_tank_id(nation, int(tank_xml['id']))
|
||||
tank['userStr'] = tank_xml['userString']
|
||||
tank['nation'] = nation
|
||||
@@ -120,6 +122,7 @@ async def extract_tanks(blitz_app_base : str, nation: str):
|
||||
#debug(tank_xml['price'])
|
||||
tank['is_premium'] = issubclass(type(tank_xml['price']), dict)
|
||||
tank['type'] = await get_tank_type(tank_xml['tags'])
|
||||
#bu.debug('Reading tank string: ' + tank['userStr'], force=True)
|
||||
tanks.append(tank)
|
||||
except Exception as err:
|
||||
bu.error(err)
|
||||
@@ -160,10 +163,25 @@ async def convert_tank_names(tanklist : list, tank_strs: dict) -> dict:
|
||||
try:
|
||||
for tank in tanklist:
|
||||
tank['name'] = tank_strs[tank['userStr']]
|
||||
userStrs[tank['userStr'].split(':')[1]] = tank['name']
|
||||
#userStrs[tank['userStr'].split(':')[1]] = tank['name']
|
||||
tank.pop('userStr', None)
|
||||
tankopedia[str(tank['tank_id'])] = 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 = collections.OrderedDict()
|
||||
for tank_id in sorted(tankopedia.keys(), key=int):
|
||||
@@ -172,6 +190,7 @@ async def convert_tank_names(tanklist : list, tank_strs: dict) -> dict:
|
||||
userStrs_sorted = collections.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)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"grossberg": "Dynasty's Pearl",
|
||||
"himmelsdorf": "Himmelsdorf",
|
||||
"holmeisk": "Wasteland",
|
||||
"iceworld": "Everfrost",
|
||||
"idle": "Yukon",
|
||||
"italy": "Vineyards",
|
||||
"karelia": "Rockfield",
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
| Release | Date | Notes | Inaccurate? |
|
||||
|:-------:|:--------:|-------------|-----|
|
||||
| 7.7 | 2021-02-24 | Czechoslovakian medium tanks | |
|
||||
| 7.6 | 2021-01-27 | AMC 13 90, TOG II buffs, free replay camera | |
|
||||
| 7.5 | 2020-12-09 | ATGM removal, Batchat, Kpz70, T-2020 buff | |
|
||||
| 7.4 | 2020-11-12 | TD buffs, Clan missions | |
|
||||
| 7.3 | 2020-10-08 | Swedish HT line | |
|
||||
| 7.2 | 2020-08-19 | Balance changes, added tier X ammo capa, new aiming reticles | |
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
,TOTAL,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
|
||||
,Total,6942,100%,71.80%,2058,1.86,1.4,64%,2.3,5.4,86.10%,90.80%,65.30%,90%,57.08%,50.15%,50.12%,0.20%
|
||||
,,,,,,,,,,,,,,,,,,
|
||||
,Result,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
|
||||
,Loss,1959,28%,0.00%,1715,1.12,1.3,61%,1.3,1.3,84.70%,88.60%,13.40%,78%,56.47%,49.56%,50.92%,0.10%
|
||||
,Win,4983,72%,100.00%,2193,2.16,1.4,65%,3,15.1,86.50%,91.40%,85.70%,95%,57.32%,50.39%,49.80%,0.20%
|
||||
,,,,,,,,,,,,,,,,,,
|
||||
,Battle Type,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
|
||||
,Encounter,4652,67%,73.00%,1969,1.98,1.4,67%,2.5,6,85.70%,90.70%,66.80%,91%,57.43%,50.41%,50.33%,0.30%
|
||||
,Supremacy,2290,33%,69.30%,2238,1.63,1.2,60%,2,4.3,87.10%,91.20%,62.20%,89%,56.37%,49.63%,49.69%,0.00%
|
||||
,,,,,,,,,,,,,,,,,,
|
||||
,Tier,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
|
||||
,Bottom tier,2480,36%,69.50%,1750,1.53,1.2,0%,2.2,3.9,85.40%,89.20%,60.70%,88%,56.91%,50.13%,49.99%,0.10%
|
||||
,Top tier,4462,64%,73.10%,2229,2.05,1.5,100%,2.4,6.4,86.50%,91.60%,67.90%,91%,57.18%,50.16%,50.19%,0.20%
|
||||
,,,,,,,,,,,,,,,,,,
|
||||
,Tank Tier,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
|
||||
,1,89,1%,80.90%,678,1.89,1.9,40%,3,7.6,86.20%,91.80%,75.30%,95%,55.40%,48.80%,48.40%,0.00%
|
||||
,2,180,3%,77.80%,970,2.64,1.6,73%,3.3,9.1,82.90%,90.60%,71.10%,93%,56.04%,49.36%,49.46%,0.90%
|
||||
,3,540,8%,75.70%,1099,2.15,1.4,36%,3.1,7,83.60%,90.00%,69.10%,93%,57.55%,51.15%,50.16%,1.90%
|
||||
,4,628,9%,75.20%,1256,2.21,1.3,73%,3.1,7.4,85.00%,91.60%,70.20%,94%,55.72%,51.76%,51.65%,0.00%
|
||||
,5,626,9%,80.20%,1503,2.29,1.3,71%,3.2,8.3,86.90%,89.50%,72.20%,93%,55.69%,50.38%,49.95%,0.00%
|
||||
,6,1064,15%,74.20%,1801,1.97,1.4,62%,2.4,5.8,86.70%,92.20%,65.90%,90%,56.82%,49.24%,49.31%,0.00%
|
||||
,7,1141,16%,71.80%,2143,1.69,1.3,50%,2.5,4.9,85.70%,90.10%,65.80%,90%,58.48%,49.62%,49.60%,0.00%
|
||||
,8,1222,18%,68.20%,2371,1.59,1.4,67%,2.1,4.2,87.40%,90.90%,61.80%,89%,56.84%,49.80%,49.99%,0.00%
|
||||
,9,598,9%,64.70%,2806,1.61,1.2,49%,2.1,4,89.10%,91.90%,59.50%,87%,56.58%,50.13%,50.32%,0.00%
|
||||
,10,854,12%,65.60%,3270,1.62,1.5,100%,2,4,88.30%,90.20%,59.60%,88%,58.37%,50.83%,51.16%,0.00%
|
||||
,,,,,,,,,,,,,,,,,,
|
||||
,Tank Class,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
|
||||
,Light Tank,1337,19%,74.40%,1589,1.88,1.9,59%,2.3,5.9,83.70%,89.20%,68.40%,90%,57.15%,50.20%,49.90%,0.70%
|
||||
,Medium Tank,1957,28%,73.10%,2181,1.99,1.7,67%,2.4,5.9,88.20%,91.40%,66.40%,90%,57.23%,50.26%,50.12%,0.10%
|
||||
,Heavy Tank,2032,29%,70.00%,2180,1.75,1.2,70%,1.9,5,87.20%,90.70%,65.20%,91%,56.72%,50.16%,50.23%,0.00%
|
||||
,Tank Destroyer,1616,23%,70.30%,2144,1.84,0.7,59%,3,4.8,85.10%,92.60%,61.50%,91%,57.31%,49.98%,50.16%,0.00%
|
||||
,,,,,,,,,,,,,,,,,,
|
||||
,Player WR,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
|
||||
,35 - 45%,80,1%,43.80%,1255,0.93,1,55%,1.1,1.5,84.00%,87.10%,37.50%,82%,44.52%,49.84%,51.14%,0.00%
|
||||
,45 - 50%,584,8%,69.20%,1397,1.46,1.1,62%,1.8,3.8,81.20%,87.40%,61.30%,89%,47.86%,49.84%,49.86%,0.00%
|
||||
,50 - 55%,1970,28%,69.00%,1861,1.73,1.3,63%,2.1,4.3,85.40%,90.20%,60.20%,88%,53.06%,50.10%,49.99%,0.00%
|
||||
,55 - 65%,3677,53%,73.50%,2249,2.01,1.4,66%,2.5,6.3,87.50%,91.50%,68.10%,92%,58.78%,50.13%,50.14%,0.00%
|
||||
,65 - %,631,9%,76.40%,2274,1.95,1.4,61%,2.8,7,84.60%,91.40%,72.10%,91%,69.92%,50.79%,50.51%,1.90%
|
||||
,,,,,,,,,,,,,,,,,,
|
||||
,Player Hit Rate,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
|
||||
, 0 - 25%,32,0%,43.80%,1,0,1.6,47%,0,0,0.00%, inf%,25.00%,57%,54.55%,50.15%,51.14%,0.00%
|
||||
,25 - 50%,47,1%,44.70%,693,0.34,1.3,51%,0.6,0.5,38.70%,90.50%,36.20%,77%,55.05%,50.14%,51.15%,0.00%
|
||||
,50 - 60%,172,2%,56.40%,876,0.6,1.2,58%,0.8,1.1,54.70%,87.80%,44.80%,81%,56.38%,50.61%,50.48%,0.00%
|
||||
,60 - 70%,461,7%,58.80%,1254,1.08,1.2,61%,1.4,2.4,65.40%,87.70%,54.20%,87%,56.29%,50.18%,50.50%,0.00%
|
||||
,70 - 80%,941,14%,70.50%,1691,1.59,1.3,63%,2,4.6,75.50%,89.10%,65.70%,91%,56.92%,50.36%,50.24%,0.00%
|
||||
,80 - 90%,2091,30%,74.00%,2071,1.98,1.4,66%,2.5,6.3,85.00%,90.60%,68.50%,93%,57.00%,50.10%,50.08%,0.10%
|
||||
,90 - 95%,1258,18%,81.00%,2561,2.52,1.5,66%,3.3,11,92.30%,92.00%,77.10%,96%,57.73%,50.08%,49.85%,0.30%
|
||||
,95 - %,1940,28%,69.60%,2259,1.82,1.4,64%,2.2,4.5,99.00%,91.70%,59.80%,86%,57.17%,50.12%,50.11%,0.40%
|
||||
,,,,,,,,,,,,,,,,,,
|
||||
,Player Pen Rate,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
|
||||
, 0 - 25%,49,1%,38.80%,6,0,1.5,39%,0,0,45.80%,10.50%,22.40%,56%,55.11%,49.96%,50.47%,0.00%
|
||||
,25 - 50%,43,1%,30.20%,447,0.19,1.7,44%,0.3,0.2,82.30%,38.40%,20.90%,66%,55.41%,50.75%,51.06%,0.00%
|
||||
,50 - 60%,110,2%,41.80%,642,0.27,1.5,60%,0.5,0.4,77.50%,53.10%,32.70%,70%,54.44%,50.46%,50.61%,0.00%
|
||||
,60 - 70%,210,3%,52.40%,1085,0.63,1.3,56%,0.9,1.1,81.00%,65.10%,40.00%,79%,55.71%,50.58%,50.39%,0.00%
|
||||
,70 - 80%,469,7%,65.00%,1669,1.33,1.4,61%,1.6,3.1,83.50%,75.40%,57.60%,89%,56.39%,50.24%,50.30%,0.00%
|
||||
,80 - 90%,1544,22%,71.60%,2076,1.86,1.4,61%,2.3,5.5,86.00%,85.30%,66.30%,92%,57.35%,50.17%,50.03%,0.20%
|
||||
,90 - 95%,1210,17%,79.30%,2509,2.53,1.5,66%,3.3,11,87.80%,92.40%,77.00%,96%,57.57%,50.00%,50.09%,0.20%
|
||||
,95 - %,3307,48%,73.30%,2100,1.88,1.3,67%,2.4,5.5,86.30%,99.30%,65.60%,90%,57.11%,50.14%,50.10%,0.20%
|
||||
,,,,,,,,,,,,,,,,,,
|
||||
Server,account_id_anon,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
|
||||
RU,1,33,0%,78.80%,3112,1.91,1.6,67%,2.5,7.9,89.60%,90.20%,75.80%,87%,71.43%,50.74%,51.07%,0.00%
|
||||
RU,2,23,0%,87.00%,3525,2.78,1.7,74%,4.3,32,90.40%,94.40%,91.30%,100%,59.39%,48.07%,48.75%,0.00%
|
||||
RU,3,81,1%,66.70%,1927,1.47,1,75%,1.9,3.1,82.30%,89.60%,53.10%,86%,69.59%,50.23%,50.85%,0.00%
|
||||
EU,4,26,0%,80.80%,4082,2.62,1.2,62%,4.1,17,91.10%,93.80%,84.60%,98%,53.92%,48.75%,49.31%,0.00%
|
||||
EU,5,79,1%,59.50%,1920,0.97,1.4,75%,1.4,2,80.20%,87.10%,50.60%,84%,59.06%,50.12%,51.22%,0.00%
|
||||
EU,6,92,1%,60.90%,1685,0.99,1.5,65%,1.3,1.8,87.80%,88.50%,45.70%,83%,57.79%,50.30%,50.77%,0.00%
|
||||
EU,7,50,1%,78.00%,2787,2.92,1.9,52%,4,16.2,90.60%,92.70%,82.00%,99%,64.87%,50.49%,50.50%,0.00%
|
||||
EU,8,69,1%,60.90%,1695,1.48,1.9,55%,1.5,3,87.80%,92.10%,50.70%,77%,54.73%,50.06%,49.81%,0.10%
|
||||
EU,9,33,0%,63.60%,1529,1.55,1.9,64%,2,3.6,82.10%,91.30%,57.60%,85%,52.48%,49.85%,50.92%,0.00%
|
||||
EU,10,22,0%,90.90%,2990,3.82,1,59%,3.6,42,85.60%,93.00%,90.90%,100%,57.27%,48.97%,49.67%,0.00%
|
||||
EU,11,35,1%,62.90%,1197,0.97,1.1,66%,1.3,2,85.20%,88.10%,51.40%,81%,55.89%,50.71%,49.97%,0.00%
|
||||
EU,12,20,0%,95.00%,4854,3.95,1.2,25%,4.9,79,94.40%,95.60%,95.00%,99%,56.71%,49.15%,49.81%,0.00%
|
||||
EU,13,80,1%,73.80%,2228,2.65,1.4,69%,3.3,8.5,92.10%,92.70%,68.80%,95%,58.83%,49.43%,49.21%,0.00%
|
||||
EU,14,30,0%,83.30%,2167,3.93,2,63%,4.2,19.7,87.90%,92.00%,80.00%,98%,62.43%,50.39%,49.69%,0.00%
|
||||
EU,15,305,4%,55.40%,2126,1.21,1.3,78%,1.6,2.4,85.80%,90.90%,49.50%,87%,58.91%,50.46%,50.47%,0.00%
|
||||
EU,16,49,1%,69.40%,2362,2.47,1.5,43%,4,7.1,90.20%,93.30%,65.30%,94%,62.94%,49.43%,49.50%,0.00%
|
||||
EU,17,22,0%,86.40%,1898,4.41,1.4,95%,4.8,48.5,90.60%,94.80%,90.90%,100%,57.61%,48.11%,49.25%,0.00%
|
||||
EU,18,96,1%,76.00%,1764,2.19,0.9,67%,3.2,7.2,89.40%,95.00%,69.80%,94%,56.39%,50.00%,49.58%,0.00%
|
||||
EU,19,23,0%,95.70%,1997,2.09,1.3,65%,3.8,16,90.70%,94.20%,87.00%,98%,70.96%,48.51%,49.66%,0.00%
|
||||
EU,20,256,4%,54.30%,1081,0.58,1.5,55%,0.9,0.8,81.10%,83.80%,28.10%,68%,52.29%,50.31%,49.81%,0.00%
|
||||
EU,21,73,1%,82.20%,1025,1.9,1.5,55%,2.7,6.3,84.20%,92.30%,69.90%,94%,58.95%,51.61%,50.45%,0.00%
|
||||
EU,22,53,1%,79.20%,1415,1.98,0.9,47%,3.2,7,87.40%,90.40%,71.70%,95%,57.15%,50.10%,49.76%,0.00%
|
||||
EU,23,70,1%,81.40%,2224,2.5,1.8,47%,4,9.7,89.20%,90.20%,74.30%,96%,61.99%,50.85%,50.17%,0.00%
|
||||
EU,24,83,1%,73.50%,3049,2.1,2,73%,3.2,7.2,89.60%,91.70%,71.10%,96%,60.48%,49.78%,49.51%,0.00%
|
||||
EU,25,127,2%,68.50%,1609,1.86,1.3,52%,2.4,4.5,83.40%,89.50%,59.10%,89%,54.64%,49.76%,49.35%,0.00%
|
||||
EU,26,52,1%,80.80%,2669,2.62,2.2,50%,3,11.3,87.80%,91.00%,76.90%,95%,62.66%,49.60%,49.65%,0.20%
|
||||
EU,27,30,0%,86.70%,4246,3.17,1.4,57%,3.6,23.8,88.10%,95.30%,86.70%,99%,56.38%,49.10%,49.54%,0.00%
|
||||
EU,28,24,0%,83.30%,2823,2.96,1.8,62%,4.9,17.8,89.30%,91.20%,83.30%,98%,64.91%,50.25%,51.06%,0.00%
|
||||
EU,29,29,0%,75.90%,1750,1.62,1.3,59%,2.2,3.6,88.10%,93.60%,55.20%,91%,54.39%,49.78%,49.34%,0.00%
|
||||
EU,30,26,0%,92.30%,2631,3.12,1.8,69%,3.1,27,92.90%,93.80%,88.50%,97%,57.57%,49.93%,49.96%,0.00%
|
||||
EU,31,31,0%,90.30%,2126,2.16,1.2,61%,3.4,13.4,85.20%,91.20%,83.90%,98%,52.31%,50.24%,49.30%,0.00%
|
||||
EU,32,44,1%,77.30%,1848,2.91,0.7,77%,3.4,14.2,85.40%,91.70%,79.50%,96%,50.22%,50.67%,50.91%,0.00%
|
||||
EU,33,74,1%,94.60%,1811,3.24,1.5,59%,4.1,48,86.80%,86.30%,93.20%,99%,56.08%,49.98%,49.17%,0.00%
|
||||
EU,34,158,2%,77.80%,1290,1.66,0.9,53%,2.4,4.4,78.40%,90.70%,62.70%,93%,49.45%,49.80%,50.05%,0.00%
|
||||
EU,35,21,0%,61.90%,1796,1.19,1.4,86%,1.5,2.5,77.00%,86.60%,52.40%,82%,48.99%,50.98%,50.38%,0.00%
|
||||
EU,36,120,2%,55.80%,1730,1.1,1.6,68%,1.3,2.2,85.30%,85.90%,49.20%,83%,60.61%,50.43%,49.95%,0.10%
|
||||
EU,37,25,0%,88.00%,5110,3.6,1.5,96%,3.7,45,94.90%,91.70%,92.00%,100%,60.44%,50.29%,51.16%,0.00%
|
||||
EU,38,350,5%,62.00%,1159,1.09,1.3,63%,1.2,2.3,83.10%,89.70%,52.30%,84%,52.65%,50.37%,50.42%,0.00%
|
||||
EU,39,25,0%,84.00%,4965,2.72,1.5,84%,3.6,13.6,89.40%,91.30%,80.00%,97%,54.95%,49.64%,50.59%,0.00%
|
||||
EU,40,22,0%,90.90%,4394,3.09,2.3,64%,4.2,34,92.60%,90.30%,90.90%,98%,52.49%,50.46%,50.63%,0.00%
|
||||
EU,41,57,1%,63.20%,3078,1.63,2.3,96%,2,3.3,85.40%,86.30%,50.90%,85%,60.62%,51.59%,52.10%,0.00%
|
||||
EU,42,22,0%,68.20%,2270,0.95,1.3,91%,1.6,1.9,85.20%,88.90%,50.00%,85%,59.46%,50.97%,50.61%,0.00%
|
||||
EU,43,45,1%,64.40%,1482,0.93,1.6,62%,1.2,1.8,83.00%,91.60%,48.90%,79%,50.40%,51.15%,50.79%,0.00%
|
||||
EU,44,56,1%,96.40%,1927,2.79,1.6,77%,5.4,31.2,85.60%,90.20%,91.10%,98%,54.95%,49.96%,49.62%,0.00%
|
||||
EU,45,28,0%,96.40%,4659,3.5,1.4,64%,3.8,49,89.00%,91.70%,92.90%,100%,54.81%,49.56%,49.85%,0.00%
|
||||
EU,46,134,2%,70.90%,1291,1.99,1,49%,3.3,5.6,82.90%,91.80%,64.20%,94%,55.71%,51.75%,51.64%,0.00%
|
||||
EU,47,51,1%,72.50%,1119,1.43,0.9,78%,1.9,3.8,89.20%,89.20%,62.70%,89%,52.58%,52.04%,52.06%,0.00%
|
||||
EU,48,45,1%,86.70%,2756,2.71,2.2,64%,4.1,12.2,90.10%,91.50%,77.80%,97%,63.68%,49.88%,49.92%,0.00%
|
||||
EU,49,35,1%,97.10%,5932,3.57,1.1,94%,5.7,41.7,88.10%,96.30%,91.40%,99%,70.46%,49.51%,50.42%,0.00%
|
||||
EU,50,186,3%,60.80%,1169,1.02,1.2,49%,1.2,2.1,80.20%,89.20%,52.20%,84%,56.65%,50.14%,50.07%,0.00%
|
||||
EU,51,25,0%,92.00%,2194,4.68,2.3,60%,4.9,117,86.20%,92.00%,96.00%,100%,70.74%,52.26%,51.76%,0.00%
|
||||
EU,52,93,1%,62.40%,1444,1.1,1.1,54%,1.7,2.9,88.00%,92.30%,62.40%,85%,72.96%,49.89%,50.29%,0.00%
|
||||
EU,53,25,0%,80.00%,2374,2.8,1.5,68%,3.5,11.7,86.80%,90.50%,76.00%,95%,60.96%,49.42%,48.68%,0.00%
|
||||
EU,54,30,0%,53.30%,2384,1.27,1.7,73%,1.6,2.5,85.00%,86.50%,50.00%,81%,54.58%,49.33%,50.27%,0.00%
|
||||
EU,55,29,0%,79.30%,2219,1.93,1.6,69%,2.1,5.1,88.30%,86.10%,62.10%,89%,47.58%,50.43%,49.39%,0.00%
|
||||
EU,56,73,1%,69.90%,2036,2.33,0.9,64%,3.2,8.5,90.80%,94.00%,72.60%,95%,52.47%,49.24%,49.05%,0.10%
|
||||
EU,57,26,0%,96.20%,949,3.08,2,50%,3.2,26.7,83.20%,87.10%,88.50%,97%,46.64%,49.42%,48.36%,0.00%
|
||||
EU,58,20,0%,65.00%,344,0.45,0.9,45%,0.7,1.3,75.50%,73.80%,65.00%,83%,45.36%,49.30%,49.03%,0.00%
|
||||
EU,59,149,2%,34.90%,1105,0.8,1.1,60%,0.9,1.1,82.80%,85.80%,28.90%,78%,45.21%,50.08%,51.28%,0.00%
|
||||
EU,60,22,0%,63.60%,1108,0.91,1,91%,1.3,1.4,78.70%,85.00%,36.40%,81%,54.54%,49.66%,49.03%,0.00%
|
||||
EU,61,36,1%,94.40%,2794,3.81,2.5,83%,6.2,45.7,85.30%,92.10%,91.70%,100%,63.28%,50.37%,50.28%,0.00%
|
||||
EU,62,32,0%,31.20%,1786,1.19,0.9,75%,1.8,2,90.60%,91.90%,40.60%,88%,61.60%,49.13%,50.35%,0.00%
|
||||
EU,63,21,0%,85.70%,2476,3.33,2,76%,4.8,35,91.50%,95.70%,90.50%,99%,53.93%,50.29%,49.74%,0.00%
|
||||
NA,64,23,0%,91.30%,2371,3.57,0.9,70%,4.8,20.5,86.10%,92.50%,82.60%,99%,58.19%,49.00%,50.44%,0.30%
|
||||
NA,65,25,0%,96.00%,4281,2.56,1.8,72%,4.1,64,90.20%,91.40%,96.00%,100%,62.10%,51.41%,50.28%,0.00%
|
||||
NA,66,238,3%,76.10%,2183,1.56,1.5,50%,2.8,5.7,79.70%,89.90%,72.70%,92%,68.48%,50.75%,50.57%,0.00%
|
||||
NA,67,56,1%,73.20%,2022,1.88,1.3,68%,2.5,6.6,85.20%,90.40%,71.40%,93%,55.31%,49.88%,49.73%,0.10%
|
||||
NA,68,25,0%,92.00%,6141,3.36,1.2,100%,3.5,28,92.20%,93.80%,88.00%,99%,62.64%,49.81%,50.96%,0.00%
|
||||
NA,69,44,1%,77.30%,4178,1.93,1.5,77%,3,12.1,92.10%,91.60%,84.10%,98%,53.67%,50.20%,50.23%,0.00%
|
||||
NA,70,26,0%,96.20%,3412,3.46,1.8,69%,3.6,90,92.20%,93.90%,96.20%,100%,65.85%,48.95%,49.25%,0.00%
|
||||
NA,71,58,1%,77.60%,1642,1.93,0.9,60%,2.2,5.6,86.00%,94.60%,65.50%,87%,46.03%,49.73%,50.08%,0.00%
|
||||
NA,72,197,3%,78.20%,2416,2.37,1.2,64%,3.3,10.8,86.30%,92.80%,78.20%,97%,56.68%,49.22%,49.50%,0.00%
|
||||
NA,73,23,0%,60.90%,3932,1.91,2,87%,3,4.9,89.50%,90.70%,60.90%,89%,60.65%,48.75%,50.51%,0.00%
|
||||
NA,74,42,1%,78.60%,1431,2.45,0.7,36%,2.2,6.9,84.50%,89.30%,64.30%,90%,50.81%,49.26%,49.33%,0.00%
|
||||
NA,75,21,0%,81.00%,3748,3.29,1.3,57%,4,17.2,91.90%,92.50%,81.00%,100%,61.77%,48.23%,48.98%,0.00%
|
||||
NA,76,34,0%,88.20%,2707,2.59,1.4,68%,3,44,84.50%,90.50%,94.10%,96%,55.79%,50.34%,49.42%,0.00%
|
||||
NA,77,115,2%,74.80%,2171,2.19,1.4,74%,2.9,8.4,89.00%,91.40%,73.90%,95%,59.53%,49.65%,49.88%,0.00%
|
||||
NA,78,23,0%,65.20%,1958,1.96,0.5,61%,2.3,5.6,89.80%,93.30%,65.20%,92%,62.10%,50.31%,49.85%,0.00%
|
||||
NA,79,218,3%,74.30%,1860,2.16,1.5,65%,2.6,7.2,85.90%,91.60%,70.20%,95%,53.76%,50.11%,49.84%,0.00%
|
||||
NA,80,35,1%,57.10%,3142,1.71,1.9,80%,2.3,3.2,90.00%,93.10%,45.70%,86%,56.23%,50.16%,51.49%,0.00%
|
||||
NA,81,32,0%,90.60%,3407,2.62,1.2,62%,3.6,16.8,85.80%,91.90%,84.40%,97%,54.91%,49.20%,49.43%,0.00%
|
||||
NA,82,20,0%,90.00%,4472,3.4,2.2,75%,4.1,34,86.20%,83.10%,90.00%,100%,50.16%,50.67%,50.02%,0.00%
|
||||
NA,83,51,1%,72.50%,1023,2.08,1.1,76%,2.2,5.6,88.30%,93.20%,62.70%,91%,73.85%,56.05%,50.69%,23.20%
|
||||
NA,84,47,1%,80.90%,3820,2.45,1.4,85%,3.7,12.8,93.20%,94.90%,80.90%,98%,61.01%,50.73%,50.39%,0.00%
|
||||
NA,85,71,1%,91.50%,3159,2.73,0.9,65%,3.9,27.7,89.40%,92.40%,90.10%,99%,55.39%,49.64%,50.01%,0.00%
|
||||
NA,86,28,0%,85.70%,4249,2.46,1.3,79%,3.1,23,91.90%,93.70%,89.30%,95%,61.07%,50.13%,50.15%,0.00%
|
||||
NA,87,24,0%,75.00%,3048,2.17,1.9,75%,2.7,10.4,85.40%,90.00%,79.20%,98%,55.74%,49.79%,50.05%,0.00%
|
||||
NA,88,34,0%,67.60%,3317,2.21,2.1,65%,3.6,6.8,90.50%,91.30%,67.60%,96%,61.83%,49.97%,49.88%,0.00%
|
||||
NA,89,23,0%,30.40%,1399,0.52,1.3,91%,0.9,0.8,87.30%,85.40%,30.40%,71%,49.58%,50.38%,51.69%,0.00%
|
||||
NA,90,20,0%,75.00%,1305,0.6,1.4,60%,1.1,1.7,88.10%,80.10%,65.00%,91%,48.23%,49.76%,49.40%,0.00%
|
||||
NA,91,24,0%,79.20%,2554,2.96,1.2,50%,5.6,17.8,93.30%,95.50%,83.30%,98%,58.79%,50.82%,51.10%,0.00%
|
||||
NA,92,48,1%,56.20%,1707,0.69,0.7,92%,0.8,1.4,81.30%,86.80%,52.10%,87%,55.85%,53.43%,53.97%,0.00%
|
||||
NA,93,130,2%,62.30%,1912,1.31,1.3,70%,1.3,2.9,89.00%,91.70%,55.40%,86%,59.63%,49.95%,50.11%,0.00%
|
||||
NA,94,35,1%,77.10%,3875,2.74,1.8,57%,3.6,12,89.60%,94.40%,77.10%,95%,59.73%,49.60%,50.04%,0.00%
|
||||
NA,95,23,0%,73.90%,2688,3.17,1,74%,3.4,14.6,92.50%,93.20%,78.30%,96%,51.21%,49.86%,50.33%,0.00%
|
||||
NA,96,26,0%,61.50%,1796,1.19,1.2,31%,1.6,2.8,75.60%,84.20%,57.70%,91%,54.00%,50.02%,50.03%,0.00%
|
||||
NA,97,21,0%,90.50%,1869,2.52,2.3,76%,3.2,17.7,85.40%,89.60%,85.70%,96%,56.43%,49.38%,49.01%,0.00%
|
||||
NA,98,101,1%,71.30%,1499,2,0.7,58%,2.7,5.3,83.30%,91.50%,62.40%,93%,52.60%,49.80%,49.63%,0.10%
|
||||
NA,99,20,0%,60.00%,1368,1,0.9,45%,1.4,2.9,83.50%,87.90%,65.00%,84%,54.73%,50.49%,50.26%,0.00%
|
||||
NA,100,24,0%,79.20%,1998,2.54,1.6,75%,3.4,20.3,85.30%,83.70%,87.50%,98%,53.43%,50.95%,50.42%,0.30%
|
||||
NA,101,26,0%,42.30%,1170,0.77,1.7,73%,1.3,1.2,75.50%,86.50%,34.60%,81%,46.59%,49.31%,50.13%,0.00%
|
||||
NA,102,26,0%,53.80%,1013,0.85,0.8,62%,1.1,1.2,74.20%,82.40%,26.90%,83%,52.75%,49.08%,49.42%,0.00%
|
||||
NA,103,23,0%,91.30%,3980,3,1.4,83%,3.1,34.5,86.00%,90.80%,91.30%,97%,63.72%,49.86%,49.82%,0.00%
|
||||
NA,104,57,1%,71.90%,1694,1.49,0.6,74%,2.8,4.7,82.80%,86.80%,68.40%,92%,46.26%,49.48%,48.84%,0.00%
|
||||
ASIA,105,330,5%,82.40%,2195,2.01,1.7,63%,2.7,7.5,89.80%,92.70%,73.00%,94%,57.98%,49.95%,49.71%,0.00%
|
||||
ASIA,106,132,2%,77.30%,1268,1.31,0.8,58%,2.2,3.5,82.20%,92.40%,62.10%,85%,62.78%,49.79%,50.26%,0.00%
|
||||
ASIA,107,21,0%,90.50%,3749,3.67,1.8,81%,4.1,38.5,90.70%,93.70%,90.50%,97%,53.77%,49.37%,50.06%,0.00%
|
||||
ASIA,108,35,1%,85.70%,4501,2.6,1.6,74%,4,15.2,89.10%,91.20%,82.90%,99%,54.47%,49.39%,49.24%,0.00%
|
||||
ASIA,109,67,1%,73.10%,1278,1.7,1.5,54%,2.8,5.7,83.80%,88.00%,70.10%,93%,55.23%,51.01%,50.11%,0.10%
|
||||
ASIA,110,70,1%,91.40%,1525,1.54,1.1,54%,3.5,54,78.50%,86.10%,97.10%,99%,49.45%,49.53%,48.75%,0.00%
|
||||
ASIA,111,49,1%,67.30%,2050,1.96,0.7,73%,2.7,5.3,85.70%,91.40%,63.30%,93%,52.58%,49.79%,49.79%,0.00%
|
||||
|
+5124
-3440
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3.8
|
||||
#!/usr/bin/env python3
|
||||
## PYTHON VERSION MUST BE 3.7 OR HIGHER
|
||||
|
||||
import json, argparse, inspect, sys, os, io, base64, aiohttp, urllib, asyncio, aiofiles, aioconsole
|
||||
import logging, re, concurrent.futures, configparser, motor.motor_asyncio, ssl, zipfile
|
||||
|
||||
Reference in new issue
Block a user