#! /usr/bin/python """ (c) 2009 JRG Productions """ """ Everything here written 100% by Joe Gillotti """ """ PieStats! A better, simpler, faster stats system for Soldat Copyright (C) 2009 Joseph 'jrgp' Gillotti This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ # Some config console_log_path = '../logs/' kill_log_path = '../logs/kills/' t_prefix = 'pys_' version = '0.1' # Get teh kewel shit import glob import re import MySQLdb import time import os import sys import copy from optparse import OptionParser # Hold the regexes chats_regex = re.compile("\n(\d{2}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(.+)\](.+)") grabs_regex = re.compile("\n(\d\d-\d\d-\d\d \d\d:\d\d:\d\d) (.*) captured the (Red Flag|Blue Flag|Yellow Flag|Objective)") returns_regex = re.compile("\n(\d\d-\d\d-\d\d \d\d:\d\d:\d\d) (.*) returned the (Red Flag|Blue Flag|Yellow Flag|Objective)") caps_regex = re.compile("\n(\d\d-\d\d-\d\d \d\d:\d\d:\d\d) (.*) scores for (Alpha|Bravo|Charlie|Delta) Team") date_regex = re.compile('(\d{2}\-\d{2}\-\d{2}\-\d{2})') kill_regex = re.compile("\n---\s(\d{2}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})\n(.+)\n(.+)\n(Ak\-74|Barret M82A1|Barrett M82A1|Bow|Chainsaw|Clusters|Combat Knife|Desert Eagles|FN Minimi|Flame Bow|Flamer|Grenade|HK MP5|Hands|LAW|M79|Ruger 77|Spas\-12|Stationary gun|Steyr AUG|USSOCOM|XM214 Minigun|Selfkill)") # Function defs: def strtotime(str): """ Convert dates from console logs into unix time. """ return int(time.mktime(time.strptime(str, '%y-%m-%d %H:%M:%S'))) def getdate(str): """ Return date part of file name """ get = re.findall(date_regex, str) if get: return get[0] else: return '00-00-00-00' def player_id(player): """ Get player id. If doesn't exist, insert it and get new id """ global player_id_cache player = player.strip() player = db.literal(player) # exist in cache? if player in player_id_cache: #print 'cache' return player_id_cache[player] c = db.cursor() c.execute('select `id` from `%splayers` where `name` = %s limit 1' % (t_prefix, player)) if c.rowcount == 1: row = c.fetchone() c.close() player_id_cache[player] = row[0] return row[0] else: c = db.cursor() c.execute('insert into `%splayers` set name = %s' % (t_prefix, player)) id = c.lastrowid player_id_cache[player] = id c.close() db.commit() return id def save_setting(key, value): """ Save setting into settings table """ key = db.literal(key) value = 'UNIX_TIMESTAMP()' if value == 'UNIX_TIMESTAMP()' else db.literal(value) c = db.cursor() c.execute('select s.value from `%ssettings` as s where s.key = %s limit 1' % (t_prefix, key)) if c.rowcount == 1: current = c.fetchone() if current[0] != value: c = db.cursor() c.execute('update `%ssettings` as s set s.value = %s where s.key = %s' % (t_prefix, value, key)) db.commit() else: c = db.cursor() c.execute('insert into `%ssettings` (`key`, value) values (%s, %s)' % (t_prefix, key, value)) db.commit() # very fucking important. THIS IS INNODB!! def inc_setting(setting, new): """ Increment a setting. Maybe a stat like number of kills or something """ key = db.literal(setting) new = int(new) c = db.cursor() c.execute('update `%ssettings` set `value` = `value` + %d where `key` = %s limit 1' % (new, setting)) affected = c.rowcount c.close() if affected == 0: c = db.cursor() c.execute('insert into `%ssettings` set `value` = %d where `key` = %s' % (new, setting)) c.close() db.commit() def get_setting(key): """ get a setting from the db """ key = db.literal(key) c = db.cursor() c.execute('select s.value from `%ssettings` as s where s.key = %s limit 1' % (t_prefix, key)) if c.rowcount == 1: value = c.fetchone() c.close() return value[0] else: c.close() return '' def latest_console_log(): """ Get latest console log name and size. """ log = get_setting('last_console_log') if log == '': return ('00-00-00-00', '0') else: return log.split(';') def latest_kill_log(): """ Get latest kill log name and size. """ log = get_setting('last_kill_log') if log == '': return ('00-00-00-00', '0') else: return log.split(';') def parse_console_logs(): """ Parse and collect data from console logs """ # Need to be able to modify these global new_console_log_name, new_console_log_size # Latest console log, and size last_console_log = latest_console_log() last_console_log_date = getdate(last_console_log[0]) # Get list of console logs console_log_files = glob.glob('%sConsoleLog-*-*-*-*.txt' % (console_log_path)) # Nothing? if len(console_log_files) == 0: return # Go through the files for filepath in console_log_files: # Exclude path fn = os.path.basename(filepath) # And old file? Skip it if getdate(fn) < last_console_log_date: print 'Skipping %s' % fn continue # Our latest file? elif getdate(fn) == last_console_log_date: # If it is the same file size, skip it if int(last_console_log[1]) == os.path.getsize(filepath): print 'Skipping unchanged file %s' % fn return # just end here else: # Otherwise start working from where we left off print 'Working on current file %s' % fn f = open(filepath, 'r') f.seek(int(last_console_log[1]) - 1, 0) # See if it's a new file. elif getdate(fn) > last_console_log_date: print 'Working on new file %s' % fn f = open(filepath, 'r') # Get file's contents contents = f.read() # Do chats if parse_messages: matches = re.findall(chats_regex, contents) if matches: for match in matches: if match[2].strip() != '' and match[2].find('joining game') == -1: chats.append({'name':match[1], 'date':strtotime(match[0]), 'msg':match[2]}) # Do grabs matches = re.findall(grabs_regex, contents) if matches: for match in matches: if match[1] in players: players[match[1]]['grabs'] += 1 else: players[match[1]] = copy.deepcopy(player_template) players[match[1]]['grabs'] = 1 # Do returns matches = re.findall(returns_regex, contents) if matches: for match in matches: if match[1] in players: if 'returns' in players[match[1]]: players[match[1]]['returns'] += 1 else: players[match[1]]['returns'] = 1 else: players[match[1]] = copy.deepcopy(player_template) players[match[1]]['returns'] = 1 # Do caps matches = re.findall(caps_regex, contents) if matches: for match in matches: if match[1] in players: if 'caps' in players[match[1]]: players[match[1]]['caps'] += 1 else: players[match[1]]['caps'] = 1 else: players[match[1]] = copy.deepcopy(player_template) players[match[1]]['caps'] = 1 # Set most recent file position and name new_console_log_size = f.tell() new_console_log_name = fn # Close this file f.close() def parse_kill_logs(): """ Parse and collect data from console logs """ # Need to be able to modify these global new_kill_log_size, new_kill_log_name # Latest kill log, and size last_kill_log = latest_kill_log() last_kill_log_date = getdate(last_kill_log[0]) # Get list of kill logs kill_log_files = glob.glob('%sKillLog-*-*-*-*.txt' % (kill_log_path)) # Nothing? if len(kill_log_files) == 0: return # Go through the files for filepath in kill_log_files: # Exclude path fn = os.path.basename(filepath) # And old file? Skip it if getdate(fn) < last_kill_log_date: print 'Skipping %s' % fn continue # Our latest file? elif getdate(fn) == last_kill_log_date: # If it is the same file size, skip it if int(last_kill_log[1]) == os.path.getsize(filepath): print 'Skipping unchanged file %s' % fn return # just end here else: # Otherwise start working from where we left off print 'Working on current file %s' % fn f = open(filepath, 'r') f.seek(int(last_kill_log[1]) - 1, 0) # See if it's a new file. elif getdate(fn) > last_kill_log_date: print 'Working on new file %s' % fn f = open(filepath, 'r') # Get file's contents contents = f.read() # Do kills :D matches = re.findall(kill_regex, contents) if matches: for match in matches: #print match # Localize and prepare for database kill_date = strtotime(match[0]) weapon = re.sub(' ', '-', match[3]) # Fix weapon bug weapon = 'USSOCOM' if match[1] != match[2] and weapon == 'Selfkill' else weapon # A suicide? suicide = True if match[1] == match[2] else False # Make sure match[1] is there if match[1] not in players: players[match[1]] = copy.deepcopy(player_template) # Make sure match[2] is there, if not match[1], since that's a waste of a few microseconds if not suicide and match[2] not in players: players[match[2]] = copy.deepcopy(player_template) # Make sure weapons used by killer is stat'd if match[1] not in weapon_stats: weapon_stats[match[1]] = copy.deepcopy(weapon_stats_template) # Make sure weapons used by victim is stat'd if not suicide and match[2] not in weapon_stats: weapon_stats[match[2]] = copy.deepcopy(weapon_stats_template) # Make sure weapon is there if weapon not in weapons: weapons[weapon] = copy.deepcopy(weapon_template) # What? if suicide: players[match[1]]['suicides'] += 1 weapons[weapon]['suicides'] += 1 weapon_stats[match[1]][weapon+'_s'] += 1 else: players[match[1]]['kills'] = players[match[1]]['kills'] + 1 players[match[2]]['deaths'] += 1 players[match[1]]['last_kill'] = kill_date weapons[weapon]['kills'] += 1 weapon_stats[match[1]][weapon+'_k'] += 1 weapon_stats[match[2]][weapon+'_d'] += 1 # Set most recent file position and name new_kill_log_size = f.tell() new_kill_log_name = fn # close file f.close() # End function defs. Get working with them: # Fun little message print '[*] JRG Productions PyStats v%s' % version # Connect to mysql try: db = MySQLdb.connect('localhost', '', '', '') except MySQLdb.OperationalError, error: print 'Failed connecting to MySQL - %s' % error[1] sys.exit() # Deal with CLI stuff cli_parser = OptionParser() cli_parser.add_option('-p', '--prefix', dest='prefix', type='string', help='Use this prefix for the tables') cli_parser.add_option('-f', '--folder', dest='path', type='string', help='Use this log/ path') cli_parser.add_option('-m', dest='messages', action='store_true', default=False, help='Record all in-game messages') cli_parser.add_option('-c', dest='clear', action='store_true', default=False, help='Empty tables') cli_parser.add_option('-d', dest='delete', action='store_true', default=False, help='Delete tables') cli_parser.add_option('-i', dest='install', action='store_true', default=False, help='Install. Create tables with this prefix.') (cli_options, cli_args) = cli_parser.parse_args(sys.argv) # Custom database prefixes if cli_options.prefix: print '[*] Using table preifx prefix %s' % cli_options.prefix t_prefix = cli_options.prefix # Work with in game messages? Slow and a waste of space. if cli_options.messages: parse_messages = True else: parse_messages = False # Install the tables under this prefix? if cli_options.install: tables_file = '%s/tables.sql' % sys.path[0] if os.path.exists(tables_file): print 'Installing..' f = open(tables_file, 'r') statements = f.read() f.close() statements = re.sub('\/\*prefix\*\/', t_prefix, statements) statements = statements.split(';') for statement in statements: if statement.strip() != '': c = db.cursor() c.execute(statement) c.close() db.commit() else: print 'Cannot find tables.sql ' sys.exit() # Do we wanna clear everything? if cli_options.clear: print 'Clearing..' # The order here is important.. to_clear = ['weapon_stats', 'messages', 'weapons', 'settings', 'players'] for table in to_clear: c = db.cursor() c.execute('truncate if exists `%s%s`' % (t_prefix, table)) db.commit() c.close() sys.exit() # Do we wanna delete everything? if cli_options.delete: print 'Deleting..' # The order here is important.. to_delete = ['weapon_stats', 'messages', 'weapons', 'settings', 'players'] for table in to_delete: c = db.cursor() c.execute('drop table IF EXISTS `%s%s`' % (t_prefix, table)) db.commit() c.close() sys.exit() # Custom logfile paths if cli_options.path: new_path = cli_options.path if cli_options.path[-1] == '/' else cli_options.path+'/' console_log_path = new_path kill_log_path = '%skills/' % new_path print "[*] Using %s for paths" % cli_options.path # Base player dictionary. player_template = {'grabs': 0,'caps': 0,'returns': 0,'kills': 0,'deaths': 0,'suicides': 0,'last_kill': 0} # Base weapon dictionary weapon_template = {'kills': 0,'suicides': 0} # Player weapon stats dictionary weapon_stats_template = {} # Helps speed player_id_cache = {} # Populate it c = db.cursor() c.execute('explain `%sweapon_stats`' % t_prefix) res = c.fetchall() for field in res: if field[0].find('_k') > -1 or field[0].find('_s') > -1 or field[0].find('_d') > -1: weapon_stats_template[field[0]] = 0 c.close() # Hold the chats. A tuple filled with dictionaries. chats = [] # Hold the players. A multidimensional dictionary. Player names are keys. players = {} # Hold the weapons. Also a multidimensional dictionary. Weapon names are keys. weapons = {} # Hold the weapon stats weapon_stats = {} # New console log position new_console_log_size = 0 new_console_log_name = '' # New kill log position new_kill_log_size = 0 new_kill_log_name = '' # Handle console logs parse_console_logs() # Handle kill logs parse_kill_logs() # Start saving info print '[*] Saving modified players (at least %d)' % len(players) # Work with new and existing players for k, v in players.iteritems(): c = db.cursor() c.execute(""" update `%splayers` set `returns` = `returns` + %d, `grabs` = `grabs` + %d, `caps` = `caps` + %d, `kills` = `kills` + %d, `deaths` = `deaths` + %d, `suicides` = `suicides` + %d, `last_kill` = %d where `id` = %d limit 1 """ % ( t_prefix, v['returns'], v['grabs'], v['caps'], v['kills'], v['deaths'], v['suicides'], v['last_kill'], player_id(k) ) ) c.close() db.commit() # Work with weapons print '[*] Saving modified weapons (%d)' % len(weapons) # Weapons themselves for k, v in weapons.iteritems(): c = db.cursor() c.execute(""" update `%sweapons` set `kills` = `kills` + %d, `suicides` = `suicides` + %d where `weapon` = %s limit 1 """ % (t_prefix, v['kills'], v['suicides'], db.literal(k))) affected = c.rowcount c.close() db.commit() # Weapon not there? Add it if affected == 0: c = db.cursor() c.execute(""" insert into `%sweapons` set `kills` = %d, `suicides` = %d, `weapon` = %s """ % (t_prefix, v['kills'], v['suicides'], db.literal(k))) c.close() db.commit() # Weapons for each player. Deliberately a second table. for k, v in weapon_stats.iteritems(): player = player_id(k) update_values = [] insert_values = [] for w, r in v.iteritems(): update_values.append('`%s` = `%s` + %d' % (w, w, r)) insert_values.append('`%s` = %d' % (w, r)) update_string = ', '.join(update_values) insert_string = ', '.join(insert_values) c = db.cursor() c.execute(""" update `%sweapon_stats` set %s where `player` = %d limit 1 """ % (t_prefix, update_string, player)) affected = c.rowcount c.close() db.commit() # Weapon stat for this player not there? Add it if affected == 0: c = db.cursor() c.execute(""" insert into `%sweapon_stats` set `player` = %d, %s """ % (t_prefix, player, insert_string)) c.close() db.commit() # Insert new chats print '[*] Inserting new chats (%d)' % len(chats) for chat in chats: c = db.cursor() c.execute(""" insert into `%smessages` set `date` = %d, `playerid` = %d, `message` = %s """ % (t_prefix, chat['date'], player_id(chat['name']), db.literal(chat['msg']))) c.close() db.commit() # Save our console log position, if need be if new_console_log_name != '': save_setting('last_console_log', '%s;%d' % (new_console_log_name, new_console_log_size)) # Same goes for kill logs if new_kill_log_name != '': save_setting('last_kill_log', '%s;%d' % (new_kill_log_name, new_kill_log_size)) # Whence updated save_setting('last_updated', 'UNIX_TIMESTAMP()') # That might just be it. print 'Done'