Author SHA1 Message Date
Jylpah 858a38092d read_replay_JSON(): Start of same-tier support 2021-03-09 10:59:40 +02:00
12 changed files with 10936 additions and 6361 deletions

No files matched your search

-16
View File
@@ -1,16 +0,0 @@
__pycache__/
BlitzApp
tmp/
ssl/
*.ini
*.swp
*.code-workspace
*.sqlite3
.vscode/
*.sqlite3-journal
.gitattributes
export*
*.csv
!/stats/*.csv
!*.py
/.venv
+23 -29
View File
@@ -1,9 +1,5 @@
## Unmaintained
Please use [blitz-replays](https://github.com/Jylpah/blitz-replays) instead. It is faster and more feature-rich.
# 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 later. 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.
### 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.
@@ -17,48 +13,46 @@ 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/) or later to your computer/laptop 1. Install [Python 3.8](https://www.python.org/downloads/) on to your computer/laptop
2. Install the required Python modules (see [requirements.txt](requirements.txt): motor, aiohttp, aioconsole, aiofiles, aiosqlite, beautifulsoup4, lxml, xmltodict, progress 2. Install the required Python modules: motor, aiohttp, aioconsole, aiofiles, aiosqlite, beautifulsoup4, lxml, xmltodict, progress
``` ```
python -m pip install -r requirements.txt # Remember to run the command with a Python interpreter >= 3.7
python -m pip install motor aiohttp aioconsole aiofiles aiosqlite beautifulsoup4 lxml xmltodict progress
``` ```
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`. 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`.
4. Upload your replays.
``` ```
git clone https://github.com/Jylpah/blitz-tools.git # Windows, run directly from Command Prompt or Powershell
``` # 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
# Linux or Windows Subsystem for Linux (WSL) # Syntax
./upload_wotb_replays.py --help python.exe .\upload_wotb_replays.py REPLAY_FILE.wotbreplay
# 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 with `analyze_wotb_replays.py` 5. Analyze replays.
``` ```
## Check options # Windows, run directly from Command Prompt or Powershell
# Windows Command Prompt or Powershell
python.exe .\analyze_wotb_replays.py --help python.exe .\analyze_wotb_replays.py --help
# Syntax (Windows) # Syntax
python.exe .\analyze_wotb_replays.py REPLAY_FILE.wotbreplay.json [DIR_WITH_REPLAYS ...] python.exe .\analyze_wotb_replays.py REPLAY_FILE.wotbreplay.json
# Example (Windows) # Example
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 or Windows Subsystem for Linux (WSL) # Linux
./analyze_wotb_replays.py --help ./analyze_wotb_replays.py --help
``` ```
+2315 -3078
View File
File diff suppressed because it is too large. Load diff
Executable
+2027
View File
File diff suppressed because it is too large. Load diff
-2715
View File
File diff suppressed because it is too large. Load diff
+223
View File
@@ -0,0 +1,223 @@
#!/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)
import sys, argparse, json, os, inspect, aiohttp, asyncio, aiofiles, re, logging, time, xmltodict, collections, configparser
import blitzutils as bu
from blitzutils import WG
logging.getLogger("asyncio").setLevel(logging.DEBUG)
FILE_CONFIG = 'blitzstats.ini'
BLITZAPP_STRINGS='/assets/Data/Strings/en.yaml'
BLITZAPP_VEHICLES_DIR='/assets/Data/XML/item_defs/vehicles/'
BLITZAPP_VEHICLE_FILE='/list.xml'
wg = None
## main() -------------------------------------------------------------
async def main(argv):
global wg
# set the directory for the script
os.chdir(os.path.dirname(sys.argv[0]))
## Read config
BLITZAPP_FOLDER = '.'
try:
if os.path.isfile(FILE_CONFIG):
config = configparser.ConfigParser()
config.read(FILE_CONFIG)
configOptions = config['EXTRACT_TANKOPEDIA']
BLITZAPP_FOLDER = configOptions.get('blitz_app_dir', BLITZAPP_FOLDER)
except:
pass
parser = argparse.ArgumentParser(description='Extract Tankopedia data from Blitz game files')
parser.add_argument('blitz_app_base', type=str, nargs='?', metavar="BLITZAPP_FOLDER", default=BLITZAPP_FOLDER, help='Base dir of the Blitz App files')
parser.add_argument('tanks', type=str, default='tanks.json', nargs='?', metavar="TANKS_FILE", help='File to write Tankopedia')
parser.add_argument('maps', type=str, default='maps.json', nargs='?', metavar='MAPS_FILE', help='File to write map names')
arggroup = parser.add_mutually_exclusive_group()
arggroup.add_argument('-d', '--debug', action='store_true', default=False, help='Debug mode')
arggroup.add_argument('-v', '--verbose', action='store_true', default=False, help='Verbose mode')
arggroup.add_argument('-s', '--silent', action='store_true', default=False, help='Silent mode')
args = parser.parse_args()
bu.set_log_level(args.silent, args.verbose, args.debug)
wg = WG()
tasks = []
for nation in wg.NATION:
tasks.append(asyncio.create_task(extract_tanks(args.blitz_app_base, nation)))
tanklist = []
for tanklist_tmp in await asyncio.gather(*tasks):
tanklist.extend(tanklist_tmp)
tank_strs, map_strs = await read_user_strs(args.blitz_app_base)
json_data = None
userStrs = {}
tanks = {}
if os.path.exists(args.tanks):
try:
async with aiofiles.open(args.tanks) as infile:
json_data = json.loads(await infile.read())
userStrs = json_data['userStr']
tanks = json_data['data']
except Exception as err:
bu.error('Unexpected error when reading file: ' + args.tanks + ' : ' + str(err))
async with aiofiles.open(args.tanks, 'w', encoding="utf8") as outfile:
new_tanks, new_userStrs = await convert_tank_names(tanklist, tank_strs)
# merge old and new tankopedia
tanks.update(new_tanks)
userStrs.update(new_userStrs)
tankopedia = collections.OrderedDict()
tankopedia['status'] = 'ok'
tankopedia['meta'] = { "count": len(tanks) }
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:
maps = {}
if os.path.exists(args.maps):
try:
async with aiofiles.open(args.maps) as infile:
maps = json.loads(await infile.read())
except Exception as err:
bu.error('Unexpected error when reading file: ' + args.maps + ' : ' + str(err))
# merge old and new map data
maps.update(map_strs)
async with aiofiles.open(args.maps, 'w', encoding="utf8") as outfile:
bu.verbose_std('New maps file \'' + args.maps + '\' contains ' + str(len(maps)) + ' maps')
await outfile.write(json.dumps(maps, ensure_ascii=False, indent=4, sort_keys=True))
return None
async def extract_tanks(blitz_app_base : str, nation: str):
tanks = []
list_xml_file = blitz_app_base + BLITZAPP_VEHICLES_DIR + nation + BLITZAPP_VEHICLE_FILE
if not os.path.isfile(list_xml_file):
print('ERROR: cannot open ' + list_xml_file)
return None
bu.debug('Opening file: ' + list_xml_file + ' (Nation: ' + nation + ')')
async with aiofiles.open(list_xml_file, 'r', encoding="utf8") as f:
try:
tankList = xmltodict.parse(await f.read())
for data in tankList['root'].keys():
tank_xml = tankList['root'][data]
tank = dict()
tank['tank_id'] = await get_tank_id(nation, int(tank_xml['id']))
tank['userStr'] = tank_xml['userString']
tank['nation'] = nation
tank['tier'] = int(tank_xml['level'])
#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)
sys.exit(2)
return tanks
async def read_user_strs(blitz_app_base : str) -> dict:
"""Read user strings to convert map and tank names"""
tank_strs = {}
map_strs = {}
filename = blitz_app_base + BLITZAPP_STRINGS
bu.debug('Opening file: ' + filename + ' for reading UserStrings')
try:
async with aiofiles.open(filename, 'r', encoding="utf8") as f:
p_tank = re.compile('^"(#\\w+?_vehicles:.+?)": "(.+)"$')
p_map = re.compile('^"#maps:(.+?):.+?: "(.+?)"$')
async for l in f:
m = p_tank.match(l)
if m != None:
tank_strs[m.group(1)] = m.group(2)
m = p_map.match(l)
if m != None and m.group(2) != 'Macragge':
map_strs[m.group(1)] = m.group(2)
except Exception as err:
bu.error(err)
sys.exit(1)
return tank_strs, map_strs
async def convert_tank_names(tanklist : list, tank_strs: dict) -> dict:
"""Convert tank names for Tankopedia"""
tankopedia = {}
userStrs = {}
try:
for tank in tanklist:
tank['name'] = tank_strs[tank['userStr']]
#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):
tankopedia_sorted[str(tank_id)] = tankopedia[str(tank_id)]
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)
sys.exit(1)
return tankopedia_sorted, userStrs_sorted
async def get_tank_id(nation: str, tank_id : int) -> int:
return (tank_id << 8) + (wg.NATION_ID[nation] << 4) + 1
async def get_tank_type(tagstr : str):
tags = tagstr.split(' ')
for t_type in wg.TANK_TYPE:
if tags[0] == t_type:
return t_type
return None
### main()
if __name__ == "__main__":
# To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp
if sys.platform.startswith("win") and sys.version_info >= (3, 8):
try:
from asyncio import WindowsSelectorEventLoopPolicy
except ImportError:
pass
else:
if not isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy):
asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
#asyncio.run(main(sys.argv[1:]), debug=True)
asyncio.run(main(sys.argv[1:]))
+91
View File
@@ -0,0 +1,91 @@
#!/usr/local/bin/python3.7
# Get Tankpedia from WoTinspector.com
import sys
import argparse
import json
import os
import inspect
import aiohttp
import asyncio
import re
import logging
from pathlib import Path
import blitzutils as bu
from blitzutils import WG
from blitzutils import WoTinspector
logging.getLogger("asyncio").setLevel(logging.DEBUG)
VERBOSE = False
DEBUG = False
tank_type = ['lightTank', 'mediumTank', 'heavyTank', 'AT-SPG']
# @asyncio.coroutine
async def get_tankopedia(filename: str):
url = "https://wotinspector.com/static/armorinspector/tank_db_blitz.js"
async with aiohttp.ClientSession() as client:
async with client.get(url) as r:
if r.status == 200:
WI_tank_db = await r.text()
WI_tank_db = WI_tank_db.split("\n")
else:
print('Error: Could not get valid HTTPS response. HTTP: ' + str(r.status) )
sys.exit(1)
tanks = {}
n = 0
p = re.compile('\\s*(\\d+):\\s{"en":"([^"]+)",.*?"tier":(\\d+), "type":(\\d), "premium":(\\d).*')
for line in WI_tank_db[1:-1]:
m = p.match(line)
tank = {}
tank['tank_id'] = int(m.group(1))
tank['name'] = m.group(2)
tank['tier'] = int(m.group(3))
tank['type'] = tank_type[int(m.group(4))]
tank['is_premium'] = (int(m.group(5)) == 1)
tanks[str(m.group(1))] = tank
n += 1
tankopedia = {}
tankopedia['status'] = "ok"
tankopedia['meta'] = {"count" : n}
tankopedia['data'] = tanks
print("Tankopedia has " + str(n) + " tanks in: " + filename)
with open(filename,'w') as outfile:
outfile.write(json.dumps(tankopedia, ensure_ascii=False, indent=4, sort_keys=True))
return None
async def main(argv):
# set the directory for the script
os.chdir(os.path.dirname(sys.argv[0]))
parser = argparse.ArgumentParser(description='Retrieve Tankopedia from WoTinspector.com')
parser.add_argument('--file', dest="outfile", help='Write Tankopedia to file')
parser.add_argument('-d', '--debug', action='store_true', default=False, help='Debug mode')
parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Verbose mode')
parser.add_argument('-s', '--silent', action='store_true', default=False, help='Silent mode')
args = parser.parse_args()
bu.set_log_level(args.silent, args.verbose, args.debug)
wi = WoTinspector()
await wi.get_tankopedia(args.outfile)
await wi.close()
# main()
if __name__ == "__main__":
# To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp
if sys.platform.startswith("win") and sys.version_info >= (3, 8):
try:
from asyncio import WindowsSelectorEventLoopPolicy
except ImportError:
pass
else:
if not isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy):
asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
asyncio.run(main(sys.argv[1:]), debug=False)
+55 -1
View File
@@ -1 +1,55 @@
{"Random": {"key": "Random", "name": "Random map", "id": -1, "mode": 1}, "amigosville": {"key": "amigosville", "name": "Falls Creek", "id": -1, "mode": 1}, "amigosville_02": {"key": "amigosville_02", "name": "Falls Creek - Bridge", "id": -1, "mode": 3}, "amigosville_03": {"key": "amigosville_03", "name": "Falls Creek - Factory", "id": -1, "mode": 3}, "asia": {"key": "asia", "name": "Lost Temple", "id": -1, "mode": 1}, "canal": {"key": "canal", "name": "Canal", "id": -1, "mode": 1}, "canal_01": {"key": "canal_01", "name": "Canal - Factory", "id": -1, "mode": 3}, "canyon": {"key": "canyon", "name": "Canyon", "id": -1, "mode": 1}, "desert_train": {"key": "desert_train", "name": "Desert Sands", "id": -1, "mode": 1}, "desert_train_02": {"key": "desert_train_02", "name": "Desert Sands - Town", "id": -1, "mode": 3}, "desert_train_03": {"key": "desert_train_03", "name": "Desert Sands - Dunes", "id": -1, "mode": 3}, "erlenberg": {"key": "erlenberg", "name": "Middleburg", "id": -1, "mode": 1}, "erlenberg_01": {"key": "erlenberg_01", "name": "Middleburg - Town", "id": -1, "mode": 3}, "erlenberg_02": {"key": "erlenberg_02", "name": "Middleburg - Hill", "id": -1, "mode": 3}, "faust": {"key": "faust", "name": "Faust", "id": -1, "mode": 1}, "forgecity": {"key": "forgecity", "name": "New Bay", "id": -1, "mode": 1}, "fort": {"key": "fort", "name": "Fort Despair", "id": -1, "mode": 1}, "grossberg": {"key": "grossberg", "name": "Dynasty's Pearl", "id": -1, "mode": 1}, "himmelsdorf": {"key": "himmelsdorf", "name": "Himmelsdorf", "id": -1, "mode": 1}, "holland": {"key": "holland", "name": "Molendijk", "id": -1, "mode": 1}, "holmeisk": {"key": "holmeisk", "name": "Wasteland", "id": -1, "mode": 1}, "iceworld": {"key": "iceworld", "name": "Everfrost", "id": -1, "mode": 2}, "idle": {"key": "idle", "name": "Yukon", "id": -1, "mode": 1}, "italy": {"key": "italy", "name": "Vineyards", "id": -1, "mode": 1}, "karelia": {"key": "karelia", "name": "Rockfield", "id": -1, "mode": 1}, "karieri": {"key": "karieri", "name": "Copperfield", "id": -1, "mode": 1}, "lake": {"key": "lake", "name": "Mirage", "id": -1, "mode": 1}, "lumber": {"key": "lumber", "name": "Horrorstadt", "id": -1, "mode": 1}, "lumber_01": {"key": "lumber_01", "name": "Alpenstadt - Town", "id": -1, "mode": 3}, "malinovka": {"key": "malinovka", "name": "Winter Malinovka", "id": -1, "mode": 1}, "medvedkovo": {"key": "medvedkovo", "name": "Dead Rail", "id": -1, "mode": 1}, "medvedkovo_02": {"key": "medvedkovo_02", "name": "Dead Rail - Valley", "id": -1, "mode": 3}, "medvedkovo_03": {"key": "medvedkovo_03", "name": "Dead Rail - Railroad", "id": -1, "mode": 3}, "milbase": {"key": "milbase", "name": "Yamato Harbor", "id": -1, "mode": 1}, "milbase_02": {"key": "milbase_02", "name": "Yamato Harbor - Battleship", "id": -1, "mode": 3}, "milbase_03": {"key": "milbase_03", "name": "Yamato Harbor - Hills", "id": -1, "mode": 3}, "milbase_04": {"key": "milbase_04", "name": "Yamato Harbor - Center", "id": -1, "mode": 3}, "milbase_05": {"key": "milbase_05", "name": "Yamato Harbor - Dock", "id": -1, "mode": 3}, "moon": {"key": "moon", "name": "Sea of Tranquility", "id": -1, "mode": 2}, "mountain": {"key": "mountain", "name": "Black Goldville", "id": -1, "mode": 1}, "neptune": {"key": "neptune", "name": "Normandy", "id": -1, "mode": 1}, "neptune_01": {"key": "neptune_01", "name": "Normandy - Hills", "id": -1, "mode": 3}, "neptune_02": {"key": "neptune_02", "name": "Normandy - Beach", "id": -1, "mode": 3}, "north": {"key": "north", "name": "North", "id": -1, "mode": 1}, "ordeal": {"key": "ordeal", "name": "Trial by Fire", "id": -1, "mode": 1}, "plant": {"key": "plant", "name": "Ghost Factory", "id": -1, "mode": 1}, "pliego": {"key": "pliego", "name": "Castilla", "id": -1, "mode": 1}, "port": {"key": "port", "name": "Port Bay", "id": -1, "mode": 1}, "rift": {"key": "rift", "name": "Hellas", "id": -1, "mode": 1}, "rock": {"key": "rock", "name": "Mayan Ruins", "id": -1, "mode": 1}, "rudniki": {"key": "rudniki", "name": "Mines", "id": -1, "mode": 1}, "rudniki_01": {"key": "rudniki_01", "name": "Mines - Hill", "id": -1, "mode": 3}, "rudniki_02": {"key": "rudniki_02", "name": "Mines - Village", "id": -1, "mode": 3}, "rudniki_03": {"key": "rudniki_03", "name": "Mines - Center", "id": -1, "mode": 3}, "savanna": {"key": "savanna", "name": "Oasis Palms", "id": -1, "mode": 1}, "skit": {"key": "skit", "name": "Naval Frontier", "id": -1, "mode": 1}, "test": {"key": "test", "name": "World of Ducks", "id": -1, "mode": 2}, "tutorial": {"key": "tutorial", "name": "Proving Grounds", "id": -1, "mode": 0}} {
"Random": "Random map",
"amigosville": "Falls Creek",
"amigosville_02": "Falls Creek - Bridge",
"amigosville_03": "Falls Creek - Factory",
"asia": "Lost Temple",
"canal": "Canal",
"canal_01": "Canal - Factory",
"canyon": "Canyon",
"desert_train": "Desert Sands",
"desert_train_02": "Desert Sands - Town",
"desert_train_03": "Desert Sands - Dunes",
"erlenberg": "Middleburg",
"erlenberg_01": "Middleburg - Town",
"erlenberg_02": "Middleburg - Hill",
"faust": "Faust",
"forgecity": "New Bay",
"fort": "Fort Despair",
"grossberg": "Dynasty's Pearl",
"himmelsdorf": "Himmelsdorf",
"holmeisk": "Wasteland",
"iceworld": "Everfrost",
"idle": "Yukon",
"italy": "Vineyards",
"karelia": "Rockfield",
"karieri": "Copperfield",
"lake": "Mirage",
"lumber": "Alpenstadt",
"lumber_01": "Alpenstadt - Town",
"malinovka": "Winter Malinovka",
"medvedkovo": "Dead Rail",
"medvedkovo_02": "Dead Rail - Valley",
"medvedkovo_03": "Dead Rail - Railroad",
"milbase": "Yamato Harbor",
"milbase_02": "Yamato Harbor - Battleship",
"milbase_03": "Yamato Harbor - Hills",
"milbase_04": "Yamato Harbor - Center",
"milbase_05": "Yamato Harbor - Dock",
"moon": "Sea of Tranquility",
"mountain": "Black Goldville",
"neptune": "Normandy",
"neptune_01": "Normandy - Hills",
"neptune_02": "Normandy - Beach",
"north": "North",
"ordeal": "Trial by Fire",
"pliego": "Castilla",
"port": "Port Bay",
"rift": "Hellas",
"rock": "Mayan Ruins",
"rudniki": "Mines",
"savanna": "Oasis Palms",
"skit": "Naval Frontier",
"test": "World of Ducks",
"tutorial": "Proving Grounds"
}
+79 -106
View File
@@ -1,108 +1,81 @@
# WoT Blitz releases # WoT Blitz releases
| Release | Date | Content updates | | Release | Date | Notes | Inaccurate? |
|:-------:|:--------:|-------------| |:-------:|:--------:|-------------|-----|
| 10.4 | 2023-11-15 | Threesome-platoons for fun modes, camos | | 7.7 | 2021-02-24 | Czechoslovakian medium tanks | |
| 10.3 | 2023-09-27 | Polish Medium Tanks, Tier X rebalance, map updates: Ghost Factory, Desert Sands, Super-cool legenday camos| | 7.6 | 2021-01-27 | AMC 13 90, TOG II buffs, free replay camera | |
| 10.2 | 2023-08-16 | PBR Camos and bug fixes ... | | 7.5 | 2020-12-09 | ATGM removal, Batchat, Kpz70, T-2020 buff | |
| 10.1 | 2023-07-05 | PBR Camos and ? | | 7.4 | 2020-11-12 | TD buffs, Clan missions | |
| 10.0 | 2023-05-24 | Lagoon map, Czechoslovakian heavies, tanks start shells loaded | | 7.3 | 2020-10-08 | Swedish HT line | |
| 9.8 | 2023-03-22 | F\*cked up The Quick Commands Wheel, camos | | 7.2 | 2020-08-19 | Balance changes, added tier X ammo capa, new aiming reticles | |
| 9.7 | 2023-02-15 | Chinese light tanks | | 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 | |
| 9.6 | 2023-01-11 | Finally fixing game sounds for players with Genelec studio monitors, more gold camos | | 7.0 | 2020-06-17 | British LTs, graphics improvements, ATGM nerf, Sheridan nerf, map fixes | |
| 9.5 | 2022-12-14 | Camos and ?| | 6.10 | 2020-04-29 | T92E1 nerf, Tier V-IX heavy HP buff, Battle Pass, map fixes | |
| 9.4 | 2022-11-09 | Italian TD line | | 6.9 | 2020-03-25 | Japanese TDs, Tier X heavy HP buff | |
| 9.3 | 2022-10-04 | Nothing, except legandary camos and utterly incomprehensible shell icons | | 6.8 | 2020-02-19 | ATGM nerf, Hellas map | |
| 9.2 | 2022-08-17 | New shot sounds and shell hit marks, vehicle restoration from the game client | | 6.7 | 2020-01-15 | ATGM US Lights | |
| 9.1 | 2022-07-14 | Releases updated via DLC, graphics updates for Object 140, Patton 46 | | 6.6 | 2019-12-18 | T-62A + Leo buffs | |
| 9.0 | 2022-06-08 | Japanies heavies, Castilla graphics update, new garage | | 6.5 | 2019-11-21 | New UI | |
| 8.10 | 2022-04-28 | New battle results screen, new "Operation" mechanics | | 6.4 | 2019-10-16 | Halloween map, mid-tier HT buff... | |
| 8.9 | 2022-03-23 | "Free" camo resupply, VK 72.01 supplies | | 6.3 | 2019-09-18 | European line | |
| 8.8 | 2022-02-16 | Molendijk map | | 6.2 | 2019-08-14 | New MM | |
| 8.7 | 2022-01-19 | Soviet Light Tanks | | 6.1 | 2019-07-10 | Autoloader buff | |
| 8.6 | 2021-12-15 | Joining Light and Medium Tank classes in MM | | 6.0 | 2019-06-05 | UK TD line #2 | |
| 8.5 | 2021-11-17 | New national crew voices, economy update | | 5.10 | 2019-04-24 | new missions | |
| 8.4 | 2021-10-20 | Polish heavy tanks | | 5.9 | 2019-03-27 | New shell tracers, minor fixes | |
| 8.3 | 2021-09-16 | FV4005, FV4022, VK 91.01 (P) and WZ-121 legendary camos, -5 deg gun depression to WZ-120 | | 5.8 | 2019-02-21 | Chinese TD line | |
| 8.2 | 2021-08-18 | Ghost Factory map, XP/credits nerf for inactive players, new store UI | | 5.7 | 2019-01-16 | Avatars | |
| 8.1 | 2021-07-15 | Armor highlighting updated | | 5.6 | 2018-12-05 | Normandy map | |
| 8.0 | 2021-06-08 | American experimental heavies (Yoh-line), PBR camos | | 5.5 | 2018-11-13 | Low-tier clean-up | |
| 7.9 | 2021-04-27 | Mines rework, new tutorial, colorblind mode, JgPz E100 legendary camo | | 5.4 | 2018-10-10 | MM "rigging" adjusted | |
| 7.8 | 2021-03-24 | Mid-tier balance changes, Grille line new consumables | | 5.3 | 2018-09-12 | MM "rigging" started, Newbie queue added | |
| 7.7 | 2021-02-24 | Czechoslovakian medium tanks | | 5.2 | 2018-08-22 | Faust map | |
| 7.6 | 2021-01-27 | AMC 13 90, TOG II buffs, free replay camera | | 5.1 | 2018-07-31 | New MM: vehicle class matching | |
| 7.5 | 2020-12-09 | ATGM removal, Batchat, Kpz70, T-2020 buff | | 5.0 | 2018-06-30 | Vineyeards map update, Spare Parts killed | |
| 7.4 | 2020-11-12 | TD buffs, Clan missions | | 4.10 | 2018-05-24 | Alpenstadt map | |
| 7.3 | 2020-10-08 | Swedish HT line | | 4.9 | 2018-04-25 | Chinese heavy tanks | |
| 7.2 | 2020-08-19 | Balance changes, added tier X ammo capa, new aiming reticles | | 4.8 | 2018-03-28 | Choice of battle modes, crew skills accelerator | |
| 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 | | 4.7 | 2018-02-28 | New minimap | |
| 7.0 | 2020-06-17 | British LTs, graphics improvements, ATGM nerf, Sheridan nerf, map fixes | | 4.6 | 2018-01-31 | Chinese nation | |
| 6.10 | 2020-04-29 | T92E1 nerf, Tier V-IX heavy HP buff, Battle Pass, map fixes | | 4.5 | 2017-12-19 | Dynasty's Pearl map | |
| 6.9 | 2020-03-25 | Japanese TDs, Tier X heavy HP buff | | 4.4 | 2017-11-09 | New British tanks | |
| 6.8 | 2020-02-19 | ATGM nerf, Hellas map | | 4.3 | 2017-10-10 | Level 7 screw skills | |
| 6.7 | 2020-01-15 | ATGM US Lights | | 4.2 | 2017-09-12 | French LTs, Naval Frontier map | |
| 6.6 | 2019-12-18 | T-62A + Leo buffs | | 4.1 | 2017-08-16 | New markers | |
| 6.5 | 2019-11-21 | New UI | | 4.0 | 2017-07-13 | Rating battles | Y |
| 6.4 | 2019-10-16 | Halloween map, mid-tier HT buff... | | 3.10 | 2017-06-15 | Fixes | Y |
| 6.3 | 2019-09-18 | European line | | 3.9 | 2017-05-19 | Military Honor, Mayan Ruins | Y |
| 6.2 | 2019-08-14 | New MM | | 3.8 | 2017-04-24 | Spare Parts, New 9-slot Equipment system | Y |
| 6.1 | 2019-07-10 | Autoloader buff | | 3.7 | 2017-03-22 | Balance changes, Tournaments | Y |
| 6.0 | 2019-06-05 | UK TD line #2 | | 3.6 | 2017-02-16 | Vehicle Enrichment | Y |
| 5.10 | 2019-04-24 | new missions | | 3.5 | 2017-01-13 | Boosters, Dead rail update | Y |
| 5.9 | 2019-03-27 | New shell tracers, minor fixes | | 3.4 | 2016-11-28 | Canyon map, USA T57 line | |
| 5.8 | 2019-02-21 | Chinese TD line | | 3.3 | 2016-11-01 | Updated Mines map | |
| 5.7 | 2019-01-16 | Avatars | | 3.2 | 2016-10-04 | Copperfield update | |
| 5.6 | 2018-12-05 | Normandy map | | 3.1 | 2016-09-03 | Clan tournaments, Yamato Harbor map | Y |
| 5.5 | 2018-11-13 | Low-tier clean-up :-( | | 3.0 | 2016-07-20 | Supremacy mode, Falls Creek update | Y |
| 5.4 | 2018-10-10 | MM "rigging" adjusted | | 2.11 | 2016-06-28 | Vineyeards map, Oasis Palms update, German paper TDs | |
| 5.3 | 2018-09-12 | MM "rigging" started, Newbie queue added | | 2.10 | 2016-06-07 | Rockfield map update, Tank birthdays, high-pen gun for IS-6 | |
| 5.2 | 2018-08-22 | Faust map | | 2.9 | 2016-05-03 | Training rooms, +/-1 Matchmaker | Y |
| 5.1 | 2018-07-31 | New MM: vehicle class matching | | 2.8 | 2016-04-04 | New Russian TD line, new consumables system | Y |
| 5.0 | 2018-06-30 | Vineyeards map update, Spare Parts removed ! | | 2.7 | 2016-03-07 | Mirage map | Y |
| 4.10 | 2018-05-24 | Replays, Alpenstadt map | | 2.6 | 2016-02-10 | German LTs, shooting through obstacles | Y |
| 4.9 | 2018-04-25 | Chinese heavy tanks | | 2.5 | 2016-01-11 | Camouflages, Clan supply | Y |
| 4.8 | 2018-03-28 | Choice of battle modes, crew skills accelerator | | 2.4 | 2015-12-09 | Additional Japanese tanks, windows version | Y |
| 4.7 | 2018-02-28 | New minimap | | 2.3 | 2015-11-12 | Canal map | Y |
| 4.6 | 2018-01-31 | Chinese nation | | 2.2 | 2015-10-05 | Desert Sands map update | Y |
| 4.5 | 2017-12-19 | Dynasty's Pearl map | | 2.1 | 2015-09-07 | UK TDs, Clan Chat | Y |
| 4.4 | 2017-11-09 | New British tanks | | 2.0 | 2015-08-05 | Clans, Oasis Palms updated | |
| 4.3 | 2017-10-10 | Level 7 screw skills | | 1.11 | 2015-07-13 | KV-2, KV-13, Sherman Jumbo | Y |
| 4.2 | 2017-09-12 | French LTs, Naval Frontier map | | 1.10 | 2015-06-11 | Port Bay map | Y |
| 4.1 | 2017-08-16 | New markers | | 1.9 | 2015-05-13 | New German medium tanks (Leo) | Y |
| 4.0 | 2017-07-13 | Rating battles (date uncertain) | | 1.8 | 2015-04-14 | Game tutorial, Bot map for new players | Y |
| 3.10 | 2017-06-15 | Fixes (date uncertain) | | 1.7 | 2015-02-16 | US Turreted TDs | Y |
| 3.9 | 2017-05-19 | Military Honor, Mayan Ruins (date uncertain) | | 1.6 | 2015-01-19 | Castilla map, UK HT line | Y |
| 3.8 | 2017-04-24 | Spare Parts, New 9-slot Equipment system (date uncertain) | | 1.5 | 2014-12-01 | UK Nation | Y |
| 3.7 | 2017-03-22 | Balance changes, Tournaments (date uncertain) | | 1.4 | 2014-10-29 | German HT line (Maus) | Y |
| 3.6 | 2017-02-16 | Vehicle Enrichment (date uncertain) | | 1.3 | 2014-10-02 | USSR IS-4 line, Lost Temple, Black Goldville maps | Y |
| 3.5 | 2017-01-13 | Boosters, Dead rail update (date uncertain) | | 1.2 | 2014-08-20 | Oasis Palms map | Y |
| 3.4 | 2016-11-28 | Canyon map, USA T57 line | | 1.1.1 | 2014-07-23 | Fixes :-) | |
| 3.3 | 2016-11-01 | Updated Mines map | | 1.0 | 2014-06-26 | Worldwide launch | |
| 3.2 | 2016-10-04 | Copperfield update | | 0.9 | 2014-05-05 | Nordic soft launch | |
| 3.1 | 2016-09-03 | Clan tournaments, Yamato Harbor map (date uncertain) |
| 3.0 | 2016-07-20 | Supremacy mode, Falls Creek update (date uncertain) |
| 2.11 | 2016-06-28 | Vineyeards map, Oasis Palms update, German paper TDs |
| 2.10 | 2016-06-07 | Rockfield map update, Tank birthdays, high-pen gun for IS-6 |
| 2.9 | 2016-05-03 | Training rooms, +/-1 Matchmaker (date uncertain) |
| 2.8 | 2016-04-04 | New Russian TD line, new consumables system (date uncertain) |
| 2.7 | 2016-03-07 | Mirage map (date uncertain) |
| 2.6 | 2016-02-10 | German LTs, shooting through obstacles (date uncertain) |
| 2.5 | 2016-01-11 | Camouflages, Clan supply (date uncertain) |
| 2.4 | 2015-12-09 | Additional Japanese tanks, windows version (date uncertain) |
| 2.3 | 2015-11-12 | Canal map (date uncertain) |
| 2.2 | 2015-10-05 | Desert Sands map update (date uncertain) |
| 2.1 | 2015-09-07 | UK TDs, Clan Chat (date uncertain) |
| 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 |
+1 -2
View File
@@ -7,5 +7,4 @@ beautifulsoup4
lxml lxml
xmltodict xmltodict
progress progress
pytest-asyncio pytest-asyncio
blitz-utils @ git+https://github.com/Jylpah/blitz-utils
+5811 -1
View File
File diff suppressed because it is too large. Load diff
+311 -413
View File
@@ -1,445 +1,343 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
## PYTHON VERSION MUST BE 3.7 OR HIGHER ## PYTHON VERSION MUST BE 3.7 OR HIGHER
import json, argparse, sys, os, io, asyncio, aiofiles, aioconsole import json, argparse, inspect, sys, os, io, base64, aiohttp, urllib, asyncio, aiofiles, aioconsole
import logging, re, configparser, zipfile import logging, re, concurrent.futures, configparser, motor.motor_asyncio, ssl, zipfile
import blitzutils1 as bu import blitzutils as bu
from blitzutils1 import WG from blitzutils import WG
from blitzutils1 import WoTinspector from blitzutils import WoTinspector
logging.getLogger("asyncio").setLevel(logging.DEBUG) logging.getLogger("asyncio").setLevel(logging.DEBUG)
FILE_CONFIG = "blitzstats.ini" FILE_CONFIG = 'blitzstats.ini'
# DB_C_REPLAYS = 'Replays' # DB_C_REPLAYS = 'Replays'
DEBUG = False DEBUG = False
VERBOSE = False VERBOSE = False
SLEEP = 2 SLEEP = 2
MAX_RETRIES = 3 MAX_RETRIES = 3
REPLAY_N = 0 REPLAY_N = 0
SKIPPED_N = 0 SKIPPED_N = 0
ERROR_N = 0 ERROR_N = 0
WIurl = "https://wotinspector.com/api/replay/upload?" WIurl='https://wotinspector.com/api/replay/upload?'
WG_appID = "81381d3f45fa4aa75b78a7198eb216ad" WG_appID = '81381d3f45fa4aa75b78a7198eb216ad'
wg: WG | None = None wg = None
wi: WoTinspector | None = None wi = None
async def main(argv):
global wg, wi
current_dir = os.getcwd()
# set the directory for the script
os.chdir(os.path.dirname(sys.argv[0]))
# options defaults
OPT_WORKERS_N = 5
WG_ID = None
WG_ACCOUNT = None # format: nick@server, where server is either 'eu', 'ru', 'na', 'asia' or 'china'.
# China is not supported since WG API stats are not available there
WG_RATE_LIMIT = 10
try:
## Read config
if os.path.isfile(FILE_CONFIG):
bu.debug('Reading config file: ' + FILE_CONFIG)
config = configparser.ConfigParser()
config.read(FILE_CONFIG)
try:
if 'UPLOADER' in config.sections():
configUploader = config['UPLOADER']
OPT_WORKERS_N = configUploader.getint('opt_workers', OPT_WORKERS_N)
except (KeyError, configparser.NoSectionError) as err:
bu.error(exception=err)
try:
if 'WG' in config.sections():
configWG = config['WG']
# WG account id of the uploader:
# # Find it here: https://developers.wargaming.net/reference/all/wotb/account/list/
WG_ID = configWG.getint('wg_id', WG_ID)
WG_ACCOUNT = configWG.get('wg_account', WG_ACCOUNT)
## WG API Rules limit 10 request / sec. Higher rate of requests will return errors ==> extra delay
WG_RATE_LIMIT = configWG.getint('wg_rate_limit', WG_RATE_LIMIT)
except (KeyError, configparser.NoSectionError) as err:
bu.error(exception=err)
except Exception as err:
bu.error(exception=err)
parser = argparse.ArgumentParser(description='Post replays(s) to WoTinspector.com and retrieve replay data as JSON')
parser.add_argument('-id', dest='accountID', type=int, default=WG_ID, help='WG account_id')
parser.add_argument('-a', '--account', dest='account', type=str, default=WG_ACCOUNT, help='Uploader\'s WG account name. Format: ACCOUNT_NAME@SERVER')
parser.add_argument('-t','--title', type=str, default=None, help='Title for replays. Use "NN" for continous numbering. Default is automatic naming')
parser.add_argument('-p', '--private', dest="private", action='store_true', default=False, help='Set replays private on WoTinspector.com')
parser.add_argument('--tankopedia', type=str, default='tanks.json', help='JSON file to read Tankopedia from. Default: "tanks.json"')
parser.add_argument('--mapfile', type=str, default='maps.json', help='JSON file to read Blitz map names from. Default: "maps.json"')
parser.add_argument('-d', '--debug', action='store_true', default=False, help='Debug mode')
parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Verbose mode')
parser.add_argument('-s', '--silent', action='store_true', default=False, help='Silent mode')
parser.add_argument('files', metavar='FILE1 [FILE2 ...]', type=str, nargs='+', help='Files to read. Use \'-\' for STDIN"')
args = parser.parse_args()
bu.set_verbose(args.verbose)
bu.set_log_level(args.silent, args.verbose, args.debug)
wg = WG(WG_appID, args.tankopedia, args.mapfile)
wi = WoTinspector(rate_limit=WG_RATE_LIMIT)
if args.account != None:
args.accountID = await wg.get_account_id(args.account)
bu.debug('WG account_id: ' + str(args.accountID))
if args.accountID == None:
args.accountID = 0
try:
queue = asyncio.Queue()
# rebase file arguments due to moving the working directory to the script location
args.files = bu.rebase_file_args(current_dir, args.files)
tasks = []
# Make replay Queue
tasks.append(asyncio.create_task(mkReplayQ(queue, args.files, args.title)))
# Start tasks to process the Queue
for i in range(OPT_WORKERS_N):
tasks.append(asyncio.create_task(replayWorker(queue, i, args.accountID, args.private)))
bu.debug('Task ' + str(i) + ' started')
bu.debug('Waiting for the replay scanner to finish')
await asyncio.wait([tasks[0]])
bu.debug('Scanner finished. Waiting for workers to finish queue')
await queue.join()
bu.debug('Cancelling workers')
for task in tasks:
task.cancel()
bu.debug('Waiting for workers to cancel')
await asyncio.gather(*tasks, return_exceptions=True)
bu.verbose_std(str(REPLAY_N) + ' replays: ' + str(REPLAY_N - SKIPPED_N - ERROR_N) + ' uploaded, ' + str(SKIPPED_N) + ' skipped, ' + str(ERROR_N) + ' errors')
except KeyboardInterrupt:
print('Ctrl-c pressed ...')
sys.exit(1)
finally:
## Need to close the aiohttp.session since Python destructors do not support async methods...
await wg.session.close()
await wi.close()
return None
async def main(argv) -> int: async def mkReplayQ(queue : asyncio.Queue, files : list, title : str):
global wg, wi """Create queue of replays to post"""
current_dir = os.getcwd() p_replayfile = re.compile('.*\\.wotbreplay$')
# set the directory for the script if files[0] == '-':
os.chdir(os.path.dirname(sys.argv[0])) bu.debug('reading replay file list from STDIN')
stdin, _ = await aioconsole.get_standard_streams()
# options defaults while True:
OPT_WORKERS_N = 2 line = (await stdin.readline()).decode('utf-8').rstrip()
WG_ID = None if not line:
WG_ACCOUNT = None # format: nick@server, where server is either 'eu', 'ru', 'na', 'asia' or 'china'. break
# China is not supported since WG API stats are not available there else:
WG_RATE_LIMIT = 10 if (p_replayfile.match(line) != None):
await queue.put(await mkQueueItem(line, title))
try: else:
## Read config for fn in files:
if os.path.isfile(FILE_CONFIG): if fn.endswith('"'):
bu.debug("Reading config file: " + FILE_CONFIG) fn = fn[:-1]
config = configparser.ConfigParser() if os.path.isfile(fn) and (p_replayfile.match(fn) != None):
config.read(FILE_CONFIG) await queue.put(await mkQueueItem(fn, title))
bu.debug('File added to queue: ' + fn)
try: elif os.path.isdir(fn):
if "WG" in config.sections(): with os.scandir(fn) as dirEntry:
configWG = config["WG"] for entry in dirEntry:
# WG account id of the uploader: try:
# # Find it here: https://developers.wargaming.net/reference/all/wotb/account/list/ bu.debug('Found: ' + entry.name)
WG_ID = configWG.getint("wg_id", WG_ID) if entry.is_file() and (p_replayfile.match(entry.name) != None):
WG_ACCOUNT = configWG.get("wg_account", WG_ACCOUNT) bu.debug(entry.name)
## WG API Rules limit 10 request / sec. Higher rate of requests will return errors ==> extra delay await queue.put(await mkQueueItem(entry.path, title))
WG_RATE_LIMIT = configWG.getint("wg_rate_limit", WG_RATE_LIMIT) bu.debug('File added to queue: ' + entry.path)
except (KeyError, configparser.NoSectionError) as err: except Exception as err:
bu.error(exception=err) bu.error(str(err))
else:
except Exception as err: bu.error('File not found: ' + fn)
bu.error(exception=err)
bu.debug('Finished')
parser = argparse.ArgumentParser( return None
description="Post replays(s) to WoTinspector.com and retrieve replay data as JSON"
)
parser.add_argument(
"-id", dest="accountID", type=int, default=WG_ID, help="WG account_id"
)
parser.add_argument(
"-a",
"--account",
dest="account",
type=str,
default=WG_ACCOUNT,
help="Uploader's WG account name. Format: ACCOUNT_NAME@SERVER",
)
parser.add_argument(
"-t",
"--title",
type=str,
default=None,
help='Title for replays. Use "NN" for continous numbering. Default is automatic naming',
)
parser.add_argument(
"-p",
"--private",
dest="private",
action="store_true",
default=False,
help="Set replays private on WoTinspector.com",
)
parser.add_argument(
"--tankopedia",
type=str,
default="tanks.json",
help='JSON file to read Tankopedia from. Default: "tanks.json"',
)
parser.add_argument(
"--mapfile",
type=str,
default="maps.json",
help='JSON file to read Blitz map names from. Default: "maps.json"',
)
parser.add_argument(
"-d", "--debug", action="store_true", default=False, help="Debug mode"
)
parser.add_argument(
"-v", "--verbose", action="store_true", default=False, help="Verbose mode"
)
parser.add_argument(
"-s", "--silent", action="store_true", default=False, help="Silent mode"
)
parser.add_argument(
"files",
metavar="FILE1 [FILE2 ...]",
type=str,
nargs="+",
help="Files to read. Use '-' for STDIN\"",
)
args = parser.parse_args()
bu.set_verbose(args.verbose)
bu.set_log_level(args.silent, args.verbose, args.debug)
wg = WG(WG_appID, args.tankopedia, args.mapfile)
wi = WoTinspector()
if args.account is not None:
args.accountID = await wg.get_account_id(args.account)
bu.debug("WG account_id: " + str(args.accountID))
if args.accountID is None:
args.accountID = 0
try:
queue: asyncio.Queue = asyncio.Queue()
# rebase file arguments due to moving the working directory to the script location
bu.verbose_std("replays.wotinspector.com rate limits replays to 6 / minute")
args.files = bu.rebase_file_args(current_dir, args.files)
tasks = []
# Make replay Queue
tasks.append(asyncio.create_task(mkReplayQ(queue, args.files, args.title)))
# Start tasks to process the Queue
for i in range(OPT_WORKERS_N):
tasks.append(
asyncio.create_task(
replayWorker(queue, i, args.accountID, args.private)
)
)
bu.debug("Task " + str(i) + " started")
bu.debug("Waiting for the replay scanner to finish")
await asyncio.wait([tasks[0]])
bu.debug("Scanner finished. Waiting for workers to finish queue")
await queue.join()
bu.debug("Cancelling workers")
for task in tasks:
task.cancel()
bu.debug("Waiting for workers to cancel")
await asyncio.gather(*tasks, return_exceptions=True)
bu.verbose_std(
str(REPLAY_N)
+ " replays: "
+ str(REPLAY_N - SKIPPED_N - ERROR_N)
+ " uploaded, "
+ str(SKIPPED_N)
+ " skipped, "
+ str(ERROR_N)
+ " errors"
)
except KeyboardInterrupt:
print("Ctrl-c pressed ...")
sys.exit(1)
finally:
## Need to close the aiohttp.session since Python destructors do not support async methods...
await wg.session.close()
await wi.close()
return 0
async def mkReplayQ(queue: asyncio.Queue, files: list, title: str): async def mkQueueItem(filename : str, title : str) -> list:
"""Create queue of replays to post""" """Make an item to replay queue"""
p_replayfile = re.compile(".*\\.wotbreplay$") global REPLAY_N
if files[0] == "-": REPLAY_N +=1
bu.debug("reading replay file list from STDIN") return [filename, REPLAY_N, getTitle(filename, title, REPLAY_N)]
stdin, _ = await aioconsole.get_standard_streams()
while True:
line = (await stdin.readline()).decode("utf-8").rstrip()
if not line:
break
else:
if p_replayfile.match(line) is not None:
await queue.put(await mkQueueItem(line, title))
else:
for fn in files:
if fn.endswith('"'):
fn = fn[:-1]
if os.path.isfile(fn) and (p_replayfile.match(fn) is not None):
await queue.put(await mkQueueItem(fn, title))
bu.debug("File added to queue: " + fn)
elif os.path.isdir(fn):
with os.scandir(fn) as dirEntry:
for entry in dirEntry:
try:
bu.debug("Found: " + entry.name)
if entry.is_file() and (
p_replayfile.match(entry.name) is not None
):
bu.debug(entry.name)
await queue.put(await mkQueueItem(entry.path, title))
bu.debug("File added to queue: " + entry.path)
except Exception as err:
bu.error(str(err))
else:
bu.error("File not found: " + fn)
bu.debug("Finished")
return None
async def mkQueueItem(filename: str, title: str) -> list: async def replayWorker(queue: asyncio.Queue, workerID: int, account_id: int, priv = False):
"""Make an item to replay queue""" """Async Worker to process the replay queue"""
global REPLAY_N global SKIPPED_N
REPLAY_N += 1 global ERROR_N
return [filename, REPLAY_N, getTitle(filename, title, REPLAY_N)]
while True:
item = await queue.get()
filename = item[0]
N = item[1]
title = item[2]
replay_json_fn = filename + '.json'
msg_str = 'Replay[' + str(N) + ']: '
#bu.debug(msg_str + replay_json_fn)
try:
if os.path.isfile(replay_json_fn):
async with aiofiles.open(replay_json_fn) as fp:
replay_json = json.loads(await fp.read())
#if replay_json['status'] == 'ok':
if wi.chk_JSON_replay(replay_json):
bu.verbose_std(msg_str + title + ' has already been posted. Skipping.' )
else:
os.remove(replay_json_fn)
bu.debug(msg_str + "Replay JSON not valid: Deleting " + replay_json_fn, id=workerID)
SKIPPED_N += 1
queue.task_done()
continue
except asyncio.CancelledError as err:
raise err
except Exception as err:
bu.error(msg_str + 'Unexpected error: ' + str(type(err)) + ' : '+ str(err))
try:
#bu.debug('Opening file [' + str(N) +']: ' + filename)
async with aiofiles.open(filename,'rb') as fp:
filename = os.path.basename(filename)
bu.debug(msg_str + 'File: ' + filename)
json_resp = await wi.post_replay(await fp.read(), filename, account_id, title, priv, N)
if json_resp != None:
if (await bu.save_JSON(replay_json_fn,json_resp)):
if wi.chk_JSON_replay(json_resp):
if not bu.debug(msg_str + 'Replay saved OK: ' + filename):
bu.verbose_std(msg_str + title + ' posted')
else:
bu.warning(msg_str +'Replay file is not valid: ' + filename)
ERROR_N += 1
else:
bu.error(msg_str + 'Error saving replay: ' + filename)
ERROR_N += 1
else:
bu.error(msg_str + 'Replay file is not valid: ' + filename)
ERROR_N += 1
except Exception as err:
bu.error(msg_str + 'Unexpected Exception: ' + str(type(err)) + ' : ' + str(err) )
bu.debug(msg_str + 'Marking task done')
queue.task_done()
return None
async def replayWorker( def getTitle_old(replayfile: str, title: str, i : int) -> str:
queue: asyncio.Queue, workerID: int, account_id: int, priv=False global wg
):
"""Async Worker to process the replay queue"""
global SKIPPED_N
global ERROR_N
global wi
assert wi is not None, "wi cannot be None" if title == None:
try:
while True: filename = os.path.basename(replayfile)
item = await queue.get() bu.debug(filename)
filename = item[0] map_usrStrs = wg.get_map_user_strs()
N = item[1] tank_userStrs = wg.get_tank_user_strs()
title = item[2]
#p = re.compile('\\d{8}_\\d{4}_(.+)_(' + '|'.join(map_usrStrs) + ')(?:-\\d)?\\.wotbreplay$')
replay_json_fn = filename + ".json" # update 6.2 changed the file name format. Bug fixed 2019-09-09 Jylpah
msg_str = "Replay[" + str(N) + "]: " p = re.compile('\\d{8}_\\d{4}_.*?(' + '|'.join(tank_userStrs) + ')_(' + '|'.join(map_usrStrs) + ')(?:-\\d)?\\.wotbreplay$')
# bu.debug(msg_str + replay_json_fn) m = p.match(filename)
try: if (m != None):
if os.path.isfile(replay_json_fn): if wg.tanks != None:
async with aiofiles.open(replay_json_fn) as fp: tank = m.group(1)
replay_json = json.loads(await fp.read()) if tank in wg.tanks['userStr']:
# if replay_json['status'] == 'ok': tank = wg.tanks['userStr'][tank]
if wi.chk_JSON_replay(replay_json): else:
bu.verbose_std( bu.error('Tank code: "' + tank + '" not found from Tankopedia (tanks.json)')
msg_str + title + " has already been posted. Skipping." else:
) tank = m.group(1)
else: blitz_map = m.group(2)
os.remove(replay_json_fn) if blitz_map in wg.maps:
bu.debug( blitz_map = wg.maps[blitz_map]
msg_str else:
+ "Replay JSON not valid/complete: Deleting " bu.error('Mapcode: "' + blitz_map + '" not found from map database (maps.json)')
+ replay_json_fn, title = tank + ' @ ' + blitz_map
id=workerID, else:
) title = re.sub('\\.wotbreplay$', '', filename)
SKIPPED_N += 1 except Exception as err:
queue.task_done() bu.error(err)
continue else:
title.replace('NN', str(i))
except asyncio.CancelledError as err: return title
raise err
except Exception as err:
bu.error(msg_str + "Unexpected error: " + str(type(err)) + " : " + str(err))
try:
# bu.debug('Opening file [' + str(N) +']: ' + filename)
async with aiofiles.open(filename, "rb") as fp:
filename = os.path.basename(filename)
bu.debug(msg_str + "File: " + filename)
json_resp = await wi.post_replay(
await fp.read(), filename, account_id, title, priv, N
)
if json_resp is not None:
if await bu.save_JSON(replay_json_fn, json_resp):
if wi.chk_JSON_replay(json_resp):
if not bu.debug(msg_str + "Replay saved OK: " + filename):
bu.verbose_std(msg_str + title + " posted")
else:
bu.warning(
msg_str
+ "Replay file is not valid/complete: "
+ filename
)
ERROR_N += 1
else:
bu.error(msg_str + "Error saving replay: " + filename)
ERROR_N += 1
else:
bu.error(msg_str + "Replay file is not valid/complete: " + filename)
ERROR_N += 1
except Exception as err:
bu.error(
msg_str + "Unexpected Exception: " + str(type(err)) + " : " + str(err)
)
bu.debug(msg_str + "Marking task done")
queue.task_done()
return None
# def getTitle_old(replayfile: str, title: str, i: int) -> str: def getTitle(replayfile: str, title: str = None, i : int = 0) -> str:
# global wg global wg
if title == None:
# if title is None: try:
# try: filename = os.path.basename(replayfile)
# filename = os.path.basename(replayfile) bu.debug(filename)
# bu.debug(filename) tank = None
# map_usrStrs = wg.get_map_user_strs() map_name = None
# tank_userStrs = wg.get_tank_user_strs()
# # p = re.compile('\\d{8}_\\d{4}_(.+)_(' + '|'.join(map_usrStrs) + ')(?:-\\d)?\\.wotbreplay$')
# # update 6.2 changed the file name format. Bug fixed 2019-09-09 Jylpah
# p = re.compile(
# "\\d{8}_\\d{4}_.*?("
# + "|".join(tank_userStrs)
# + ")_("
# + "|".join(map_usrStrs)
# + ")(?:-\\d)?\\.wotbreplay$"
# )
# m = p.match(filename)
# if m is not None:
# if wg.tanks is not None:
# tank = m.group(1)
# if tank in wg.tanks["userStr"]:
# tank = wg.tanks["userStr"][tank]
# else:
# bu.error(
# 'Tank code: "'
# + tank
# + '" not found from Tankopedia (tanks.json)'
# )
# else:
# tank = m.group(1)
# blitz_map = m.group(2)
# if blitz_map in wg.maps:
# blitz_map = wg.maps[blitz_map]
# else:
# bu.error(
# 'Mapcode: "'
# + blitz_map
# + '" not found from map database (maps.json)'
# )
# title = tank + " @ " + blitz_map
# else:
# title = re.sub("\\.wotbreplay$", "", filename)
# except Exception as err:
# bu.error(err)
# else:
# title.replace("NN", str(i))
# return title
def getTitle(replayfile: str, title: str | None = None, i: int = 0) -> str:
global wg
assert wg is not None, "wg must not be None"
if title is None:
try:
filename = os.path.basename(replayfile)
bu.debug(filename)
tank = None
map_name = None
with zipfile.ZipFile(replayfile, "r") as archive:
# bu.debug('Replay file: ' + replayfile + ' opened')
with io.TextIOWrapper(archive.open("meta.json")) as meta:
# bu.debug('Replay file\'s metadata: opened')
try:
metadata_json = json.load(meta)
# player = metadata_json['playerName']
tank_name = wg.get_tank_name(
metadata_json["vehicleCompDescriptor"]
)
map_name = wg.get_map(metadata_json["mapName"])
bu.debug(
"Tank: " + tank_name
if tank_name is not None
else "NOT FOUND IN tanks.json" + " Map: " + map_name
)
except Exception as err:
bu.error(exception=err)
if (tank_name is not None) and (map_name is not None):
title = tank_name + " @ " + map_name
else:
title = re.sub("\\.wotbreplay$", "", filename)
except Exception as err:
bu.error(err)
else:
title.replace("NN", str(i))
bu.debug("Returning: " + title)
return title
with zipfile.ZipFile(replayfile, 'r') as archive:
# bu.debug('Replay file: ' + replayfile + ' opened')
with io.TextIOWrapper(archive.open('meta.json')) as meta:
# bu.debug('Replay file\'s metadata: opened')
try:
metadata_json = json.load(meta)
#player = metadata_json['playerName']
tank = wg.tank_str2name(metadata_json['playerVehicleName'])
map_name = wg.get_map(metadata_json['mapName'])
bu.debug('Tank: ' + tank + ' Map: ' + map_name)
except Exception as err:
bu.error(exception = err)
if (tank != None) and (map_name != None):
title = tank + ' @ ' + map_name
else:
title = re.sub('\\.wotbreplay$', '', filename)
except Exception as err:
bu.error(err)
else:
title.replace('NN', str(i))
bu.debug('Returning: ' + title)
return title
### UNIT TESTS - pytest ### UNIT TESTS - pytest
# class TestClass: # class TestClass:
# def test_getTitle(self): # def test_getTitle(self):
# #player = "jylpah" # #player = "jylpah"
# WG_appID = '81381d3f45fa4aa75b78a7198eb216ad' # WG_appID = '81381d3f45fa4aa75b78a7198eb216ad'
# wg = WG(WG_appID, 'tanks.json', 'maps.json') # wg = WG(WG_appID, 'tanks.json', 'maps.json')
# tank_name = "T28 Defender" # "A68_T28D" # tank_name = "T28 Defender" # "A68_T28D"
# map_name = "Yukon" # "idle" # map_name = "Yukon" # "idle"
# replay = './test/' + '20201030_2358__jylpah_A_1157485188135816346.wotbreplay' # replay = './test/' + '20201030_2358__jylpah_A_1157485188135816346.wotbreplay'
# res = tank_name + ' @ ' + map_name # res = tank_name + ' @ ' + map_name
# assert getTitle(replay) == res # assert getTitle(replay) == res
# @pytest.mark.asyncio # @pytest.mark.asyncio
# async def test_getTitle(self): # async def test_getTitle(self):
# #player = "jylpah" # #player = "jylpah"
# WG_appID = '81381d3f45fa4aa75b78a7198eb216ad' # WG_appID = '81381d3f45fa4aa75b78a7198eb216ad'
# wg = WG(WG_appID, 'tanks.json', 'maps.json') # wg = WG(WG_appID, 'tanks.json', 'maps.json')
# tank_name = "T28 Defender" # "A68_T28D" # tank_name = "T28 Defender" # "A68_T28D"
# map_name = "Yukon" # "idle" # map_name = "Yukon" # "idle"
# replay = './test/' + '20201030_2358__jylpah_A_1157485188135816346.wotbreplay' # replay = './test/' + '20201030_2358__jylpah_A_1157485188135816346.wotbreplay'
# res = tank_name + ' @ ' + map_name # res = tank_name + ' @ ' + map_name
# assert getTitle(replay) == res # assert getTitle(replay) == res
### main() ------------------------------------------- ### main() -------------------------------------------
if __name__ == "__main__": if __name__ == "__main__":
# To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp # To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp
if sys.platform.startswith("win") and sys.version_info >= (3, 8): if sys.platform.startswith("win") and sys.version_info >= (3, 8):
try: try:
from asyncio import WindowsSelectorEventLoopPolicy from asyncio import WindowsSelectorEventLoopPolicy
except ImportError: except ImportError:
pass pass
else: else:
if not isinstance( if not isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy):
asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
): asyncio.run(main(sys.argv[1:]), debug=False)
asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
asyncio.run(main(sys.argv[1:]), debug=False)