#!/usr/bin/python2.2
# -*- mode: python -*-
#
# guidscan
#
# $Id: guidscan,v 1.1 2003/07/03 23:08:02 elho Exp $
#
# Copyright (C) 2003 Elmar Hoffmann
#
# guidscan scans Quake III server logs that contain PunkBuster
# messages as well and creates a list of GUID's with associated
# player names.
#
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

import sys
import os
import getopt
import re


class GUID:
    # _private_ precompiled regular expressions
    _re_print = re.compile('^broadcast: print "(.*)\\\\n"$')

    _re_disc = re.compile(
        '^(?P<name>.+)\^7 (disconnected|timed out|Server command overflow)$')
    _re_ren = re.compile('^(?P<old>.+)\^7 renamed to (?P<new>.+)$')

    _re_guid1 = re.compile(
        '^\^3PunkBuster Server: Player (?P<guid>[0-9a-z]+)(\([^\)]*\))?'
        ' (?P<name>.*) \(slot #(?P<slot>\d+) power.*\) (?P<ip>[^:]*):\d+$')
    _re_guid2 = re.compile(
        '^(\^3PunkBuster|\[skipnotify\]\^3.* PB) Server: Player GUID'
        ' Computed (?P<guid>[0-9a-z]+)(\([^\)]*\))? \(slot #(?P<slot>\d+)\)'
        ' (?P<ip>[^:]*):\d+ (?P<name>.*)$')

    def __init__(self):
        self.guids = {}

    def ScanQ3ConsoleLog(self, file):
        players = {}
        newplayers = {}

        for line in file:
            m = self._re_print.match(line)
            if m:
                line = m.group(1)
                # player disconnect
                m = self._re_disc.match(line)
                if m:
                    name = m.group("name")
                    if name in players:
                        del players[name]
                else:
                    # player rename
                    m = self._re_ren.match(line)
                    if m:
                        old = m.group("old")
                        new = m.group("new")
                        try:
                            guid = players[old]
                        except KeyError:
                            try:
                                guid = players[new]
                            except KeyError:
                                # player GUID not yet known
                                newplayers[new] = old
                            else:
                                # delayed rename message
                                if old not in self.guids[guid]["names"]:
                                    self.guids[guid]["names"].append(old)
                        else:
                            del players[old]
                            players[new] = guid
                            if new not in self.guids[guid]["names"]:
                                self.guids[guid]["names"].append(new)

            else:
                # PB GUID
                m = self._re_guid1.match(line)
                if not m:
                    m = self._re_guid2.match(line)
                if m:
                    guid = m.group("guid")
                    name = m.group("name")

                    players[name] = guid
                    if guid in self.guids:
                        if name not in self.guids[guid]["names"]:
                            self.guids[guid]["names"].append(name)
                        if m.group("ip") not in self.guids[guid]["ips"]:
                            self.guids[guid]["ips"].append(m.group("ip"))
                    else:
                        self.guids[guid] = {
                            "names" :  [name],
                            "ips" : [m.group("ip")]
                            }
                    while name in newplayers:
                        old = name
                        name = newplayers[name]
                        self.guids[guid]["names"].append(name)
                        del newplayers[old]
                    continue

            if line[:35] == '------- Game Initialization -------':
                # New map, reset players
                players = {}
                newplayers = {}
        return self.guids

    def WriteText(self, file, stripcolors=False):
        keys = self.guids.keys()
        keys.sort()
        for k in keys:
            file.write(k)
            names = self.guids[k]['names']
            if stripcolors:
                names = map(self.Q3StripColor, names)
            for n in names:
                file.write(' "%s"' % n)
            file.write('\n')

    def Q3StripColor(self, string):
        """strip Q3A color codes from given string
        returns string"""
        s = ''
        i = 0
        while i < len(string):
            if string[i] == '^':
                i += 1
            else:
                s += string[i]
            i += 1
        return s


def main():
    try:
        opts, args = getopt.getopt(sys.argv[1:], 'shV',
                                   [ 'stripcolors', 'help', 'version'])
    except getopt.GetoptError, err:
        errorhelp("unrecognized option '%s'" % err)
        sys.exit(2)

    stripcolors = False
    for (o, a) in opts:
        if o in ('-h', '--help'):
            usage()
            sys.exit()
        elif o in ('-V', '--version'):
            version()
            sys.exit()
        elif o in ('-s', '--stripcolors'):
            stripcolors = True

    if not len(args):
        errorhelp("no input files")
        sys.exit(2)

    guid = GUID()
    rc = 0
    for arg in args:
        try:
            f = file(arg)
        except IOError, err:
            error(err)
            rc = 1
        else:
            guid.ScanQ3ConsoleLog(f)
            f.close()

    guid.WriteText(sys.stdout, stripcolors)
    sys.exit(rc)

def usage():
    print "Usage: %s [options] file...\n" % os.path.basename(sys.argv[0])

    print "  -h, --help            print this help, then exit"
    print "  -V, --version         print version number, then exit"
    print
    print "  -s, --stripcolors     strip colorcodes from playernames"
    print

def version():
    print "guidscan $Revision: 1.1 $"
    print "Copyright 2003 Elmar Hoffmann"
    print "This is free software; see the GNU General Public Licence version 2 or"
    print "later for copying conditions.  There is NO warranty."

def error(msg):
    sys.stderr.write("%s: %s\n" % (os.path.basename(sys.argv[0]), msg))

def errorhelp(msg):
    progname = os.path.basename(sys.argv[0])
    sys.stderr.write("%s: %s\nTry '%s --help' for more information.\n"
                     % (progname, msg, progname))


if __name__ == "__main__":
    main()
