Author SHA1 Message Date
Jylpah ec696d6946 add pyutils 2022-10-29 17:47:33 +03:00
Jylpah 1f41901042 update 9.2 2022-10-09 00:18:30 +03:00
Jylpah d275914b32 Update .gitignore 2022-08-18 09:44:39 +03:00
Jylpah 9f8c82f434 Stream client support 2022-08-18 09:42:55 +03:00
Jylpah 3a8f065f35 Update 9.2 2022-08-18 09:42:32 +03:00
Jylpah 0cac200f06 Update 9.1 2022-07-14 11:25:38 +03:00
Jylpah 342b90bcf9 SSL -> TLS 2022-06-09 20:48:50 +03:00
Jylpah a9845b6fbb Update 9.0 2022-06-08 17:25:06 +03:00
Jylpah ee36e9192f Update 8.10 2022-04-30 21:43:24 +03:00
Jylpah 5d5821564e update 8.9 2022-03-24 15:44:48 +02:00
Jylpah 3c6f9dcae3 Merge branch 'master' of https://github.com/Jylpah/blitz-tools 2022-03-24 15:41:26 +02:00
Jylpah 433e2295c2 Update 8.9 2022-03-24 15:41:22 +02:00
Jylpah 3dc9533c6a Update maps.json 2022-03-24 15:41:15 +02:00
Jylpah 83456ae150 Add updates 8.5 - 8.8 2022-02-16 17:48:01 +02:00
Jylpah 0c25c781a9 Update 8.8 2022-02-16 17:41:50 +02:00
Jylpah 244ae314d8 Update 8.8 2022-02-16 17:40:17 +02:00
Jylpah 33b0efb48d Update 8.6 2022-01-22 22:27:34 +02:00
Jylpah 574fb95f00 Update 8.6 2021-12-20 10:15:47 +02:00
Jylpah fdc5db966e Update installation and usage instructions 2021-11-24 10:28:32 +02:00
Jylpah 65e4fd83d8 Update installation instructions 2021-11-24 09:48:39 +02:00
Jylpah e9451c7f5d Merge pull request #34 from Jylpah/stable-filters-update2
Fix --filters and improve performance when filtering
2021-11-23 21:42:45 +02:00
Jylpah fa65872ad2 --filters: Bug fix and performance improvement 2021-11-23 21:42:09 +02:00
Jylpah b2f37ebcf5 Merge branch 'stable-filter-update' into stable-filters-update2 2021-11-23 18:44:09 +02:00
Jylpah d323f11662 Update analyze_wotb_replays.py 2021-11-23 18:38:49 +02:00
Jylpah d847a394a9 Add allies/enemies categorizations: xxx_damage_dealt, xxx_wins, process_battle_results(): Fix bug filter(s) 2021-11-23 18:20:11 +02:00
Jylpah 8514e9db17 Update analyze_wotb_replays.py 2021-11-23 18:08:24 +02:00
Jylpah 521986adf1 Update 8.5 2021-11-23 18:01:17 +02:00
Jylpah 7e39fdc442 Fix --filters bug with number categorization 2021-11-23 09:46:04 +02:00
Jylpah 14ee570ee5 Merge branch 'master' of https://github.com/Jylpah/blitz-tools 2021-10-29 21:18:27 +03:00
Jylpah e0228a4bf1 Fix shebang 2021-10-29 21:18:24 +03:00
Jylpah 2a93cfbce3 update 8.4 2021-10-24 21:19:06 +03:00
Jylpah 78a959224c Update 8.4 2021-10-23 22:07:23 +03:00
Jylpah ae24ab35f7 Updates 8.0 - 8.3 added 2021-09-17 10:44:38 +03:00
Jylpah ba6b4781e5 Update 8.3 2021-09-16 17:31:10 +03:00
Jylpah 050bd99488 get_filter_categories(): fix, add room_type=5: Quick Tournament(?) 2021-09-10 21:03:41 +03:00
Jylpah eb39c9ce84 Update 8.2 2021-08-22 21:21:42 +03:00
Jylpah 24547763e9 replayWorker(): Better warning text to incomplete replays 2021-08-13 13:43:17 +03:00
Jylpah 28347e1653 Fix printing of player/allies/enemies average damage columns 2021-08-07 16:42:19 +03:00
Jylpah ffe5afbf1e getTitle(): More robust tank names based on tank_ids, not UsrStrs that keep changing. 2021-08-07 16:40:42 +03:00
Jylpah 17ac09afde Update analyze_wotb_replays.py 2021-08-07 16:26:25 +03:00
Jylpah 9117963085 filter_results() bug fix 2021-08-03 19:14:44 +03:00
Jylpah dc8f9a233d Merge pull request #32 from Jylpah/master-platoon
help_extended(): change order of filter helps
2021-08-03 12:43:59 +03:00
Jylpah ce3f9993c5 help_extended(): change order of filter helps 2021-08-03 12:43:43 +03:00
Jylpah fd7eb8261d Merge pull request #31 from Jylpah/master-platoon
Added platoon rate and categorization by platoon/solo games
2021-08-03 12:43:19 +03:00
Jylpah 748ed7806b Added platoon rate and categorization by platoon/solo games 2021-08-03 12:40:36 +03:00
Jylpah 1ada279728 Update Lupus tanks.json 2021-07-27 18:47:08 +03:00
Jylpah 57cbc5b233 Tanks sorted by tank_id 2021-07-15 20:22:46 +03:00
Jylpah 43842dc140 Update 8.1 2021-07-15 20:21:53 +03:00
Jylpah 170a801652 sort_dict(): added 2021-07-15 20:21:32 +03:00
Jylpah 210e943157 Update 8.0 2021-06-08 16:57:42 +03:00
Jylpah b3b7b81eb4 BattleCategory(): add 'hits_bounced' result field 2021-06-03 15:02:27 +03:00
Jylpah 446e289041 Add --mode dmg_received, improved --mode help, new fields 2021-06-03 14:48:17 +03:00
Jylpah f8a0fdfbe1 Merge pull request #30 from Jylpah/stable-export-replays
Stable export replays
2021-05-12 00:16:42 +03:00
12 changed files with 1516 additions and 321 deletions

No files matched your search

Executable
+15
View File
@@ -0,0 +1,15 @@
__pycache__/
BlitzApp
tmp/
ssl/
*.ini
*.swp
*.code-workspace
*.sqlite3
.vscode/
*.sqlite3-journal
.gitattributes
export*
*.csv
!/stats/*.csv
!*.py
+3
View File
@@ -0,0 +1,3 @@
[submodule "pyutils"]
path = pyutils
url = https://github.com/Jylpah/pyutils.git
+25 -23
View File
@@ -1,5 +1,5 @@
# blitz-tools # blitz-tools
Python scripts to analyze WoT Blitz replays and stats. All the scripts require [Python 3.7](https://www.python.org/downloads/) or higher. I am using 3.8 myself so you may have to change the Python interpreter version in the scripts' first line. Python scripts to analyze WoT Blitz replays and stats. All the scripts require [Python 3.7](https://www.python.org/downloads/) or later.
### Replays ### Replays
* [upload_wotb_replays.py](upload_wotb_replays.py): An asynchronous replay uploader. * [upload_wotb_replays.py](upload_wotb_replays.py): An asynchronous replay uploader.
@@ -13,46 +13,48 @@ Python scripts to analyze WoT Blitz replays and stats. All the scripts require [
* [blitzutils.py](blitzutils.py): A utils class for various Blitz related to functions. *Other scripts use this*, so you need to place it to the same folder with the other scripts * [blitzutils.py](blitzutils.py): A utils class for various Blitz related to functions. *Other scripts use this*, so you need to place it to the same folder with the other scripts
### Data ### Data
* [tanks.json](tanks.json): Tankopedia in WG JSON format extracted from the Blitz app files with [extract_tankopedia.py]](extractTankopedia.py]). Contains only limited information required by the scripts. * [tanks.json](tanks.json): Tankopedia in WG JSON format extracted from the Blitz app files with [extract_tankopedia.py](extractTankopedia.py). Contains only limited information required by the scripts.
* [maps.json](maps.json): Blitz map names extracted from the Blitz app files with [extract_tankopedia.py]](extract_tankopedia.py]). * [maps.json](maps.json): Blitz map names extracted from the Blitz app files with [extract_tankopedia.py](extract_tankopedia.py).
### Quick instructions ### Quick instructions
1. Install [Python 3.8](https://www.python.org/downloads/) on to your computer/laptop 1. Install [Python 3.8](https://www.python.org/downloads/) or later to your computer/laptop
2. Install the required Python modules: motor, aiohttp, aioconsole, aiofiles, aiosqlite, beautifulsoup4, lxml, xmltodict, progress 2. Install the required Python modules (see [requirements.txt](requirements.txt): motor, aiohttp, aioconsole, aiofiles, aiosqlite, beautifulsoup4, lxml, xmltodict, progress
``` ```
# Remember to run the command with a Python interpreter >= 3.7 python -m pip install -r requirements.txt
python -m pip install motor aiohttp aioconsole aiofiles aiosqlite beautifulsoup4 lxml xmltodict progress
``` ```
3. Download all the files including the [maps.json](maps.json) and [tanks.json](tanks.json) files. **The recommened way** is to install the scipts via `git clone`: [GitHub Desktop](https://desktop.github.com/) is an excellent GUI for git. This makes it easy to receive the updates via `git pull`. 3. Clone the repository. [GitHub Desktop](https://desktop.github.com/) is an excellent GUI for `git`. This makes it easy to receive the updates via `git pull`.
4. Upload your replays.
``` ```
# Windows, run directly from Command Prompt or Powershell git clone https://github.com/Jylpah/blitz-tools.git
# Check the options ```
4. Upload your replays with `upload_wotb_replays.py`
```
## Check the options
# Windows Command Prompt or Powershell
python.exe .\upload_wotb_replays.py --help python.exe .\upload_wotb_replays.py --help
# Syntax # Linux or Windows Subsystem for Linux (WSL)
python.exe .\upload_wotb_replays.py REPLAY_FILE.wotbreplay ./upload_wotb_replays.py --help
# Upload replays (Windows)
python.exe .\upload_wotb_replays.py REPLAY_FILE.wotbreplay DIR_WITH_REPLAYS
# example # example
python.exe .\upload_wotb_replays.py 20200223_0122__jylpah_T-54_grossberg.wotbreplay python.exe .\upload_wotb_replays.py 20200223_0122__jylpah_T-54_grossberg.wotbreplay
# Linux
./upload_wotb_replays.py --help
``` ```
5. Analyze replays. 5. Analyze replays with `analyze_wotb_replays.py`
``` ```
# Windows, run directly from Command Prompt or Powershell ## Check options
# Windows Command Prompt or Powershell
python.exe .\analyze_wotb_replays.py --help python.exe .\analyze_wotb_replays.py --help
# Syntax # Syntax (Windows)
python.exe .\analyze_wotb_replays.py REPLAY_FILE.wotbreplay.json python.exe .\analyze_wotb_replays.py REPLAY_FILE.wotbreplay.json [DIR_WITH_REPLAYS ...]
# Example # Example (Windows)
python.exe .\analyze_wotb_replays.py 20200223_0122__jylpah_T-54_grossberg.wotbreplay.json python.exe .\analyze_wotb_replays.py 20200223_0122__jylpah_T-54_grossberg.wotbreplay.json
# Linux # Linux or Windows Subsystem for Linux (WSL)
./analyze_wotb_replays.py --help ./analyze_wotb_replays.py --help
``` ```
+239 -97
View File
@@ -29,6 +29,8 @@ 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_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:')
@@ -167,6 +169,7 @@ class BattleCategorizationList():
'Regular' : 1, 'Regular' : 1,
'Training Room' : 2, 'Training Room' : 2,
'Tournament' : 4, 'Tournament' : 4,
'Quick Tournament' : 5, # maybe?
'Rating' : 7, 'Rating' : 7,
'Mad Games' : 8, 'Mad Games' : 8,
'Realistic Battles' : 22, 'Realistic Battles' : 22,
@@ -184,6 +187,7 @@ class BattleCategorizationList():
'battle_type' : [ 'Battle Type', 'category', [ 'Encounter', 'Supremacy']], 'battle_type' : [ 'Battle Type', 'category', [ 'Encounter', 'Supremacy']],
'room_type' : [ 'Battle Mode', 'category', _BATTLE_MODES ], 'room_type' : [ 'Battle Mode', 'category', _BATTLE_MODES ],
'top_tier' : [ 'Tier', 'category', [ 'Bottom tier', 'Top tier']], 'top_tier' : [ 'Tier', 'category', [ 'Bottom tier', 'Top tier']],
'in_platoon' : [ 'Platoon', 'category', [ 'Solo', 'Platoon']],
'tank_tier' : [ 'Tank Tier', 'number' ], 'tank_tier' : [ 'Tank Tier', 'number' ],
'is_premium' : [ 'Tank Type', 'category', [ 'Tech tree', 'Premium']], 'is_premium' : [ 'Tank Type', 'category', [ 'Tech tree', 'Premium']],
'tank_type' : [ 'Tank Class', 'category', WG.TANK_TYPE_STR], 'tank_type' : [ 'Tank Class', 'category', WG.TANK_TYPE_STR],
@@ -203,7 +207,11 @@ class BattleCategorizationList():
'battle_duration' : [ 'Battle Duration', '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' ], '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_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_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 ], 'player_name' : [ 'Player', 'string', 25 ],
'protagonist' : [ 'account_id', 'number' ], 'protagonist' : [ 'account_id', 'number' ],
'tank_name' : [ 'Tank', 'string', 25], 'tank_name' : [ 'Tank', 'string', 25],
@@ -220,10 +228,41 @@ class BattleCategorizationList():
'top_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 @classmethod
def get_categorizations_all(cls): def get_categorizations_all(cls, cats: list = None) -> list:
return cls._categorizations.keys() 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 @classmethod
@@ -325,7 +364,6 @@ class BattleCategorizationList():
self.urls = collections.OrderedDict() self.urls = collections.OrderedDict()
self.url_title_max_len = 0 self.url_title_max_len = 0
cats = list(set(cats)) # remove duplicates cats = list(set(cats)) # remove duplicates
# ordering # ordering
self.categorizations_list = [ cat for cat in self._categorizations if cat in cats ] self.categorizations_list = [ cat for cat in self._categorizations if cat in cats ]
@@ -354,6 +392,15 @@ class BattleCategorizationList():
bu.error('BattleCategorizationList()', exception=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): def get_categorization(self, cat: str):
"""Return a BattleCategorization for a category""" """Return a BattleCategorization for a category"""
try: try:
@@ -362,6 +409,7 @@ class BattleCategorizationList():
bu.error(exception=err) bu.error(exception=err)
return None return None
def record_result(self, result: dict): def record_result(self, result: dict):
try: try:
for cat in self.categorizations_list: for cat in self.categorizations_list:
@@ -411,7 +459,7 @@ class BattleCategorizationList():
def get_filter_categories(self, cat: str, filters: list) -> dict: def get_filter_categories(self, cat: str, filters: list) -> dict:
try: try:
return self.categorizations[cat].get_filter_categories(filters) return self.categorizations[str(cat)].get_filter_categories(filters)
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
return None return None
@@ -457,6 +505,20 @@ class BattleCategorizationList():
return None 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(): class BattleCategorization():
RESULT_CAT_HEADER_LEN = 15 RESULT_CAT_HEADER_LEN = 15
@@ -491,10 +553,13 @@ class BattleCategorization():
def get_filter_categories(self, filters: list) -> list: def get_filter_categories(self, filters: list) -> list:
try:
if type(filters) != list: if type(filters) != list:
filters = [ filters ] filters = [ filters ]
return [ x for x in filters if x in self.get_categories() ] 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): def help(self):
"""Help""" """Help"""
@@ -713,7 +778,10 @@ class BattleStringCategorization(BattleCategorization):
def get_filter_categories(self, filters: list) -> list: def get_filter_categories(self, filters: list) -> list:
try: try:
return filters if type(filters) != list:
filters = [ filters ]
return [ x for x in filters if x in self.get_categories() ]
# return filters
except Exception as err: except Exception as err:
bu.error('Category: ' + str(self.category_key), exception=err) bu.error('Category: ' + str(self.category_key), exception=err)
return None return None
@@ -749,11 +817,14 @@ class BattleNumberCategorization(BattleCategorization):
def get_filter_categories(self, filters: list) -> list: def get_filter_categories(self, filters: list) -> list:
try: try:
if type(filters) != list:
filters = [ filters ]
return list(map(str,filters)) return list(map(str,filters))
except Exception as err: except Exception as err:
bu.error('Category: ' + str(self.category_key), exception=err) bu.error('Category: ' + str(self.category_key), exception=err)
return None return None
def help(self): def help(self):
super().help() super().help()
print('INTEGER') print('INTEGER')
@@ -894,12 +965,20 @@ class BattleCategory():
'battles%' : [ '% Battles', 'Share of Battles', 9, '{:9.0%}' ], 'battles%' : [ '% Battles', 'Share of Battles', 9, '{:9.0%}' ],
'win' : [ 'WR', 'Win rate', 6, '{:6.1%}' ], 'win' : [ 'WR', 'Win rate', 6, '{:6.1%}' ],
'damage_made' : [ 'DPB', 'Average Damage', 5, '{:5.0f}' ], '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}' ], 'enemies_destroyed' : [ 'KPB', 'Kills / Battle', 4, '{:4.2f}' ],
'DR' : [ 'DR', 'Damage Ratio', 5, '{:5.1f}' ], 'DR' : [ 'DR', 'Damage Ratio', 5, '{:5.1f}' ],
'KDR' : [ 'KDR', 'Kills / Death', 4, '{:4.1f}' ], 'KDR' : [ 'KDR', 'Kills / Death', 4, '{:4.1f}' ],
'enemies_spotted' : [ 'Spot', 'Enemies spotted per battle', 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%}' ], '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' : [ '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%}' ], 'survived' : [ 'Surv%', 'Survival rate', 6, '{:6.1%}' ],
'time_alive' : [ 'T alive', 'Time being alive in a battle in secs', 7, '{:7.0f}' ], '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%}' ], 'alive' : [ 'Share live', 'Percentage of time being alive in a battle', 8, '{:8.0%}' ],
@@ -912,9 +991,9 @@ class BattleCategory():
'player_battles' : [ 'Player Btls', 'Average number battles of the player', 11, '{:11.0f}' ], '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}' ], '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}' ], 'enemies_battles' : [ 'Enemies Btls', 'Average number battles of the enemies', 12, '{:12.0f}' ],
'player_damage_dealt' : [ 'Player Avg Dmg', 'Player Average damage', 11, '{:11.0f}' ], 'player_damage_dealt' : [ 'Player Avg Dmg', 'Player Average damage', 14, '{:14.0f}' ],
'allies_damage_dealt' : [ 'Allies Avg Dmg', 'Average damage of the allies', 11, '{:11.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', 12, '{:12.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%}'] MISSING_STATS : [ 'No stats', 'Players without stats avail', 8, '{:8.1%}']
} }
@@ -923,13 +1002,15 @@ class BattleCategory():
_result_fields_modes = { _result_fields_modes = {
'default' : [ 'battles', 'win','damage_made', 'enemies_destroyed', 'enemies_spotted', 'default' : [ 'battles', 'win','damage_made', 'enemies_destroyed', 'enemies_spotted',
'top_tier', 'DR', 'survived', 'allies_wins', 'enemies_wins'], 'top_tier', 'DR', 'survived', 'allies_wins', 'enemies_wins'],
'team' : [ 'battles', 'win','player_wins','allies_wins','enemies_wins'], 'team' : [ 'battles', 'win', 'in_platoon', 'player_wins', 'allies_wins','enemies_wins'],
'team_extended' : [ 'battles', 'battles%', 'win','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_damage_dealt', 'allies_damage_dealt', 'enemies_damage_dealt',
'player_battles','allies_battles','enemies_battles' ], 'player_battles','allies_battles','enemies_battles' ],
'extended' : [ 'battles', 'battles%', 'win','damage_made', 'enemies_destroyed', 'enemies_spotted', 'extended' : [ 'battles', 'battles%', 'win', 'in_platoon', 'damage_made', 'enemies_destroyed', 'enemies_spotted',
'top_tier', 'DR', 'KDR', 'hit_rate', 'pen_rate', 'survived', 'alive', 'top_tier', 'DR', 'KDR', 'hit_rate', 'pen_rate', 'survived', 'alive',
'player_wins', 'allies_wins', 'enemies_wins', MISSING_STATS ], '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() ] 'all' : [ cat for cat in _result_fields.keys() ]
} }
@@ -945,7 +1026,10 @@ class BattleCategory():
'KDR' : [ 'enemies_destroyed', 'destroyed' ], 'KDR' : [ 'enemies_destroyed', 'destroyed' ],
'DR' : [ 'damage_made', 'damage_received' ], 'DR' : [ 'damage_made', 'damage_received' ],
'hit_rate' : [ 'shots_hit', 'shots_made' ], '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' : [ 'shots_pen', 'shots_hit' ],
'pen_rate_received' : [ 'hits_pen', 'hits_received' ],
'dmg_block_rate' : [ 'damage_blocked', 'damage_received' ] 'dmg_block_rate' : [ 'damage_blocked', 'damage_received' ]
} }
@@ -1016,12 +1100,21 @@ class BattleCategory():
@classmethod @classmethod
def get_field_name(cls, field: str): def get_field_name(cls, field: str) -> str:
try: try:
return cls._result_fields[field][0] return cls._result_fields[field][0]
except Exception as err: except Exception as err:
bu.error(exception=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 @classmethod
def get_field_width(cls, field: str): def get_field_width(cls, field: str):
try: try:
@@ -1083,6 +1176,17 @@ class BattleCategory():
return None 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): def __init__(self):
try: try:
@@ -1316,7 +1420,7 @@ class ErrorCatchingArgumentParser(argparse.ArgumentParser):
## main() ------------------------------------------------------------- ## main() -------------------------------------------------------------
async def main(argv): async def main(argv):
global wg, wi, WG_APP_ID, OPT_MODE_DEFAULT global wg, wi, WG_APP_ID, OPT_MODE_DEFAULT, OPT_EXPORT_CSV_FILE, OPT_EXPORT_JSON_FILE
# set the directory for the script # set the directory for the script
current_dir = os.getcwd() current_dir = os.getcwd()
os.chdir(os.path.dirname(sys.argv[0])) os.chdir(os.path.dirname(sys.argv[0]))
@@ -1329,8 +1433,8 @@ async def main(argv):
OPT_HIST = False OPT_HIST = False
OPT_STAT_FUNC = StatFunc.get_default() OPT_STAT_FUNC = StatFunc.get_default()
OPT_WORKERS_N = 10 OPT_WORKERS_N = 10
OPT_EXPORT_CSV_FILE = 'export.csv' # OPT_EXPORT_CSV_FILE = 'export.csv'
OPT_EXPORT_JSON_FILE = 'export.json' # OPT_EXPORT_JSON_FILE = 'export.json'
#WG_ACCOUNT = None # format: nick@server, where server is either 'eu', 'ru', 'na', 'asia' or 'china'. #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 # China is not supported since WG API stats are not available there
@@ -1342,8 +1446,8 @@ async def main(argv):
## VERY unlikely you have a DB set up ## VERY unlikely you have a DB set up
DB_SERVER = 'localhost' DB_SERVER = 'localhost'
DB_PORT = 27017 DB_PORT = 27017
DB_SSL = False DB_TLS = False
DB_CERT_REQ = ssl.CERT_NONE DB_CERT_REQ = False
DB_AUTH = 'admin' DB_AUTH = 'admin'
DB_NAME = 'BlitzStats' DB_NAME = 'BlitzStats'
DB_USER = 'mongouser' DB_USER = 'mongouser'
@@ -1395,14 +1499,14 @@ async def main(argv):
OPT_DB = configDB.getboolean('opt_DB', OPT_DB) OPT_DB = configDB.getboolean('opt_DB', OPT_DB)
DB_SERVER = configDB.get('db_server', DB_SERVER) DB_SERVER = configDB.get('db_server', DB_SERVER)
DB_PORT = configDB.getint('db_port', DB_PORT) DB_PORT = configDB.getint('db_port', DB_PORT)
DB_SSL = configDB.getboolean('db_ssl', DB_SSL) DB_TLS = configDB.getboolean('db_tls', DB_TLS)
DB_CERT_REQ = configDB.getint('db_ssl_req', DB_CERT_REQ) DB_CERT_REQ = configDB.getboolean('db_tls_req', DB_CERT_REQ)
DB_AUTH = configDB.get('db_auth', DB_AUTH) DB_AUTH = configDB.get('db_auth', DB_AUTH)
DB_NAME = configDB.get('db_name', DB_NAME) DB_NAME = configDB.get('db_name', DB_NAME)
DB_USER = configDB.get('db_user', DB_USER) DB_USER = configDB.get('db_user', DB_USER)
DB_PASSWD = configDB.get('db_password', DB_PASSWD) DB_PASSWD = configDB.get('db_password', DB_PASSWD)
DB_CERT = configDB.get('db_ssl_cert_file', DB_CERT) DB_CERT = configDB.get('db_tls_cert_file', DB_CERT)
DB_CA = configDB.get('db_ssl_ca_file', DB_CA) DB_CA = configDB.get('db_tls_ca_file', DB_CA)
except (KeyError, configparser.NoSectionError) as err: except (KeyError, configparser.NoSectionError) as err:
bu.error(exception=err) bu.error(exception=err)
@@ -1424,7 +1528,7 @@ async def main(argv):
parser.add_argument('--replays', action='store_true', default=False, help='Export replay info in JSON 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('-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('--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,10] }, { "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: : [ {"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('--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('--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('-d', '--debug', action='store_true', default=False, help='Debug mode')
@@ -1448,7 +1552,7 @@ async def main(argv):
#### Connect to MongoDB. You are unlikely to have this set up... #### Connect to MongoDB. You are unlikely to have this set up...
bu.debug('DB_SERVER: ' + DB_SERVER) bu.debug('DB_SERVER: ' + DB_SERVER)
bu.debug('DB_PORT: ' + str(DB_PORT)) bu.debug('DB_PORT: ' + str(DB_PORT))
bu.debug('DB_SSL: ' + "True" if DB_SSL else "False") bu.debug('DB_TLS: ' + "True" if DB_TLS else "False")
bu.debug('DB_AUTH: ' + DB_AUTH) bu.debug('DB_AUTH: ' + DB_AUTH)
bu.debug('DB_NAME: ' + DB_NAME) bu.debug('DB_NAME: ' + DB_NAME)
@@ -1456,7 +1560,7 @@ async def main(argv):
db = None db = None
if args.db: if args.db:
try: try:
client = motor.motor_asyncio.AsyncIOMotorClient(DB_SERVER,DB_PORT, authSource=DB_AUTH, username=DB_USER, password=DB_PASSWD, ssl=DB_SSL, ssl_cert_reqs=DB_CERT_REQ, ssl_certfile=DB_CERT, tlsCAFile=DB_CA) 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] db = client[DB_NAME]
bu.debug('Database connection initiated') bu.debug('Database connection initiated')
except Exception as err: except Exception as err:
@@ -1486,7 +1590,7 @@ async def main(argv):
args.account_id = await wg.get_account_id(args.account) args.account_id = await wg.get_account_id(args.account)
bu.debug('WG account_id: ' + str(args.account_id)) bu.debug('WG account_id: ' + str(args.account_id))
try: # try:
BattleCategory.set_fields(args.mode) BattleCategory.set_fields(args.mode)
replayQ = asyncio.Queue(maxsize=1000) replayQ = asyncio.Queue(maxsize=1000)
reader_tasks = [] reader_tasks = []
@@ -1522,10 +1626,18 @@ async def main(argv):
if args.min != None: if args.min != None:
results = filter_min_replays_by_player(results, args.min) 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) players = get_players(results)
replays = filter_replays(replays, results)
(player_stats, stat_id_map) = await process_player_stats(players, OPT_WORKERS_N, args, db) (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))) bu.debug('Number of player stats: ' + str(len(player_stats)))
teamresults = calc_team_stats(results, player_stats, stat_id_map, args) teamresults = calc_team_stats(results, player_stats, stat_id_map, args)
@@ -1539,20 +1651,35 @@ async def main(argv):
print_player_dist(histograms) print_player_dist(histograms)
if args.json: if args.json:
res = blt_cat_list.get_results_json() await export_json(args, blt_cat_list, histograms)
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)
if args.csv: 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 != None:
await wg.close()
if wi != 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() rows = blt_cat_list.get_results_list()
if args.hist: if args.hist:
rows.append('') rows.append('')
@@ -1569,26 +1696,27 @@ async def main(argv):
csvwriter = csv.writer(csvfile) csvwriter = csv.writer(csvfile)
csvwriter.writerows(rows) csvwriter.writerows(rows)
bu.verbose_std('Results exported to: ' + export_file) bu.verbose_std('Results exported to: ' + export_file)
if args.replays:
await export_replays(replays)
bu.debug('Finished. Cleaning up..................')
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
except UserWarning as err:
bu.warning(str(err))
except asyncio.CancelledError: async def export_json(args : argparse.Namespace, blt_cat_list: BattleCategorizationList, histograms: dict = None):
pass """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: except Exception as err:
bu.error(exception=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
async def help_extended(db : motor.motor_asyncio.AsyncIOMotorDatabase = None, parser: argparse.ArgumentParser = None): async def help_extended(db : motor.motor_asyncio.AsyncIOMotorDatabase = None, parser: argparse.ArgumentParser = None):
@@ -1597,11 +1725,33 @@ async def help_extended(db : motor.motor_asyncio.AsyncIOMotorDatabase = None, pa
parser.print_help() parser.print_help()
# Result category options # Result category options
# Results fields
print('\n-------------------------------------------------------------------')
print('| Result fields |')
print('-------------------------------------------------------------------\n')
BattleCategory.help()
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 # Filter usage
print('\n-------------------------------------------------------------------') print('\n-------------------------------------------------------------------')
print('| DB Filter usage |') print('| DB Filter usage |')
print('-------------------------------------------------------------------') print('-------------------------------------------------------------------\n')
print('')
print('You are unlikely to have DB setup required for these filters. This is "Jylpah\'s special"') print('You are unlikely to have DB setup required for these filters. This is "Jylpah\'s special"')
print('') print('')
print("Syntax: --filters_db '{ \"replay.param\": VALUE, \"replay.param2\": { \"$MONGO_OPERATOR\" : VALUE }, ... }'") print("Syntax: --filters_db '{ \"replay.param\": VALUE, \"replay.param2\": { \"$MONGO_OPERATOR\" : VALUE }, ... }'")
@@ -1620,24 +1770,6 @@ async def help_extended(db : motor.motor_asyncio.AsyncIOMotorDatabase = None, pa
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
print('\n-------------------------------------------------------------------')
print('| Filters |')
print('-------------------------------------------------------------------')
print('')
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('-------------------------------------------------------------------')
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.')
async def export_replays(replays: dict): async def export_replays(replays: dict):
"""Export replays""" """Export replays"""
@@ -1740,15 +1872,15 @@ def print_player_dist(histograms: dict):
print('') print('')
hist.print() hist.print()
def process_battle_results(results: dict, args : argparse.Namespace): def process_battle_results(results: dict, args : argparse.Namespace):
"""Process replay battle results""" """Process replay battle results"""
try: try:
url = args.url url = args.url
cats = BattleCategorizationList.get_categorizations(args) cats = BattleCategorizationList.get_categorizations(args)
blt_cat_list= None
# remove if?
if args.filters != None:
results = filter_results(results, args.filters)
if len(results) > 0: if len(results) > 0:
blt_cat_list = BattleCategorizationList(cats) blt_cat_list = BattleCategorizationList(cats)
for result in results: for result in results:
@@ -1759,7 +1891,7 @@ def process_battle_results(results: dict, args : argparse.Namespace):
blt_cat_list.calc_results() blt_cat_list.calc_results()
else: else:
warning_txt = 'No replays to process ' warning_txt = 'No replays to process '
if args.filter != None: if args.filters != None:
warning_txt = warning_txt + 'after filtering' warning_txt = warning_txt + 'after filtering'
bu.warning(warning_txt) bu.warning(warning_txt)
@@ -1768,24 +1900,31 @@ def process_battle_results(results: dict, args : argparse.Namespace):
return blt_cat_list return blt_cat_list
def filter_results(results: list, filter_json : str) -> bool: def filter_results(results: list, filters_json : str, stats_filters=None) -> bool:
"""Filter replays based on battle category filters""" """Filter replays based on battle category filters"""
try: try:
filters = json.loads(filter_json) if filters_json == None:
return results
filters = json.loads(filters_json)
bu.debug(str(filters)) bu.debug(str(filters))
for filter in filters:
if type(filters[filter]) != list:
filters[filter] = [ filters[filter]]
if type(filters) != dict: if type(filters) != dict:
bu.error('invalid --filter arguments: ' + str(filters)) bu.error('invalid --filter arguments: ' + str(filter))
sys.exit(1) sys.exit(1)
filter_cats = BattleCategorizationList(filters.keys()) filter_cats = BattleCategorizationList.get_categorizations_stats(filters.keys(), stats_filters)
for cat in filters: filter_cats = BattleCategorizationList(filter_cats)
if filter_cats.len() == 0:
bu.debug('Nothing to filter')
return results
bu.verbose('Filters applied:')
for category in filter_cats:
cat = category.category_key
categorization = filter_cats.get_categorization(cat) categorization = filter_cats.get_categorization(cat)
bu.debug('Category: ' + cat + ' filters: ' + str(filters))
filters[cat] = categorization.get_filter_categories(filters[cat]) filters[cat] = categorization.get_filter_categories(filters[cat])
bu.verbose('Filter: ' + cat + ': ' + ','.join(filters[cat])) bu.verbose(' ' + cat + ' = ' + ','.join(filters[cat]))
bu.verbose('')
res = list() res = list()
for result in results: for result in results:
@@ -1802,8 +1941,12 @@ def filter_results(results: list, filter_json : str) -> bool:
res.append(result) res.append(result)
except Exception as err: except Exception as err:
bu.error(exception=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))) bu.verbose_std('Replays after filtering: ' + str(len(res)))
return res return res
except UserWarning:
raise
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
return None return None
@@ -2346,7 +2489,6 @@ async def mk_replayQ(queue : asyncio.Queue, args : argparse.Namespace, db : moto
Nreplays += 1 Nreplays += 1
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
bu.verbose('Finished scanning replays: ' + str(Nreplays) + ' replays to process')
return Nreplays return Nreplays
@@ -2357,7 +2499,7 @@ async def mk_readerQ_item(replay_json, filename : str = None, _id: str = None) -
try: try:
if wi.chk_JSON_replay(replay_json): if wi.chk_JSON_replay(replay_json):
if not '_id' in replay_json: if not '_id' in replay_json:
if _id != None: if _id == None:
_id = wi.read_replay_id(replay_json) _id = wi.read_replay_id(replay_json)
replay_json['_id'] = _id replay_json['_id'] = _id
else: else:
@@ -2483,20 +2625,20 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
player_tank_tier = wg.get_tank_data(player['vehicle_descr'], 'tier') player_tank_tier = wg.get_tank_data(player['vehicle_descr'], 'tier')
btl_tier = max(btl_tier, player_tank_tier) btl_tier = max(btl_tier, player_tank_tier)
if (protagonist != None) and (player['dbid'] == protagonist):
protagonist_tank = player['vehicle_descr']
if player['dbid'] == account_id: if player['dbid'] == account_id:
# player itself is not put in results['allies'] # player itself is not put in results['allies']
tmp = dict() tmp = dict()
tmp['account_id'] = account_id tmp['account_id'] = account_id
tmp['tank_id'] = player['vehicle_descr'] tank_id = player['vehicle_descr']
tmp['tank_id'] = tank_id
tmp['tank_tier'] = player_tank_tier tmp['tank_tier'] = player_tank_tier
tmp['tank_name'] = wg.get_tank_name(tmp['tank_id']) tmp['tank_name'] = wg.get_tank_name(tmp['tank_id'])
tmp['tank_type'] = wg.get_tank_type_id(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['tank_nation'] = wg.get_tank_nation_id(tmp['tank_id'])
tmp['is_premium'] = int(wg.is_premium(tmp['tank_id'])) tmp['is_premium'] = int(wg.is_premium(tmp['tank_id']))
tmp['squad_index'] = player['squad_index'] 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: for key in replay_details_flds:
tmp[key] = player[key] tmp[key] = player[key]
if player['hitpoints_left'] == 0: if player['hitpoints_left'] == 0:
@@ -2529,7 +2671,7 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
result['battle_duration'] = btl_duration result['battle_duration'] = btl_duration
## Rather use 'player' than incomprehensible 'protagonist'... ## Rather use 'player' than incomprehensible 'protagonist'...
result['player'] = get_stat_id(protagonist, protagonist_tank, result['battle_start_timestamp']) result['player'] = get_stat_id(account_id, tank_id, result['battle_start_timestamp'])
bu.debug('Player stats_id: ' + result['player']) bu.debug('Player stats_id: ' + result['player'])
# remove platoon buddy from stats # remove platoon buddy from stats
if result['squad_index'] != None: if result['squad_index'] != None:
+8
View File
@@ -643,6 +643,14 @@ def get_JSON_value(json, key : str = None, keys : list = None, keypath = None):
raise KeyError('Key not found: ' + get_JSON_keypath(keypath, keys[0])) 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: item[0]))
# ----------------------------------------------------------- # -----------------------------------------------------------
# Class SlowBar # Class SlowBar
# ----------------------------------------------------------- # -----------------------------------------------------------
+14 -8
View File
@@ -3,6 +3,7 @@
# Extract tankopedia data in WG API JSON format from Blitz app files (Android APK unzipped) # Extract tankopedia data in WG API JSON format from Blitz app files (Android APK unzipped)
from os import path
import sys, argparse, json, os, inspect, aiohttp, asyncio, aiofiles, re, logging, time, xmltodict, collections, configparser import sys, argparse, json, os, inspect, aiohttp, asyncio, aiofiles, re, logging, time, xmltodict, collections, configparser
import blitzutils as bu import blitzutils as bu
from blitzutils import WG from blitzutils import WG
@@ -11,9 +12,11 @@ logging.getLogger("asyncio").setLevel(logging.DEBUG)
FILE_CONFIG = 'blitzstats.ini' FILE_CONFIG = 'blitzstats.ini'
BLITZAPP_STRINGS='/assets/Data/Strings/en.yaml' # BLITZAPP_STRINGS='/assets/Data/Strings/en.yaml'
BLITZAPP_VEHICLES_DIR='/assets/Data/XML/item_defs/vehicles/' # BLITZAPP_VEHICLES_DIR='/assets/Data/XML/item_defs/vehicles/'
BLITZAPP_VEHICLE_FILE='/list.xml' BLITZAPP_STRINGS='Data/Strings/en.yaml'
BLITZAPP_VEHICLES_DIR='Data/XML/item_defs/vehicles/'
BLITZAPP_VEHICLE_FILE='list.xml'
wg = None wg = None
@@ -79,8 +82,8 @@ async def main(argv):
tankopedia = collections.OrderedDict() tankopedia = collections.OrderedDict()
tankopedia['status'] = 'ok' tankopedia['status'] = 'ok'
tankopedia['meta'] = { "count": len(tanks) } tankopedia['meta'] = { "count": len(tanks) }
tankopedia['data'] = tanks tankopedia['data'] = bu.sort_dict(tanks, number=True)
tankopedia['userStr'] = userStrs 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(tanks)) + ' tanks')
bu.verbose_std('New tankopedia \'' + args.tanks + '\' contains ' + str(len(userStrs)) + ' tank strings') 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)) await outfile.write(json.dumps(tankopedia, ensure_ascii=False, indent=4, sort_keys=False))
@@ -104,8 +107,11 @@ async def main(argv):
async def extract_tanks(blitz_app_base : str, nation: str): async def extract_tanks(blitz_app_base : str, nation: str):
tanks = [] tanks = []
list_xml_file = blitz_app_base + BLITZAPP_VEHICLES_DIR + nation + BLITZAPP_VEHICLE_FILE # WG has changed the location of Data directory - at least in steam client
if not os.path.isfile(list_xml_file): 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) print('ERROR: cannot open ' + list_xml_file)
return None return None
bu.debug('Opening file: ' + list_xml_file + ' (Nation: ' + nation + ')') bu.debug('Opening file: ' + list_xml_file + ' (Nation: ' + nation + ')')
@@ -132,7 +138,7 @@ async def read_user_strs(blitz_app_base : str) -> dict:
"""Read user strings to convert map and tank names""" """Read user strings to convert map and tank names"""
tank_strs = {} tank_strs = {}
map_strs = {} map_strs = {}
filename = blitz_app_base + BLITZAPP_STRINGS filename = path.join(blitz_app_base, BLITZAPP_STRINGS)
bu.debug('Opening file: ' + filename + ' for reading UserStrings') bu.debug('Opening file: ' + filename + ' for reading UserStrings')
try: try:
async with aiofiles.open(filename, 'r', encoding="utf8") as f: async with aiofiles.open(filename, 'r', encoding="utf8") as f:
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/local/bin/python3.7 #!/usr/bin/env python3
# Get Tankpedia from WoTinspector.com # Get Tankpedia from WoTinspector.com
+2
View File
@@ -18,6 +18,7 @@
"fort": "Fort Despair", "fort": "Fort Despair",
"grossberg": "Dynasty's Pearl", "grossberg": "Dynasty's Pearl",
"himmelsdorf": "Himmelsdorf", "himmelsdorf": "Himmelsdorf",
"holland": "Molendijk",
"holmeisk": "Wasteland", "holmeisk": "Wasteland",
"iceworld": "Everfrost", "iceworld": "Everfrost",
"idle": "Yukon", "idle": "Yukon",
@@ -43,6 +44,7 @@
"neptune_02": "Normandy - Beach", "neptune_02": "Normandy - Beach",
"north": "North", "north": "North",
"ordeal": "Trial by Fire", "ordeal": "Trial by Fire",
"plant": "Ghost Factory",
"pliego": "Castilla", "pliego": "Castilla",
"port": "Port Bay", "port": "Port Bay",
"rift": "Hellas", "rift": "Hellas",
Submodule
+1
Submodule pyutils added at 1eb17e8402.
+94 -81
View File
@@ -1,83 +1,96 @@
# WoT Blitz releases # WoT Blitz releases
| Release | Date | Notes | Inaccurate? | | Release | Date | Notes |
|:-------:|:--------:|-------------|-----| |:-------:|:--------:|-------------|
| 7.9 | 2021-04-27 | Mines rework, new tutorial, colorblind mode, JgPz E100 legendary camo | | | 9.1 | 2022-07-14 | Releases updated via DLC, graphics updates for Object 140, Patton 46 |
| 7.8 | 2021-03-24 | Mid-tier balance changes, Grille line new consumables | | | 9.0 | 2022-06-08 | Japanies heavies, Castilla graphics update, new garage |
| 7.7 | 2021-02-24 | Czechoslovakian medium tanks | | | 8.10 | 2022-04-28 | New battle results screen, new "Operation" mechanics |
| 7.6 | 2021-01-27 | AMC 13 90, TOG II buffs, free replay camera | | | 8.9 | 2022-03-23 | "Free" camo resupply, VK 72.01 supplies |
| 7.5 | 2020-12-09 | ATGM removal, Batchat, Kpz70, T-2020 buff | | | 8.8 | 2022-02-16 | Molendijk map |
| 7.4 | 2020-11-12 | TD buffs, Clan missions | | | 8.7 | 2022-01-19 | Soviet Light Tanks |
| 7.3 | 2020-10-08 | Swedish HT line | | | 8.6 | 2021-12-15 | Joining Light and Medium Tank classes in MM |
| 7.2 | 2020-08-19 | Balance changes, added tier X ammo capa, new aiming reticles | | | 8.5 | 2021-11-17 | New national crew voices, economy update |
| 7.1 | 2020-07-15 | New Yukon map, IS-6, KV-4, Patton 48 armor buff, Grille DPM buff, SP IC, AMZ 13 75, M41 Bulldog mobility buff | | | 8.4 | 2021-10-20 | Polish heavy tanks |
| 7.0 | 2020-06-17 | British LTs, graphics improvements, ATGM nerf, Sheridan nerf, map fixes | | | 8.3 | 2021-09-16 | FV4005, FV4022, VK 91.01 (P) and WZ-121 legendary camos, -5 deg gun depression to WZ-120 |
| 6.10 | 2020-04-29 | T92E1 nerf, Tier V-IX heavy HP buff, Battle Pass, map fixes | | | 8.2 | 2021-08-18 | Ghost Factory map, XP/credits nerf for inactive players, new store UI |
| 6.9 | 2020-03-25 | Japanese TDs, Tier X heavy HP buff | | | 8.1 | 2021-07-15 | Armor highlighting updated |
| 6.8 | 2020-02-19 | ATGM nerf, Hellas map | | | 8.0 | 2021-06-08 | American experimental heavies (Yoh-line), PBR camos |
| 6.7 | 2020-01-15 | ATGM US Lights | | | 7.9 | 2021-04-27 | Mines rework, new tutorial, colorblind mode, JgPz E100 legendary camo |
| 6.6 | 2019-12-18 | T-62A + Leo buffs | | | 7.8 | 2021-03-24 | Mid-tier balance changes, Grille line new consumables |
| 6.5 | 2019-11-21 | New UI | | | 7.7 | 2021-02-24 | Czechoslovakian medium tanks |
| 6.4 | 2019-10-16 | Halloween map, mid-tier HT buff... | | | 7.6 | 2021-01-27 | AMC 13 90, TOG II buffs, free replay camera |
| 6.3 | 2019-09-18 | European line | | | 7.5 | 2020-12-09 | ATGM removal, Batchat, Kpz70, T-2020 buff |
| 6.2 | 2019-08-14 | New MM | | | 7.4 | 2020-11-12 | TD buffs, Clan missions |
| 6.1 | 2019-07-10 | Autoloader buff | | | 7.3 | 2020-10-08 | Swedish HT line |
| 6.0 | 2019-06-05 | UK TD line #2 | | | 7.2 | 2020-08-19 | Balance changes, added tier X ammo capa, new aiming reticles |
| 5.10 | 2019-04-24 | new missions | | | 7.1 | 2020-07-15 | New Yukon map, IS-6, KV-4, Patton 48 armor buff, Grille DPM buff, SP IC, AMZ 13 75, M41 Bulldog mobility buff |
| 5.9 | 2019-03-27 | New shell tracers, minor fixes | | | 7.0 | 2020-06-17 | British LTs, graphics improvements, ATGM nerf, Sheridan nerf, map fixes |
| 5.8 | 2019-02-21 | Chinese TD line | | | 6.10 | 2020-04-29 | T92E1 nerf, Tier V-IX heavy HP buff, Battle Pass, map fixes |
| 5.7 | 2019-01-16 | Avatars | | | 6.9 | 2020-03-25 | Japanese TDs, Tier X heavy HP buff |
| 5.6 | 2018-12-05 | Normandy map | | | 6.8 | 2020-02-19 | ATGM nerf, Hellas map |
| 5.5 | 2018-11-13 | Low-tier clean-up | | | 6.7 | 2020-01-15 | ATGM US Lights |
| 5.4 | 2018-10-10 | MM "rigging" adjusted | | | 6.6 | 2019-12-18 | T-62A + Leo buffs |
| 5.3 | 2018-09-12 | MM "rigging" started, Newbie queue added | | | 6.5 | 2019-11-21 | New UI |
| 5.2 | 2018-08-22 | Faust map | | | 6.4 | 2019-10-16 | Halloween map, mid-tier HT buff... |
| 5.1 | 2018-07-31 | New MM: vehicle class matching | | | 6.3 | 2019-09-18 | European line |
| 5.0 | 2018-06-30 | Vineyeards map update, Spare Parts killed | | | 6.2 | 2019-08-14 | New MM |
| 4.10 | 2018-05-24 | Alpenstadt map | | | 6.1 | 2019-07-10 | Autoloader buff |
| 4.9 | 2018-04-25 | Chinese heavy tanks | | | 6.0 | 2019-06-05 | UK TD line #2 |
| 4.8 | 2018-03-28 | Choice of battle modes, crew skills accelerator | | | 5.10 | 2019-04-24 | new missions |
| 4.7 | 2018-02-28 | New minimap | | | 5.9 | 2019-03-27 | New shell tracers, minor fixes |
| 4.6 | 2018-01-31 | Chinese nation | | | 5.8 | 2019-02-21 | Chinese TD line |
| 4.5 | 2017-12-19 | Dynasty's Pearl map | | | 5.7 | 2019-01-16 | Avatars |
| 4.4 | 2017-11-09 | New British tanks | | | 5.6 | 2018-12-05 | Normandy map |
| 4.3 | 2017-10-10 | Level 7 screw skills | | | 5.5 | 2018-11-13 | Low-tier clean-up :-( |
| 4.2 | 2017-09-12 | French LTs, Naval Frontier map | | | 5.4 | 2018-10-10 | MM "rigging" adjusted |
| 4.1 | 2017-08-16 | New markers | | | 5.3 | 2018-09-12 | MM "rigging" started, Newbie queue added |
| 4.0 | 2017-07-13 | Rating battles | Y | | 5.2 | 2018-08-22 | Faust map |
| 3.10 | 2017-06-15 | Fixes | Y | | 5.1 | 2018-07-31 | New MM: vehicle class matching |
| 3.9 | 2017-05-19 | Military Honor, Mayan Ruins | Y | | 5.0 | 2018-06-30 | Vineyeards map update, Spare Parts removed ! |
| 3.8 | 2017-04-24 | Spare Parts, New 9-slot Equipment system | Y | | 4.10 | 2018-05-24 | Alpenstadt map |
| 3.7 | 2017-03-22 | Balance changes, Tournaments | Y | | 4.9 | 2018-04-25 | Chinese heavy tanks |
| 3.6 | 2017-02-16 | Vehicle Enrichment | Y | | 4.8 | 2018-03-28 | Choice of battle modes, crew skills accelerator |
| 3.5 | 2017-01-13 | Boosters, Dead rail update | Y | | 4.7 | 2018-02-28 | New minimap |
| 3.4 | 2016-11-28 | Canyon map, USA T57 line | | | 4.6 | 2018-01-31 | Chinese nation |
| 3.3 | 2016-11-01 | Updated Mines map | | | 4.5 | 2017-12-19 | Dynasty's Pearl map |
| 3.2 | 2016-10-04 | Copperfield update | | | 4.4 | 2017-11-09 | New British tanks |
| 3.1 | 2016-09-03 | Clan tournaments, Yamato Harbor map | Y | | 4.3 | 2017-10-10 | Level 7 screw skills |
| 3.0 | 2016-07-20 | Supremacy mode, Falls Creek update | Y | | 4.2 | 2017-09-12 | French LTs, Naval Frontier map |
| 2.11 | 2016-06-28 | Vineyeards map, Oasis Palms update, German paper TDs | | | 4.1 | 2017-08-16 | New markers |
| 2.10 | 2016-06-07 | Rockfield map update, Tank birthdays, high-pen gun for IS-6 | | | 4.0 | 2017-07-13 | Rating battles (date uncertain) |
| 2.9 | 2016-05-03 | Training rooms, +/-1 Matchmaker | Y | | 3.10 | 2017-06-15 | Fixes (date uncertain) |
| 2.8 | 2016-04-04 | New Russian TD line, new consumables system | Y | | 3.9 | 2017-05-19 | Military Honor, Mayan Ruins (date uncertain) |
| 2.7 | 2016-03-07 | Mirage map | Y | | 3.8 | 2017-04-24 | Spare Parts, New 9-slot Equipment system (date uncertain) |
| 2.6 | 2016-02-10 | German LTs, shooting through obstacles | Y | | 3.7 | 2017-03-22 | Balance changes, Tournaments (date uncertain) |
| 2.5 | 2016-01-11 | Camouflages, Clan supply | Y | | 3.6 | 2017-02-16 | Vehicle Enrichment (date uncertain) |
| 2.4 | 2015-12-09 | Additional Japanese tanks, windows version | Y | | 3.5 | 2017-01-13 | Boosters, Dead rail update (date uncertain) |
| 2.3 | 2015-11-12 | Canal map | Y | | 3.4 | 2016-11-28 | Canyon map, USA T57 line |
| 2.2 | 2015-10-05 | Desert Sands map update | Y | | 3.3 | 2016-11-01 | Updated Mines map |
| 2.1 | 2015-09-07 | UK TDs, Clan Chat | Y | | 3.2 | 2016-10-04 | Copperfield update |
| 2.0 | 2015-08-05 | Clans, Oasis Palms updated | | | 3.1 | 2016-09-03 | Clan tournaments, Yamato Harbor map (date uncertain) |
| 1.11 | 2015-07-13 | KV-2, KV-13, Sherman Jumbo | Y | | 3.0 | 2016-07-20 | Supremacy mode, Falls Creek update (date uncertain) |
| 1.10 | 2015-06-11 | Port Bay map | Y | | 2.11 | 2016-06-28 | Vineyeards map, Oasis Palms update, German paper TDs |
| 1.9 | 2015-05-13 | New German medium tanks (Leo) | Y | | 2.10 | 2016-06-07 | Rockfield map update, Tank birthdays, high-pen gun for IS-6 |
| 1.8 | 2015-04-14 | Game tutorial, Bot map for new players | Y | | 2.9 | 2016-05-03 | Training rooms, +/-1 Matchmaker (date uncertain) |
| 1.7 | 2015-02-16 | US Turreted TDs | Y | | 2.8 | 2016-04-04 | New Russian TD line, new consumables system (date uncertain) |
| 1.6 | 2015-01-19 | Castilla map, UK HT line | Y | | 2.7 | 2016-03-07 | Mirage map (date uncertain) |
| 1.5 | 2014-12-01 | UK Nation | Y | | 2.6 | 2016-02-10 | German LTs, shooting through obstacles (date uncertain) |
| 1.4 | 2014-10-29 | German HT line (Maus) | Y | | 2.5 | 2016-01-11 | Camouflages, Clan supply (date uncertain) |
| 1.3 | 2014-10-02 | USSR IS-4 line, Lost Temple, Black Goldville maps | Y | | 2.4 | 2015-12-09 | Additional Japanese tanks, windows version (date uncertain) |
| 1.2 | 2014-08-20 | Oasis Palms map | Y | | 2.3 | 2015-11-12 | Canal map (date uncertain) |
| 1.1.1 | 2014-07-23 | Fixes :-) | | | 2.2 | 2015-10-05 | Desert Sands map update (date uncertain) |
| 1.0 | 2014-06-26 | Worldwide launch | | | 2.1 | 2015-09-07 | UK TDs, Clan Chat (date uncertain) |
| 0.9 | 2014-05-05 | Nordic soft launch | | | 2.0 | 2015-08-05 | Clans, Oasis Palms updated |
| 1.11 | 2015-07-13 | KV-2, KV-13, Sherman Jumbo (date uncertain) |
| 1.10 | 2015-06-11 | Port Bay map (date uncertain) |
| 1.9 | 2015-05-13 | New German medium tanks (Leo) (date uncertain) |
| 1.8 | 2015-04-14 | Game tutorial, Bot map for new players (date uncertain) |
| 1.7 | 2015-02-16 | US Turreted TDs (date uncertain) |
| 1.6 | 2015-01-19 | Castilla map, UK HT line (date uncertain) |
| 1.5 | 2014-12-01 | UK Nation (date uncertain) |
| 1.4 | 2014-10-29 | German HT line (Maus) (date uncertain) |
| 1.3 | 2014-10-02 | USSR IS-4 line, Lost Temple, Black Goldville maps (date uncertain) |
| 1.2 | 2014-08-20 | Oasis Palms map (date uncertain) |
| 1.1.1 | 2014-07-23 | Fixes :-) |
| 1.0 | 2014-06-26 | Worldwide launch |
| 0.9 | 2014-05-05 | Nordic soft launch |
+1107 -104
View File
File diff suppressed because it is too large. Load diff
+7 -7
View File
@@ -195,7 +195,7 @@ async def replayWorker(queue: asyncio.Queue, workerID: int, account_id: int, pri
bu.verbose_std(msg_str + title + ' has already been posted. Skipping.' ) bu.verbose_std(msg_str + title + ' has already been posted. Skipping.' )
else: else:
os.remove(replay_json_fn) os.remove(replay_json_fn)
bu.debug(msg_str + "Replay JSON not valid: Deleting " + replay_json_fn, id=workerID) bu.debug(msg_str + "Replay JSON not valid/complete: Deleting " + replay_json_fn, id=workerID)
SKIPPED_N += 1 SKIPPED_N += 1
queue.task_done() queue.task_done()
continue continue
@@ -216,13 +216,13 @@ async def replayWorker(queue: asyncio.Queue, workerID: int, account_id: int, pri
if not bu.debug(msg_str + 'Replay saved OK: ' + filename): if not bu.debug(msg_str + 'Replay saved OK: ' + filename):
bu.verbose_std(msg_str + title + ' posted') bu.verbose_std(msg_str + title + ' posted')
else: else:
bu.warning(msg_str +'Replay file is not valid: ' + filename) bu.warning(msg_str +'Replay file is not valid/complete: ' + filename)
ERROR_N += 1 ERROR_N += 1
else: else:
bu.error(msg_str + 'Error saving replay: ' + filename) bu.error(msg_str + 'Error saving replay: ' + filename)
ERROR_N += 1 ERROR_N += 1
else: else:
bu.error(msg_str + 'Replay file is not valid: ' + filename) bu.error(msg_str + 'Replay file is not valid/complete: ' + filename)
ERROR_N += 1 ERROR_N += 1
except Exception as err: except Exception as err:
bu.error(msg_str + 'Unexpected Exception: ' + str(type(err)) + ' : ' + str(err) ) bu.error(msg_str + 'Unexpected Exception: ' + str(type(err)) + ' : ' + str(err) )
@@ -287,13 +287,13 @@ def getTitle(replayfile: str, title: str = None, i : int = 0) -> str:
try: try:
metadata_json = json.load(meta) metadata_json = json.load(meta)
#player = metadata_json['playerName'] #player = metadata_json['playerName']
tank = wg.tank_str2name(metadata_json['playerVehicleName']) tank_name = wg.get_tank_name(metadata_json['vehicleCompDescriptor'])
map_name = wg.get_map(metadata_json['mapName']) map_name = wg.get_map(metadata_json['mapName'])
bu.debug('Tank: ' + tank + ' Map: ' + map_name) bu.debug('Tank: ' + tank_name if tank_name != None else "NOT FOUND IN tanks.json" + ' Map: ' + map_name)
except Exception as err: except Exception as err:
bu.error(exception = err) bu.error(exception = err)
if (tank != None) and (map_name != None): if (tank_name != None) and (map_name != None):
title = tank + ' @ ' + map_name title = tank_name + ' @ ' + map_name
else: else:
title = re.sub('\\.wotbreplay$', '', filename) title = re.sub('\\.wotbreplay$', '', filename)
except Exception as err: except Exception as err: