Author SHA1 Message Date
Jylpah f913f84f42 Merge pull request #33 from Jylpah/stable-filters-update2
Backport stable fixes
2021-11-23 18:44:17 +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
Jylpah 4c0de49077 name2tank_str(): error() -> debug() 2021-05-12 00:05:52 +03:00
Jylpah 07aa7af32f bug fix 2021-05-12 00:04:36 +03:00
Jylpah ea3306ee6c WoTinspector.post_replay(): add _id 2021-05-12 00:04:21 +03:00
Jylpah e4882d70af export replays(): --replays option to export replays 2021-05-11 20:55:13 +03:00
Jylpah 433c26d2f8 get_date_str(), get_replay_filename(): added 2021-05-11 20:54:39 +03:00
Jylpah f6b1b1353f Update analyze_wotb_replays.py 2021-05-11 14:14:10 +03:00
Jylpah 36fb53f0e7 convert_tank_names(): Fix tank key sorting 2021-04-29 00:09:40 +03:00
Jylpah b19a2169a7 Update 7.9 2021-04-29 00:09:04 +03:00
Jylpah 67aeaddf36 get_db_player_stats(): Fix aggregation 2021-04-27 10:26:30 +03:00
Jylpah 763f205175 Update 7.9 2021-04-27 10:24:01 +03:00
Jylpah d97c0cabd8 Update analyze_wotb_replays.py 2021-04-17 14:34:05 +03:00
Jylpah e6ff2f438d save_XXX_stats() funcs 2021-04-17 14:33:59 +03:00
Jylpah f4f153a175 stat_worker(): fix bug introduced in earlier commit 2021-04-15 09:15:00 +03:00
Jylpah 829e03f644 get_db_tank_stats(): optimize find() for index 2021-04-14 21:02:43 +03:00
Jylpah 53b2f62817 mk_readerQ_item(): add _id param 2021-04-09 12:08:14 +03:00
Jylpah 804a4a6483 update 7.8 2021-04-05 15:34:34 +03:00
Jylpah 47a1f96c4e Update 7.8 2021-04-05 14:39:38 +03:00
8 changed files with 998 additions and 240 deletions

No files matched your search

+272 -158
View File
@@ -28,6 +28,8 @@ REPLAY_I = 0
STAT_TANK_BATTLE_MIN = 100
BATTLE_TIME_BUCKET = 3600*24*14
OPT_EXPORT_REPLAY_DIR = 'export_replays'
RE_SRC_IS_DB = re.compile(r'^DB:')
class StatFunc:
@@ -98,7 +100,7 @@ replay_summary_flds = [
'protagonist',
'player_name',
'title',
'mastery_badge'
'mastery_badge'
]
replay_details_flds = [
@@ -165,6 +167,7 @@ class BattleCategorizationList():
'Regular' : 1,
'Training Room' : 2,
'Tournament' : 4,
'Quick Tournament' : 5, # maybe?
'Rating' : 7,
'Mad Games' : 8,
'Realistic Battles' : 22,
@@ -182,6 +185,7 @@ class BattleCategorizationList():
'battle_type' : [ 'Battle Type', 'category', [ 'Encounter', 'Supremacy']],
'room_type' : [ 'Battle Mode', 'category', _BATTLE_MODES ],
'top_tier' : [ 'Tier', 'category', [ 'Bottom tier', 'Top tier']],
'in_platoon' : [ 'Platoon', 'category', [ 'Solo', 'Platoon']],
'tank_tier' : [ 'Tank Tier', 'number' ],
'is_premium' : [ 'Tank Type', 'category', [ 'Tech tree', 'Premium']],
'tank_type' : [ 'Tank Class', 'category', WG.TANK_TYPE_STR],
@@ -201,7 +205,11 @@ class BattleCategorizationList():
'battle_duration' : [ 'Battle Duration', 'bucket', [ x*60 for x in range(0,8) ], 'int' ],
'distance_travelled' : [ 'Distance Driven', 'bucket', [ x*250 for x in range(0,20) ], 'int' ],
'allies_wins' : [ 'Allies WR', 'bucket', [ 0, .35, .45, .50, .55, .65], '%' ],
'allies_battles' : [ 'Player Battles', 'bucket', [ 0, 500, 1000, 2500, 5e3, 10e3, 15e3, 25e3], 'int' ],
'allies_damage_dealt' : [ 'Player Avg Dmg', 'bucket', [ 0, 500, 1000, 1250, 1500, 1750, 2000, 2500], 'int' ],
'enemies_wins' : [ 'Enemies WR', 'bucket', [ 0, .35, .45, .50, .55, .65], '%' ],
'enemies_battles' : [ 'Player Battles', 'bucket', [ 0, 500, 1000, 2500, 5e3, 10e3, 15e3, 25e3], 'int' ],
'enemies_damage_dealt' : [ 'Player Avg Dmg', 'bucket', [ 0, 500, 1000, 1250, 1500, 1750, 2000, 2500], 'int' ],
'player_name' : [ 'Player', 'string', 25 ],
'protagonist' : [ 'account_id', 'number' ],
'tank_name' : [ 'Tank', 'string', 25],
@@ -219,6 +227,12 @@ class BattleCategorizationList():
]
_categorizations_stats = [
'player_wins',
'player_battles',
'player_battles'
]
@classmethod
def get_categorizations_all(cls):
return cls._categorizations.keys()
@@ -321,8 +335,7 @@ class BattleCategorizationList():
def __init__(self, cats: list):
self.urls = collections.OrderedDict()
self.url_title_max_len = 0
self.url_title_max_len = 0
cats = list(set(cats)) # remove duplicates
# ordering
@@ -352,6 +365,11 @@ class BattleCategorizationList():
bu.error('BattleCategorizationList()', exception=err)
def __iter__(self):
"""Returns the Iterator object"""
return BattleCategorizationListIterator(self)
def get_categorization(self, cat: str):
"""Return a BattleCategorization for a category"""
try:
@@ -409,7 +427,7 @@ class BattleCategorizationList():
def get_filter_categories(self, cat: str, filters: list) -> dict:
try:
return self.categorizations[cat].get_filter_categories(filters)
return self.categorizations[str(cat)].get_filter_categories(filters)
except Exception as err:
bu.error(exception=err)
return None
@@ -455,6 +473,20 @@ class BattleCategorizationList():
return None
class BattleCategorizationListIterator():
def __init__(self, btl_cats: BattleCategorizationList) -> None:
self._btl_cats = btl_cats
self._ndx = 0
def __next__(self):
if self._ndx < len(self._btl_cats.categorizations_list):
cat = self._btl_cats.categorizations_list[self._ndx]
res = self._btl_cats.categorizations[cat]
self._ndx += 1
return res
raise StopIteration
class BattleCategorization():
RESULT_CAT_HEADER_LEN = 15
@@ -489,10 +521,13 @@ class BattleCategorization():
def get_filter_categories(self, filters: list) -> list:
if type(filters) != list:
filters = [ filters ]
return [ x for x in filters if x in self.get_categories() ]
try:
if type(filters) != list:
filters = [ filters ]
return [ x for x in filters if x in self.get_categories() ]
except Exception as err:
bu.error('Category: ' + str(self.category_key), exception=err)
return None
def help(self):
"""Help"""
@@ -711,7 +746,10 @@ class BattleStringCategorization(BattleCategorization):
def get_filter_categories(self, filters: list) -> list:
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:
bu.error('Category: ' + str(self.category_key), exception=err)
return None
@@ -747,11 +785,14 @@ class BattleNumberCategorization(BattleCategorization):
def get_filter_categories(self, filters: list) -> list:
try:
if type(filters) != list:
filters = [ filters ]
return list(map(str,filters))
except Exception as err:
bu.error('Category: ' + str(self.category_key), exception=err)
return None
def help(self):
super().help()
print('INTEGER')
@@ -892,12 +933,20 @@ class BattleCategory():
'battles%' : [ '% Battles', 'Share of Battles', 9, '{:9.0%}' ],
'win' : [ 'WR', 'Win rate', 6, '{:6.1%}' ],
'damage_made' : [ 'DPB', 'Average Damage', 5, '{:5.0f}' ],
'damage_received' : [ 'DPBr', 'Average Damage Received', 5, '{:5.0f}' ],
'enemies_destroyed' : [ 'KPB', 'Kills / Battle', 4, '{:4.2f}' ],
'DR' : [ 'DR', 'Damage Ratio', 5, '{:5.1f}' ],
'KDR' : [ 'KDR', 'Kills / Death', 4, '{:4.1f}' ],
'enemies_spotted' : [ 'Spot', 'Enemies spotted per battle', 4, '{:4.1f}' ],
'hit_rate' : [ 'Hit rate', 'Shots hit / all shots made', 8, '{:8.1%}' ],
'shots_made' : [ 'Shots', 'Shots made', 5, '{:5.1f}' ],
'hits_received' : [ 'Hits R.', 'Hits reveived', 7, '{:7.1f}' ],
'hits_bounced' : [ 'Bounced R.', 'Hits & bounced reveived', 10, '{:10.1f}' ],
'in_platoon' : [ 'Plat%', 'Platoon rate', 6, '{:6.1%}' ],
'pen_rate' : [ 'Pen rate', 'Shots pen / shots hit', 8, '{:8.1%}' ],
'pen_rate_received' : [ 'Penned%', 'Received hits pen / hits received', 7, '{:7.1%}' ],
'splash_rate' : [ 'Splash%', 'HE splash shots / shots hit', 7, '{:7.1%}' ],
'splash_rate_received' : [ 'Splashed%', 'HE splash shots received / hits hit', 9, '{:9.1%}' ],
'survived' : [ 'Surv%', 'Survival rate', 6, '{:6.1%}' ],
'time_alive' : [ 'T alive', 'Time being alive in a battle in secs', 7, '{:7.0f}' ],
'alive' : [ 'Share live', 'Percentage of time being alive in a battle', 8, '{:8.0%}' ],
@@ -910,9 +959,9 @@ class BattleCategory():
'player_battles' : [ 'Player Btls', 'Average number battles of the player', 11, '{:11.0f}' ],
'allies_battles' : [ 'Allies Btls', 'Average number battles of the allies', 11, '{:11.0f}' ],
'enemies_battles' : [ 'Enemies Btls', 'Average number battles of the enemies', 12, '{:12.0f}' ],
'player_damage_dealt' : [ 'Player Avg Dmg', 'Player Average damage', 11, '{:11.0f}' ],
'allies_damage_dealt' : [ 'Allies Avg Dmg', 'Average damage of the allies', 11, '{:11.0f}' ],
'enemies_damage_dealt' : [ 'Enemies Avg Dmg', 'Average damage of the enemies', 12, '{:12.0f}' ],
'player_damage_dealt' : [ 'Player Avg Dmg', 'Player Average damage', 14, '{:14.0f}' ],
'allies_damage_dealt' : [ 'Allies Avg Dmg', 'Average damage of the allies', 14, '{:14.0f}' ],
'enemies_damage_dealt' : [ 'Enemies Avg Dmg', 'Average damage of the enemies', 15, '{:14.0f}' ],
MISSING_STATS : [ 'No stats', 'Players without stats avail', 8, '{:8.1%}']
}
@@ -921,13 +970,15 @@ class BattleCategory():
_result_fields_modes = {
'default' : [ 'battles', 'win','damage_made', 'enemies_destroyed', 'enemies_spotted',
'top_tier', 'DR', 'survived', 'allies_wins', 'enemies_wins'],
'team' : [ 'battles', 'win','player_wins','allies_wins','enemies_wins'],
'team_extended' : [ 'battles', 'battles%', 'win','player_wins','allies_wins','enemies_wins',
'team' : [ 'battles', 'win', 'in_platoon', 'player_wins', 'allies_wins','enemies_wins'],
'team_extended' : [ 'battles', 'battles%', 'win', 'in_platoon', 'player_wins', 'allies_wins', 'enemies_wins',
'player_damage_dealt', 'allies_damage_dealt', 'enemies_damage_dealt',
'player_battles','allies_battles','enemies_battles' ],
'extended' : [ 'battles', 'battles%', 'win','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',
'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() ]
}
@@ -940,11 +991,14 @@ class BattleCategory():
]
_result_ratios = {
'KDR' : [ 'enemies_destroyed', 'destroyed' ],
'DR' : [ 'damage_made', 'damage_received' ],
'hit_rate' : [ 'shots_hit', 'shots_made' ],
'pen_rate' : [ 'shots_pen', 'shots_hit' ],
'dmg_block_rate' : [ 'damage_blocked', 'damage_received' ]
'KDR' : [ 'enemies_destroyed', 'destroyed' ],
'DR' : [ 'damage_made', 'damage_received' ],
'hit_rate' : [ 'shots_hit', 'shots_made' ],
'splash_rate' : [ 'shots_splash', 'shots_hit' ],
'splash_rate_received' : [ 'hits_splash', 'hits_received' ],
'pen_rate' : [ 'shots_pen', 'shots_hit' ],
'pen_rate_received' : [ 'hits_pen', 'hits_received' ],
'dmg_block_rate' : [ 'damage_blocked', 'damage_received' ]
}
# _extended_stats = False
@@ -1014,12 +1068,21 @@ class BattleCategory():
@classmethod
def get_field_name(cls, field: str):
def get_field_name(cls, field: str) -> str:
try:
return cls._result_fields[field][0]
except Exception as err:
bu.error(exception=err)
@classmethod
def get_field_description(cls, field: str) -> str:
try:
return cls._result_fields[field][1]
except Exception as err:
bu.error(exception=err)
@classmethod
def get_field_width(cls, field: str):
try:
@@ -1081,6 +1144,17 @@ class BattleCategory():
return None
@classmethod
def help(cls):
try:
for field in cls.get_result_fields_all():
print('{:24s} : {}'.format(field, cls.get_field_description(field)))
except Exception as err:
bu.error(exception=err)
return None
def __init__(self):
try:
@@ -1302,7 +1376,6 @@ class PlayerHistogram():
return None
class ErrorCatchingArgumentParser(argparse.ArgumentParser):
def exit(self, status=0, message=None):
if status:
@@ -1358,14 +1431,14 @@ async def main(argv):
try:
if 'ANALYZER' in config.sections():
configAnalyzer = config['ANALYZER']
OPT_MODE_DEFAULT= configAnalyzer.getboolean('mode', OPT_MODE_DEFAULT)
OPT_HIST = configAnalyzer.getboolean('histograms', OPT_HIST)
OPT_STAT_FUNC = configAnalyzer.get('stat_func', fallback=OPT_STAT_FUNC)
OPT_WORKERS_N = configAnalyzer.getint('workers', OPT_WORKERS_N)
OPT_EXPORT_CSV_FILE = configAnalyzer.get('csv_file', OPT_EXPORT_CSV_FILE)
OPT_EXPORT_JSON_FILE = configAnalyzer.get('json_file', OPT_EXPORT_JSON_FILE)
res_categorizations = configAnalyzer.get('categorizations', None)
configAnalyzer = config['ANALYZER']
OPT_MODE_DEFAULT = configAnalyzer.getboolean('mode', OPT_MODE_DEFAULT)
OPT_HIST = configAnalyzer.getboolean('histograms', OPT_HIST)
OPT_STAT_FUNC = configAnalyzer.get('stat_func', fallback=OPT_STAT_FUNC)
OPT_WORKERS_N = configAnalyzer.getint('workers', OPT_WORKERS_N)
OPT_EXPORT_CSV_FILE = configAnalyzer.get('csv_file', OPT_EXPORT_CSV_FILE)
OPT_EXPORT_JSON_FILE= configAnalyzer.get('json_file', OPT_EXPORT_JSON_FILE)
res_categorizations = configAnalyzer.get('categorizations', None)
if res_categorizations != None:
res_categorizations.replace(' ','')
BattleCategorizationList.set_categorizations_default(res_categorizations.split(','))
@@ -1420,9 +1493,10 @@ async def main(argv):
parser.add_argument('--mapfile', type=str, default='maps.json', help='JSON file to read Blitz map names from. Default is "maps.json"')
parser.add_argument('--json', action='store_true', default=False, help='Export data in JSON format')
parser.add_argument('--csv', action='store_true', default=False, help='Export data in CSV format')
parser.add_argument('--replays', action='store_true', default=False, help='Export replay info in JSON format')
parser.add_argument('-o','--outfile', type=str, default='-', metavar="OUTPUT", help='File to write results. Default STDOUT')
parser.add_argument('--db', action='store_true', default=OPT_DB, help='Use DB - You are unlikely to have it')
parser.add_argument('--filters', type=str, default=None, help='Filter replays based on categories. Filters given in JSON format.\nUse array "[]" for multiple filters/values. see --mode help.\nExample: : [ {"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('--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')
@@ -1441,7 +1515,7 @@ async def main(argv):
# res_categories = BattleCategorization.get_categorizations(OPT_CATEGORIZATIONS, args)
bu.set_log_level(args.silent, args.verbose, args.debug)
bu.set_progress_step(250) # Set the frequency of the progress dots.
# bu.set_progress_step(250) # Set the frequency of the progress dots.
#### Connect to MongoDB. You are unlikely to have this set up...
bu.debug('DB_SERVER: ' + DB_SERVER)
@@ -1491,7 +1565,7 @@ async def main(argv):
# Make replay Queue
scanner_task = asyncio.create_task(mk_replayQ(replayQ, args, db))
bu.debug('Replay scanner started')
bu.set_counter('Reading replays', 10)
# Start tasks to process the Queue
for i in range(OPT_WORKERS_N):
reader_tasks.append(asyncio.create_task(replay_reader(replayQ, i, args)))
@@ -1499,7 +1573,7 @@ async def main(argv):
bu.debug('Waiting for the replay scanner to finish')
await asyncio.wait([scanner_task])
bu.finish_progress_bar()
# bu.debug('Scanner finished. Waiting for replay readers to finish the queue')
await replayQ.join()
await asyncio.sleep(0.1)
@@ -1509,17 +1583,28 @@ async def main(argv):
await asyncio.sleep(0.1)
results = []
players = set()
replays = dict()
for res in await asyncio.gather(*reader_tasks):
results.extend(res[0])
players.update(res[1])
replays.update(res[2])
if len(players) == 0:
raise Exception("No players found to fetch stats for. No replays found?")
if args.min != None:
results = filter_min_replays_by_player(results, args.min)
players = get_players(results)
replays = filter_replays(replays, results)
# Filter based on non-stats filters
results = filter_results(results, args.filters, stats_filters=False)
(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)
(player_stats, stat_id_map) = await process_player_stats(players, OPT_WORKERS_N, args, db)
bu.debug('Number of player stats: ' + str(len(player_stats)))
teamresults = calc_team_stats(results, player_stats, stat_id_map, args)
@@ -1564,6 +1649,9 @@ async def main(argv):
csvwriter.writerows(rows)
bu.verbose_std('Results exported to: ' + export_file)
if args.replays:
await export_replays(replays)
bu.debug('Finished. Cleaning up..................')
except Exception as err:
bu.error(exception=err)
@@ -1588,11 +1676,33 @@ async def help_extended(db : motor.motor_asyncio.AsyncIOMotorDatabase = None, pa
parser.print_help()
# 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
print('\n-------------------------------------------------------------------')
print('| DB Filter usage |')
print('-------------------------------------------------------------------')
print('')
print('-------------------------------------------------------------------\n')
print('You are unlikely to have DB setup required for these filters. This is "Jylpah\'s special"')
print('')
print("Syntax: --filters_db '{ \"replay.param\": VALUE, \"replay.param2\": { \"$MONGO_OPERATOR\" : VALUE }, ... }'")
@@ -1611,23 +1721,24 @@ async def help_extended(db : motor.motor_asyncio.AsyncIOMotorDatabase = None, pa
except Exception as 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):
"""Export replays"""
try:
replay_dir = OPT_EXPORT_REPLAY_DIR + '_' + bu.get_date_str()
bu.verbose_std('Exporting replays to ' + replay_dir)
if not os.path.isdir(replay_dir):
os.mkdir(replay_dir)
i = 0
for replay in replays.values():
filename = wg.get_replay_filename(replay)
async with aiofiles.open(os.path.join(replay_dir, filename), 'w', encoding="utf8") as f:
await f.write(json.dumps(replay, indent=4))
i += 1
bu.verbose_std( str(i) + ' replays exported')
except Exception as err:
bu.error(exception=err)
## move the class PlayerHistogram?
@@ -1669,7 +1780,7 @@ def filter_min_replays_by_player(results: list, min_replays: int) -> list:
for res in results:
if res['protagonist'] in players_enough_replays:
res_ret.append(res)
bu.verbose_std('Replays after filtering: ' + str(len(res_ret)))
return res_ret
except Exception as err:
@@ -1712,15 +1823,15 @@ def print_player_dist(histograms: dict):
print('')
hist.print()
def process_battle_results(results: dict, args : argparse.Namespace):
"""Process replay battle results"""
try:
url = args.url
cats = BattleCategorizationList.get_categorizations(args)
blt_cat_list= None
if args.filters != None:
results = filter_replays(results, args.filters)
# remove if?
if len(results) > 0:
blt_cat_list = BattleCategorizationList(cats)
for result in results:
@@ -1731,7 +1842,7 @@ def process_battle_results(results: dict, args : argparse.Namespace):
blt_cat_list.calc_results()
else:
warning_txt = 'No replays to process '
if args.filter != None:
if args.filters != None:
warning_txt = warning_txt + 'after filtering'
bu.warning(warning_txt)
@@ -1740,25 +1851,25 @@ def process_battle_results(results: dict, args : argparse.Namespace):
return blt_cat_list
def filter_replays(results: list, filter_json : str) -> bool:
def filter_results(results: list, filter_json : str, stats_filters=False) -> bool:
"""Filter replays based on battle category filters"""
try:
filters = json.loads(filter_json)
bu.debug(str(filters))
for filter in filters:
if type(filters[filter]) != list:
filters[filter] = [ filters[filter]]
if type(filters) != dict:
bu.error('invalid --filter arguments: ' + str(filters))
bu.error('invalid --filter arguments: ' + str(filter))
sys.exit(1)
filter_cats = BattleCategorizationList(filters.keys())
for cat in filters:
bu.verbose('Filters applied:')
for category in filter_cats:
cat = category.category_key
categorization = filter_cats.get_categorization(cat)
bu.debug('Category: ' + cat + ' filters type: ' + str(type(filters)) + ' filters: ' + str(filters))
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()
for result in results:
try:
@@ -1774,13 +1885,27 @@ def filter_replays(results: list, filter_json : str) -> bool:
res.append(result)
except Exception as err:
bu.error(exception=err)
bu.verbose('Replays after filtering: ' + str(len(res)))
bu.verbose_std('Replays after filtering: ' + str(len(res)))
return res
except Exception as err:
bu.error(exception=err)
return None
def filter_replays(replays_in: dict, results: list) -> list:
"""Filter source replays based on (filtered) results"""
replays = dict()
for res in results:
try:
# bu.debug('res[_id] : ' + res['_id'], force=True)
_id = res['_id']
replays[_id] = replays_in[_id]
except Exception as err:
bu.error(exception=err)
return replays
async def process_player_stats(players, N_workers: int, args : argparse.Namespace, db : motor.motor_asyncio.AsyncIOMotorDatabase) -> dict:
"""Start stat_workers to retrieve and store players' stats"""
## Create queue of player-tank pairs to find stats for
@@ -1934,42 +2059,38 @@ async def stat_worker(queue : asyncio.Queue, workerID: int, args : argparse.Name
try:
bu.debug('Stat_id: ' + stat_id, id=workerID)
bu.print_progress()
# Analysing player performance based on their stats on the tier tanks they are playing
stats_tmp = None
# Try cache first
if (stat_id not in stats):
stats_tmp = None
pruned_stat_id = prune_stat_id(stat_id)
if (pruned_stat_id not in stats):
stats_tmp = await stat_wg_func(pruned_stat_id, cache_only = True)
else:
stat_id_remap[stat_id] = pruned_stat_id
queue.task_done()
continue
pruned_stat_id = prune_stat_id(stat_id)
if (pruned_stat_id not in stats):
stats_tmp = await stat_wg_func(pruned_stat_id, cache_only = True)
else:
stat_id_remap[stat_id] = pruned_stat_id
continue
if (stats_tmp != None):
stats[pruned_stat_id] = stats_tmp
stat_id_remap[stat_id] = pruned_stat_id
queue.task_done()
continue
# try DB
stats[stat_id] = await stat_db_func(db, stat_id)
bu.debug('get_db_' + args.stat_func + '_stats returned: account_id=' + str(get_account_id_f_stat_id(stat_id)) + ': ' + str(stats[stat_id]), workerID)
if (stats_tmp != None):
stats[pruned_stat_id] = stats_tmp
stat_id_remap[stat_id] = pruned_stat_id
continue
# try DB
stats[stat_id] = await stat_db_func(db, stat_id)
bu.debug('get_db_' + args.stat_func + '_stats returned: account_id=' + str(get_account_id_f_stat_id(stat_id)) + ': ' + str(stats[stat_id]), workerID)
# no DB stats found, trying WG AP
if (stats[stat_id] == None):
stats[pruned_stat_id] = await stat_wg_func(pruned_stat_id)
stat_id_remap[stat_id] = pruned_stat_id
del stats[stat_id]
bu.debug('get_wg_' + args.stat_func + '_stats returned: account_id=' + str(get_account_id_f_stat_id(pruned_stat_id)) + ': ' + str(stats[pruned_stat_id]), workerID)
# no DB stats found, trying WG AP
if (stats[stat_id] == None):
stats[pruned_stat_id] = await stat_wg_func(pruned_stat_id)
stat_id_remap[stat_id] = pruned_stat_id
del stats[stat_id]
bu.debug('get_wg_' + args.stat_func + '_stats returned: account_id=' + str(get_account_id_f_stat_id(pruned_stat_id)) + ': ' + str(stats[pruned_stat_id]), workerID)
except KeyError as err:
bu.error('Key not found', err, id=workerID)
except Exception as err:
bu.error('Unexpected error', err, id=workerID)
queue.task_done()
finally:
queue.task_done()
except (asyncio.CancelledError, concurrent.futures.CancelledError):
bu.debug('Stats queue is empty', id=workerID)
@@ -2003,7 +2124,7 @@ async def get_wg_tank_stats(stat_id_str: str, cache_only = False) -> dict:
player_stats = await wg.get_player_tank_stats(account_id, [ tank_id ], hist_stats, cache_only = cache_only)
#bu.debug('account_id: ' + str(account_id) + ' ' + str(player_stats))
return await tank_stats_helper(player_stats)
return tank_stats_helper(player_stats)
except KeyError as err:
bu.error('account_id: ' + str(account_id) + ' tank_id:' + str(tank_id) +' : Key not found', err)
@@ -2029,8 +2150,8 @@ async def get_db_tank_stats(db : motor.motor_asyncio.AsyncIOMotorDatabase, stat_
for stat in hist_stats:
project[stat] = True
project['_id'] = False
cursor = dbc.find({ '$and': [ { 'account_id': account_id }, { 'last_battle_time': { '$gte': battletime - time_buffer }}, { 'tank_id' : tank_id } ] }, projection=project).sort('last_battle_time',-1).limit(1)
bu.debug('find(): tank_id={} account_id={} last_battle_time>={}'.format(tank_id, account_id, battletime - time_buffer))
cursor = dbc.find({ '$and': [ { 'tank_id' : tank_id }, { 'account_id': account_id }, { 'last_battle_time': { '$gte': battletime - time_buffer }} ] }, projection=project).sort('last_battle_time',-1).limit(1)
# pipeline = [ { '$match': { '$and': [ { 'account_id': account_id }, { 'last_battle_time': { '$lte': battletime + time_buffer }}, { 'tank_id' : tank_id } ]}},
# { '$sort': { 'last_battle_time': -1 }},
@@ -2041,7 +2162,11 @@ async def get_db_tank_stats(db : motor.motor_asyncio.AsyncIOMotorDatabase, stat_
# cursor = dbc.aggregate(pipeline, allowDiskUse=True)
#cursor = dbc.aggregate(pipeline)
return await tank_stats_helper(await cursor.to_list(1))
stats = await cursor.to_list(1)
# store cache
# await wg.save_tank_stats(account_id, [tank_id], stats)
return tank_stats_helper(stats)
except Exception as err:
bu.error('account_id: ' + str(account_id) + ' Error', err)
@@ -2062,7 +2187,7 @@ async def get_wg_tank_tier_stats(stat_id_str: str, cache_only = False) -> dict:
player_stats = await wg.get_player_tank_stats(account_id, tier_tanks, hist_stats, cache_only = cache_only)
#bu.debug('account_id: ' + str(account_id) + ' ' + str(player_stats))
return await tank_stats_helper(player_stats)
return tank_stats_helper(player_stats)
except KeyError as err:
bu.error('account_id: ' + str(account_id) + ' Key not found', err)
@@ -2085,7 +2210,7 @@ async def get_wg_tier_x_stats(stat_id_str: str, cache_only = False) -> dict:
player_stats = await wg.get_player_tank_stats(account_id, tier_tanks, hist_stats, cache_only = cache_only)
#bu.debug('account_id: ' + str(account_id) + ' ' + str(player_stats))
return await tank_stats_helper(player_stats)
return tank_stats_helper(player_stats)
except KeyError as err:
bu.error('account_id: ' + str(account_id) + ' Key not found', err)
@@ -2113,8 +2238,10 @@ async def get_db_tank_tier_stats(db : motor.motor_asyncio.AsyncIOMotorDatabase,
# cursor = dbc.aggregate(pipeline, allowDiskUse=True)
cursor = dbc.aggregate(pipeline)
return await tank_stats_helper(await cursor.to_list(1000))
stats = await cursor.to_list(1000)
# await wg.save_tank_stats(account_id, tier_tanks, stats)
return tank_stats_helper(stats)
except Exception as err:
bu.error('account_id: ' + str(account_id) + ' Error', err)
return None
@@ -2124,23 +2251,9 @@ async def get_db_tier_x_stats(db : motor.motor_asyncio.AsyncIOMotorDatabase, sta
"""Get player stats from MongoDB (you are very unlikely to have it, unless you have set it up)"""
if db == None:
return None
try:
dbc = db[DB_C_TANK_STATS]
try:
( account_id, battletime ) = str2ints(stat_id_str)
tier_tanks = wg.get_tanks_by_tier(10)
time_buffer = 2*7*24*3600
pipeline = [ { '$match': { '$and': [ { 'account_id': account_id }, { 'last_battle_time': { '$lte': battletime + time_buffer }}, { 'tank_id' : {'$in': tier_tanks} } ]}},
{ '$sort': { 'last_battle_time': -1 }},
{ '$group': { '_id': '$tank_id', 'doc': { '$first': '$$ROOT' }}},
{ '$replaceRoot': { 'newRoot': '$doc' }},
{ '$project': { '_id': 0 }} ]
# cursor = dbc.aggregate(pipeline, allowDiskUse=True)
cursor = dbc.aggregate(pipeline)
return await tank_stats_helper(await cursor.to_list(1000))
return await get_db_tank_tier_stats(db,get_stat_id(account_id, 10, battletime) )
except Exception as err:
bu.error('account_id: ' + str(account_id) + ' Error', err)
return None
@@ -2158,14 +2271,16 @@ async def get_db_player_stats(db : motor.motor_asyncio.AsyncIOMotorDatabase, sta
pipeline = [ { '$match': { '$and': [ { 'account_id': account_id }, { 'last_battle_time': { '$lte': battletime + time_buffer }} ]}},
{ '$sort': { 'last_battle_time': -1 }},
{ '$group': { '_id': '$tank_id', 'doc': { '$first': '$$ROOT' }}},
{ '$group': { '_id': { 'tank_id':'$tank_id'}, 'doc': { '$first': '$$ROOT' }}},
{ '$replaceRoot': { 'newRoot': '$doc' }},
{ '$project': { '_id': 0 }} ]
# cursor = dbc.aggregate(pipeline, allowDiskUse=True)
cursor = dbc.aggregate(pipeline)
return await tank_stats_helper(await cursor.to_list(1000))
stats = await cursor.to_list(1000)
# await wg.save_tank_stats(account_id, [], stats)
return tank_stats_helper(stats)
except Exception as err:
bu.error('account_id: ' + str(account_id) + ' Error', err)
@@ -2193,20 +2308,17 @@ async def get_wg_player_stats(stat_id_str: str, cache_only = False) -> dict:
return None
async def tank_stats_helper(stat_list: list):
def tank_stats_helper(stat_list: list):
"""Helpher func got get_db_tank_tier_stats() and get_wg_tank_tier_stats()"""
try:
if stat_list == None:
return None
stats = dict()
stats = collections.defaultdict(def_value_zero)
hist_fields = histogram_fields.keys()
if 'battles' not in hist_fields:
bu.error('\'battles\' must be defined in \'histogram_fields\'')
return None
for field in hist_fields:
stats[field] = 0
for tank_stat in stat_list:
tank_stat = tank_stat['all']
for field in hist_fields:
@@ -2276,7 +2388,7 @@ async def mk_replayQ(queue : asyncio.Queue, args : argparse.Namespace, db : moto
async for replay_json in cursor:
_id = replay_json['_id']
#del(replay_json['_id'])
await queue.put(await mk_readerQ_item(replay_json, 'DB: _id = ' + _id))
await queue.put(await mk_readerQ_item(replay_json, _id=_id))
Nreplays += 1
bu.debug('All the matching replays have been read from the DB')
except Exception as err:
@@ -2292,7 +2404,7 @@ async def mk_replayQ(queue : asyncio.Queue, args : argparse.Namespace, db : moto
if (p_replayfile.match(line) != None):
replay_json = await bu.open_JSON(line, wi.chk_JSON_replay)
await queue.put(await mk_readerQ_item(replay_json, line))
await queue.put(await mk_readerQ_item(replay_json, filename=line))
except Exception as err:
bu.error(exception=err)
else:
@@ -2303,7 +2415,7 @@ async def mk_replayQ(queue : asyncio.Queue, args : argparse.Namespace, db : moto
fn = fn[:-1]
if os.path.isfile(fn) and (p_replayfile.match(fn) != None):
replay_json = await bu.open_JSON(fn, wi.chk_JSON_replay)
await queue.put(await mk_readerQ_item(replay_json, fn))
await queue.put(await mk_readerQ_item(replay_json, filename=fn))
bu.debug('File added to queue: ' + fn)
Nreplays += 1
elif os.path.isdir(fn):
@@ -2312,34 +2424,35 @@ async def mk_replayQ(queue : asyncio.Queue, args : argparse.Namespace, db : moto
if entry.is_file() and (p_replayfile.match(entry.name) != None):
bu.debug(entry.name)
replay_json = await bu.open_JSON(entry.path, wi.chk_JSON_replay)
await queue.put(await mk_readerQ_item(replay_json, entry.name))
await queue.put(await mk_readerQ_item(replay_json, filename=entry.name))
bu.debug('File added to queue: ' + entry.path)
Nreplays += 1
except Exception as err:
bu.error(exception=err)
bu.verbose('Finished scanning replays: ' + str(Nreplays) + ' replays to process')
return Nreplays
async def mk_readerQ_item(replay_json, filename : str = None) -> list:
async def mk_readerQ_item(replay_json, filename : str = None, _id: str = None) -> list:
"""Make an item to replay queue"""
global REPLAY_N
REPLAY_N +=1
try:
if wi.chk_JSON_replay(replay_json):
if not '_id' in replay_json:
id = wi.read_replay_id(replay_json)
replay_json['_id'] = id
if _id != None:
_id = wi.read_replay_id(replay_json)
replay_json['_id'] = _id
else:
replay_json = None # mark error
except Exception as err:
bu.error(exception=err)
if filename == None:
return [replay_json, REPLAY_N, '' ]
elif RE_SRC_IS_DB.match(filename) != None:
return [replay_json, REPLAY_N, filename ]
else:
if filename != None:
return [replay_json, REPLAY_N, os.path.basename(filename) ]
elif _id != None:
return [replay_json, REPLAY_N, 'DB: ' + _id ]
else:
return None
async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Namespace):
@@ -2347,6 +2460,7 @@ async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Nam
#global SKIPPED_N
results = []
playerstanks = set()
replays = dict()
try:
while True:
item = await queue.get()
@@ -2373,8 +2487,10 @@ async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Nam
playerstanks.update(set(result['allies']))
playerstanks.update(set(result['enemies']))
playerstanks.update(set([result['player']]))
_id = wi.read_replay_id(replay_json)
replays[_id] = replay_json
results.append(result)
bu.debug('Marking replay [' + str(replayID) + '] done', id=readerID)
except Exception as err:
@@ -2382,7 +2498,7 @@ async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Nam
queue.task_done()
except (asyncio.CancelledError, concurrent.futures.CancelledError):
bu.debug( str(len(results)) + ' replays, ' + str(len(playerstanks)) + ' player/tanks', readerID)
return results, playerstanks
return results, playerstanks, replays
return None
@@ -2442,29 +2558,27 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
result['allies_survived'] = 0 # for steamroller stats
result['enemies_survived'] = 0 # for steamroller stats
btl_duration = 0
btl_tier = 0
btl_tier_low = 10
btl_tier = 0
protagonist_tank = None
for player in replay_summary['details']:
btl_duration = max(btl_duration, player['time_alive'])
player_tank_tier = wg.get_tank_data(player['vehicle_descr'], 'tier')
btl_tier = max(btl_tier, player_tank_tier)
btl_tier_low = min(btl_tier_low, player_tank_tier)
if (protagonist != None) and (player['dbid'] == protagonist):
protagonist_tank = player['vehicle_descr']
btl_tier = max(btl_tier, player_tank_tier)
if player['dbid'] == account_id:
# player itself is not put in results['allies']
tmp = dict()
tmp['account_id'] = account_id
tmp['tank_id'] = player['vehicle_descr']
tank_id = player['vehicle_descr']
tmp['tank_id'] = tank_id
tmp['tank_tier'] = player_tank_tier
tmp['tank_name'] = wg.get_tank_name(tmp['tank_id'])
tmp['tank_type'] = wg.get_tank_type_id(tmp['tank_id'])
tmp['tank_nation'] = wg.get_tank_nation_id(tmp['tank_id'])
tmp['is_premium'] = int(wg.is_premium(tmp['tank_id']))
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:
tmp[key] = player[key]
if player['hitpoints_left'] == 0:
@@ -2497,7 +2611,7 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
result['battle_duration'] = btl_duration
## 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'])
# remove platoon buddy from stats
if result['squad_index'] != None:
@@ -2542,8 +2656,8 @@ def prune_stat_id(stat_id_str: str) -> str:
return ':'.join(stat_id)
def get_stat_id(account_id: int, tank_id: int, battletime: int) -> str:
return ':'.join([ str(account_id), str(tank_id), str(battletime)])
def get_stat_id(account_id: int, tank_id: int, battle_time: int) -> str:
return ':'.join(map(str, [account_id, tank_id, battle_time ]))
def get_stat_id_tank_tier(stat_id_str: str) -> str:
@@ -2553,7 +2667,7 @@ def get_stat_id_tank_tier(stat_id_str: str) -> str:
account_id = stat_id[0]
tank_tier = wg.get_tank_tier(stat_id[1])
battle_time = (stat_id[2] // BATTLE_TIME_BUCKET) * BATTLE_TIME_BUCKET
return ':'.join(map(str, [account_id, tank_tier, battle_time ]))
return get_stat_id(account_id, tank_tier, battle_time)
except Exception as err:
bu.error('Stats_id: ' + stat_id_str, exception=err)
return None
@@ -2566,7 +2680,7 @@ def get_stat_id_tank(stat_id_str: str) -> str:
account_id = stat_id[0]
tank_id = stat_id[1]
battle_time = (stat_id[2] // BATTLE_TIME_BUCKET) * BATTLE_TIME_BUCKET
return ':'.join(map(str, [account_id, tank_id, battle_time ]))
return get_stat_id(account_id, tank_id, battle_time)
except Exception as err:
bu.error('Stats_id: ' + stat_id_str, exception=err)
return None
+125 -41
View File
@@ -22,6 +22,11 @@ _log_level = NORMAL
LOG = False
LOGGER = None
MODE_TANK_STATS = 'tank_stats'
MODE_PLAYER_STATS = 'player_stats'
MODE_PLAYER_ACHIEVEMENTS= 'player_achievements'
MODE_TANKOPEDIA = 'tankopedia'
# Progress display
_progress_N = 100
_progress_i = 0
@@ -418,12 +423,14 @@ def set_progress_bar(heading: str, max_value: int, step: int = None, slow: bool
return
def set_counter(heading: str):
def set_counter(heading: str, step = None):
global _progress_obj, _progress_i
_progress_i = 0
if _progress_obj != None:
finish_progress_bar()
_progress_obj = Counter(heading)
_progress_obj = Counter(heading + ': ')
if step != None:
set_progress_step(step)
return
@@ -479,6 +486,16 @@ def NOW() -> int:
return int(time.time())
def get_date_str(timestamp:int = NOW(), date_format = '%Y%m%d_%H%M%S'):
"""Return YYYYMMDD_HHmm date string"""
try:
ts = datetime.fromtimestamp(timestamp)
return ts.strftime(date_format)
except Exception as err:
error(exception=err)
return None
def rebase_file_args(current_dir, files):
"""REbase file command line params after moving working dir to the script's dir"""
if isinstance(files, list):
@@ -626,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]))
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
# -----------------------------------------------------------
@@ -649,8 +674,6 @@ class StatsNotFound(Exception):
pass
# -----------------------------------------------------------
# Class WG
# -----------------------------------------------------------
@@ -686,7 +709,7 @@ class WG:
# WHERE account_id = {} AND stat = {}
# AND date >= {} LIMIT 1;"""
# TANK_STATS
SQL_TANK_STATS_TBL = 'tank_stats'
SQL_TANK_STATS_CREATE_TBL = 'CREATE TABLE IF NOT EXISTS ' + SQL_TANK_STATS_TBL + \
@@ -700,6 +723,7 @@ class WG:
SQL_TANK_STATS_UPDATE = 'REPLACE INTO ' + SQL_TANK_STATS_TBL + '(account_id, tank_id, update_time, stats) VALUES(?,?,?,?)'
# PLAYER_STATS
SQL_PLAYER_STATS_TBL = 'player_stats'
SQL_PLAYER_STATS_CREATE_TBL = 'CREATE TABLE IF NOT EXISTS ' + SQL_PLAYER_STATS_TBL + \
@@ -713,6 +737,7 @@ class WG:
SQL_PLAYER_STATS_CACHED = 'SELECT * FROM ' + SQL_PLAYER_STATS_TBL + ' WHERE account_id = ? AND update_time > ?'
# PLAYER_ACHIEVEMENTS
SQL_PLAYER_ACHIEVEMENTS_TBL = 'player_achievements'
SQL_PLAYER_ACHIEVEMENTS_CREATE_TBL = 'CREATE TABLE IF NOT EXISTS ' + SQL_PLAYER_ACHIEVEMENTS_TBL + \
@@ -720,12 +745,13 @@ class WG:
update_time INTEGER NOT NULL,
stats TEXT)"""
SQL_PLAYER_ACHIEVEMENTS_COUNT = 'SELECT COUNT(*) FROM ' + SQL_PLAYER_ACHIEVEMENTS_TBL
SQL_PLAYER_ACHIEVEMENTS_CACHED = 'SELECT * FROM ' + SQL_PLAYER_ACHIEVEMENTS_TBL + ' WHERE account_id = ? AND update_time > ?'
SQL_PLAYER_ACHIEVEMENTS_UPDATE = 'REPLACE INTO ' + SQL_PLAYER_ACHIEVEMENTS_TBL + '(account_id, update_time, stats) VALUES(?,?,?)'
SQL_PLAYER_ACHIEVEMENTS_COUNT = 'SELECT COUNT(*) FROM ' + SQL_PLAYER_ACHIEVEMENTS_TBL
SQL_TABLES = [ SQL_PLAYER_STATS_TBL, SQL_TANK_STATS_TBL, SQL_PLAYER_ACHIEVEMENTS_TBL ]
SQL_CHECK_TABLE_EXITS = """SELECT name FROM sqlite_master WHERE type='table' AND name=?"""
@@ -908,33 +934,18 @@ class WG:
@classmethod
def get_map(cls, map_str: str) -> str:
def get_map(self, map_str: str) -> str:
"""Return map name from short map string in replays"""
try:
return cls.maps[map_str]
return self.maps[map_str]
except:
error('Map ' + map_str + ' not found')
return None
@classmethod
def get_map_user_strs(cls) -> str:
return cls.maps.keys()
@classmethod
def tank_str2name(cls, tank_str: str) -> str:
"""Return tank name from short tank string in replays"""
try:
return cls.tanks["userStr"][tank_str]
except:
error('Tank ' + tank_str + ' not found')
return tank_str
@classmethod
def get_tank_user_strs(cls) -> str:
return cls.tanks["userStr"].keys()
def get_tank_user_strs(self) -> str:
return self.tanks["userStr"].keys()
@classmethod
@@ -1070,11 +1081,15 @@ class WG:
def load_tanks(self, tankopedia_fn: str):
"""Load tanks from tankopedia JSON"""
if tankopedia_fn == None:
return False
return False
try:
with open(tankopedia_fn, 'rt', encoding='utf8') as f:
self.tanks = json.loads(f.read())
self.tanks['tankStr'] = dict()
p = re.compile('.+_short$')
for usr_str in self.tanks['userStr']:
if p.match(usr_str) == None:
self.tanks['tankStr'][self.tanks['userStr'][usr_str]] = usr_str
self.tanks_by_tier = dict()
for tier in range(1,11):
self.tanks_by_tier[str(tier)] = list()
@@ -1084,7 +1099,44 @@ class WG:
except Exception as err:
error('Could not read tankopedia: ' + tankopedia_fn, err)
return False
def get_replay_filename(self, replay: dict):
try:
summary = replay['data']['summary']
timestamp = int(summary['battle_start_timestamp'])
timestamp = get_date_str(timestamp, date_format='%Y%m%d_%H%M')
player = summary['player_name']
arena_id = int(summary['arena_unique_id'])
vehicle = self.name2tank_str(summary['vehicle'])
vehicle.replace(' ','_')
return '{:s}_{:s}_{:s}_{:d}.wotbreplay.json'.format(timestamp, player, vehicle, arena_id)
except Exception as err:
error('Unexpected Exception', err)
return None
def get_map_user_strs(self) -> str:
return self.maps.keys()
def tank_str2name(self, tank_str: str) -> str:
"""Return tank name from short tank string in replays"""
try:
return self.tanks["userStr"][tank_str]
except:
error('Tank ' + tank_str + ' not found')
return tank_str
def name2tank_str(self, name: str) -> str:
"""Return shorttank string from tank string in replays"""
try:
return self.tanks["tankStr"][name]
except:
debug('Tank ' + name + ' not found')
return name
def get_tanks_by_tier(self, tier: int) -> list():
"""Returns tank_ids by tier"""
@@ -1261,7 +1313,7 @@ class WG:
# debug('JSON Response received: ' + str(json_data))
stats = json_data['data'][str(account_id)]
if cache:
await self.put_2_statsQ('tank_stats', [account_id, tank_ids], stats)
await self.save_tank_stats(account_id, tank_ids, stats)
return stats
except Exception as err:
error(exception=err)
@@ -1294,7 +1346,7 @@ class WG:
# debug('JSON Response received: ' + str(json_data))
stats = json_data['data'][str(account_id)]
if cache:
await self.put_2_statsQ('player_stats', [account_id], stats)
await self.save_player_stats(account_id, stats)
return stats
except Exception as err:
error(exception=err)
@@ -1333,7 +1385,7 @@ class WG:
for account_id in json_data['data'].keys():
stats[account_id] = json_data['data'][account_id]
if cache:
await self.put_2_statsQ('player_achievements', [int(account_id)], json_data['data'][account_id])
await self.save_player_achievements(int(account_id), json_data['data'][account_id])
return stats
except Exception as err:
error(exception=err)
@@ -1440,7 +1492,7 @@ class WG:
return None
async def put_2_statsQ(self, statsType: str, key: list, stats: list):
async def save_stats(self, statsType: str, key: list, stats: list):
"""Save stats to a async queue to be saved by the stat_saver -task"""
if self.statsQ == None:
return False
@@ -1449,6 +1501,30 @@ class WG:
return True
async def save_player_stats(self, account_id: int, stats: dict):
try:
await self.save_stats(MODE_PLAYER_STATS, [int(account_id)], stats)
except Exception as err:
error(exception=err)
return None
async def save_player_achievements(self, account_id: int, stats: dict):
try:
await self.save_stats(MODE_PLAYER_ACHIEVEMENTS, [int(account_id)], stats)
except Exception as err:
error(exception=err)
return None
async def save_tank_stats(self, account_id: int, tank_ids: list, stats: dict):
try:
await self.save_stats(MODE_TANK_STATS, [int(account_id), tank_ids], stats)
except Exception as err:
error(exception=err)
return None
async def stat_saver(self):
"""Async task for saving stats into cache in background"""
@@ -1464,8 +1540,13 @@ class WG:
await self.cache.commit()
async with self.cache.execute(WG.SQL_TANK_STATS_COUNT) as cursor:
debug('Cache contains: ' + str((await cursor.fetchone())[0]) + ' cached player tank stat records' )
if is_debug():
async with self.cache.execute(WG.SQL_PLAYER_STATS_COUNT) as cursor:
debug('Cache contains: ' + str((await cursor.fetchone())[0]) + ' player stat records' )
async with self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_COUNT) as cursor:
debug('Cache contains: ' + str((await cursor.fetchone())[0]) + ' player achievement records' )
async with self.cache.execute(WG.SQL_TANK_STATS_COUNT) as cursor:
debug('Cache contains: ' + str((await cursor.fetchone())[0]) + ' player tank stat records' )
except Exception as err:
error(exception=err)
sys.exit(1)
@@ -1474,19 +1555,20 @@ class WG:
try:
stats = await self.statsQ.get()
stats_type = stats[0]
stat_type = stats[0]
key = stats[1]
stats_data = stats[2]
update_time = stats[3]
if stats_type == 'tank_stats':
#debug('stat_type: ' + stat_type + ', key: ' + str(key))
if stat_type == MODE_TANK_STATS:
await self.store_tank_stats(key, stats_data, update_time)
elif stats_type == 'player_stats':
elif stat_type == MODE_PLAYER_STATS:
await self.store_player_stats(key, stats_data, update_time)
elif stats_type == 'player_achievements':
elif stat_type == MODE_PLAYER_ACHIEVEMENTS:
await self.store_player_achievements(key, stats_data, update_time)
else:
error('Function to saves stats type \'' + stats_type + '\' is not implemented yet')
error('Function to saves stats type \'' + stat_type + '\' is not implemented yet')
except asyncio.CancelledError:
# this is an eternal loop that will wait until cancelled
@@ -1521,7 +1603,7 @@ class WG:
for stat in stats_data:
tank_id = stat['tank_id']
await self.cache.execute(WG.SQL_TANK_STATS_UPDATE, (account_id, tank_id, update_time, json.dumps(stat)))
tank_ids.remove(tank_id)
tank_ids.discard(tank_id)
# no stats found => Add None to mark that
for tank_id in tank_ids:
await self.cache.execute(WG.SQL_TANK_STATS_UPDATE, (account_id, tank_id, update_time, None))
@@ -1793,6 +1875,8 @@ class WoTinspector:
except Exception as err:
debug(exception=err, id=N)
await asyncio.sleep(SLEEP)
_id = self.read_replay_id(json_resp)
json_resp['_id'] = _id
debug(' Could not post replay: ' + title, id=N)
return json_resp
+10 -8
View File
@@ -79,8 +79,8 @@ async def main(argv):
tankopedia = collections.OrderedDict()
tankopedia['status'] = 'ok'
tankopedia['meta'] = { "count": len(tanks) }
tankopedia['data'] = tanks
tankopedia['userStr'] = userStrs
tankopedia['data'] = bu.sort_dict(tanks, number=True)
tankopedia['userStr'] = bu.sort_dict(userStrs)
bu.verbose_std('New tankopedia \'' + args.tanks + '\' contains ' + str(len(tanks)) + ' tanks')
bu.verbose_std('New tankopedia \'' + args.tanks + '\' contains ' + str(len(userStrs)) + ' tank strings')
await outfile.write(json.dumps(tankopedia, ensure_ascii=False, indent=4, sort_keys=False))
@@ -115,13 +115,12 @@ async def extract_tanks(blitz_app_base : str, nation: str):
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['nation'] = nation
tank['tank_id'] = await get_tank_id(nation, int(tank_xml['id']))
tank['tier'] = int(tank_xml['level'])
tank['type'] = await get_tank_type(tank_xml['tags'])
tank['userStr'] = tank_xml['userString']
#bu.debug('Reading tank string: ' + tank['userStr'], force=True)
tanks.append(tank)
except Exception as err:
@@ -165,7 +164,10 @@ async def convert_tank_names(tanklist : list, tank_strs: dict) -> dict:
tank['name'] = tank_strs[tank['userStr']]
#userStrs[tank['userStr'].split(':')[1]] = tank['name']
tank.pop('userStr', None)
tankopedia[str(tank['tank_id'])] = tank
tank_tmp = collections.OrderedDict()
for key in sorted(tank.keys()):
tank_tmp[key] = tank[key]
tankopedia[str(tank['tank_id'])] = tank_tmp
for tank_str in tank_strs:
skip = False
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/local/bin/python3.7
#!/usr/bin/env python3
# Get Tankpedia from WoTinspector.com
+5 -1
View File
@@ -43,13 +43,17 @@
"neptune_02": "Normandy - Beach",
"north": "North",
"ordeal": "Trial by Fire",
"plant": "Ghost Factory",
"pliego": "Castilla",
"port": "Port Bay",
"rift": "Hellas",
"rock": "Mayan Ruins",
"rudniki": "Mines",
"rudniki_01": "Mines - Hill",
"rudniki_02": "Mines - Village",
"rudniki_03": "Mines - Center",
"savanna": "Oasis Palms",
"skit": "Naval Frontier",
"test": "World of Ducks",
"tutorial": "Proving Grounds"
}
}
+7
View File
@@ -2,6 +2,13 @@
| Release | Date | Notes | Inaccurate? |
|:-------:|:--------:|-------------|-----|
| 8.4 | 2021-10-20 | Polish heavy tanks | |
| 8.3 | 2021-09-16 | FV4005, FV4022, VK 91.01 (P) and WZ-121 legendary camos, -5 deg gun depression to WZ-120 | |
| 8.2 | 2021-08-18 | Ghost Factory map, XP/credits nerf for inactive players, new store UI | |
| 8.1 | 2021-07-15 | Armor highlighting updated | |
| 8.0 | 2021-06-08 | American experimental heavies (Yoh-line), PBR camos | |
| 7.9 | 2021-04-27 | Mines rework, new tutorial, colorblind mode, JgPz E100 legendary camo | |
| 7.8 | 2021-03-24 | Mid-tier balance changes, Grille line new consumables | |
| 7.7 | 2021-02-24 | Czechoslovakian medium tanks | |
| 7.6 | 2021-01-27 | AMC 13 90, TOG II buffs, free replay camera | |
| 7.5 | 2020-12-09 | ATGM removal, Batchat, Kpz70, T-2020 buff | |
+570 -23
View File
File diff suppressed because it is too large. Load diff
+8 -8
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.' )
else:
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
queue.task_done()
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):
bu.verbose_std(msg_str + title + ' posted')
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
else:
bu.error(msg_str + 'Error saving replay: ' + filename)
ERROR_N += 1
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
except Exception as err:
bu.error(msg_str + 'Unexpected Exception: ' + str(type(err)) + ' : ' + str(err) )
@@ -286,14 +286,14 @@ def getTitle(replayfile: str, title: str = None, i : int = 0) -> str:
# bu.debug('Replay file\'s metadata: opened')
try:
metadata_json = json.load(meta)
#player = metadata_json['playerName']
tank = wg.tank_str2name(metadata_json['playerVehicleName'])
#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 + ' 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:
bu.error(exception = err)
if (tank != None) and (map_name != None):
title = tank + ' @ ' + map_name
if (tank_name != None) and (map_name != None):
title = tank_name + ' @ ' + map_name
else:
title = re.sub('\\.wotbreplay$', '', filename)
except Exception as err: