# OpenArena 0.8.1 parser for BigBrotherBot(B3) (www.bigbrotherbot.net)
# Copyright (C) 2010 Courgette & GrosBedo
# 
# 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., 51 Franklin St, Fifth Floor, Boston, MA    02110-1301    USA
#
# CHANGELOG
# 08/08/2010 - 0.1 - Courgette
# * creation based on smg11 parser
# 09/08/2010 - 0.2 - Courgette
# * implement rotatemap()
# 09/08/2010 - 0.3 - Courgette & GrosBedo
# * bot now recognize /tell commands correctly
# 10/08/2010 - 0.4 - Courgette
# * recognizes MOD_SUICIDE as suicide
# * get rid of PunkBuster related code
# * should \rcon dumpuser in cases the ClientUserInfoChanged line does not have
#   guid while player is not a bot. (untested, cannot reproduce)
# 11/08/2010 - 0.5 - GrosBedo
# * minor fix for the /rcon dumpuser when no guid
# * added !nextmap (with recursive detection !)
# 11/08/2010 - 0.6 - GrosBedo
# * fixed the permanent ban command (banClient -> banaddr)
# 12/08/2010 - 0.7 - GrosBedo
# * added weapons and means of death. Define what means of death are suicides
# 17/08/2010 - 0.7.1 - GrosBedo
# * added say_team recognition
# 20/08/2010 - 0.7.5 - GrosBedo
# * added many more regexp to detect ctf events, cvars and awards
# * implement permban by ip and unbanbyip
# * implement team recognition
# 20/08/2010 - 0.8 - Courgette
# * clean regexp (Item, CTF, Award, fallback)
# * clean OnItem
# * remove OnDamage
# * add OnCtf and OnAward
# 27/08/2010 - 0.8.1 - GrosBedo
# * fixed findnextmap underscore bug (maps and vstr cvars with an underscore are now correctly parsed)
# 28/08/2010 - 0.8.2 - Courgette
# * fix another findnextmap underscore bug
# 28/08/2010 - 0.8.3 - Courgette
# * fix issue with the regexp that match 'Award:' lines
# 04/09/2010 - 0.8.4 - GrosBedo
# * fix issue with CTF flag capture events
# 17/09/2010 - 0.8.5 - GrosBedo
# * fix crash issue when a player has disconnected at the very time the bot check for the list of players
# 20/10/2010 - 0.9 - GrosBedo
# * fix a BIG issue when detecting teams (were always unknown)
# 20/10/2010 - 0.9.1 - GrosBedo
# * fix tk issue with DM and other team free gametypes
# 20/10/2010 - 0.9.2 - GrosBedo
# * added EVT_GAME_FLAG_RETURNED (move it to q3a or a generic ioquake3 parser?)
# 23/10/2010 - 0.9.3 - GrosBedo
# * detect gametype and modname at startup
# * added flag_taken action
# * fix a small bug when triggering the flag return event
# 07/11/2010 - 0.9.4 - GrosBedo
# * ban and unban messages are now more generic and can be configured from b3.xml
# * messages now support named $variables instead of %s
# 08/11/2010 - 0.9.5 - GrosBedo
# * messages can now be empty (no message broadcasted on kick/tempban/ban/unban)
# 09/04/2011 - 0.9.6 - Courgette
# * reflect that cid are not converted to int anymore in the clients module
# 06/06/2011 - 0.10.0 - Courgette
# * change data format for EVT_CLIENT_BAN events
# 14/06/2011 - 0.11.0 - Courgette
# * cvar code moved to q3a AbstractParser
#
__author__  = 'Courgette, GrosBedo'
__version__ = '0.11.0'
 
import re, string, thread, time, threading
import b3
import b3.events
from b3.parsers.q3a.abstractParser import AbstractParser
 
class Oa081Parser(AbstractParser):
    gameName = 'oa081'
    _connectingSlots = []
    _maplist = None
 
    _settings = {}
    _settings['line_length'] = 65
    _settings['min_wrap_length'] = 100
 
    _empty_name_default = 'EmptyNameDefault'
 
    _commands = {}
    #_commands['message'] = 'tell %(cid)s %(prefix)s ^3[pm]^7 %(message)s'
    #_commands['deadsay'] = 'tell %(cid)s %(prefix)s [DEAD]^7 %(message)s'
    _commands['message'] = 'say %(prefix)s ^3[pm]^7 %(message)s'
    _commands['deadsay'] = 'say %(prefix)s [DEAD]^7 %(message)s'
    _commands['say'] = 'say %(prefix)s %(message)s'
    _commands['set'] = 'set %(name)s "%(value)s"'
    _commands['kick'] = 'clientkick %(cid)s'
    _commands['ban'] = 'banaddr %(cid)s' #addip for q3a
    _commands['tempban'] = 'clientkick %(cid)s'
    _commands['banByIp'] = 'banaddr %(ip)s'
    _commands['unbanByIp'] = 'bandel %(cid)s' #removeip for q3a
    _commands['banlist'] = 'listbans' #g_banips for q3a
 
    _eventMap = {
        'warmup' : b3.events.EVT_GAME_WARMUP,
        'restartgame' : b3.events.EVT_GAME_ROUND_END
    }
 
    # remove the time off of the line
    _lineClear = re.compile(r'^(?:[0-9:.]+\s?)?')
 
    _lineFormats = (
        re.compile(r'^(?P<action>[a-z]+):\s*(?P<data>(?P<cid>[0-9]+):\s*(?P<pbid>[0-9A-Z]{32}):\s*(?P<name>[^:]+):\s*(?P<num1>[0-9]+):\s*(?P<num2>[0-9]+):\s*(?P<ip>[0-9.]+):(?P<port>[0-9]+))$', re.IGNORECASE),
        re.compile(r'^(?P<action>[a-z]+):\s*(?P<data>(?P<cid>[0-9]+):\s*(?P<name>.+):\s+(?P<text>.*))$', re.IGNORECASE),
 
        # 1:25 CTF: 1 2 2: Sarge returned the BLUE flag!
        # 1:16 CTF: 1 1 3: Sarge fragged RED's flag carrier!
        # 6:55 CTF: 2 1 2: Burpman returned the RED flag!
        # 7:02 CTF: 2 2 1: Burpman captured the BLUE flag!
        re.compile(r'^(?P<action>CTF):\s+(?P<cid>[0-9]+)\s+(?P<fid>[0-9]+)\s+(?P<type>[0-9]+):\s+(?P<data>.*(?P<color>RED|BLUE).*)$', re.IGNORECASE),
 
        #47:05 Kill: 2 4 11: Sarge killed ^6Jondah by MOD_LIGHTNING
        re.compile(r'^(?P<action>[a-z]+):\s*(?P<data>(?P<acid>[0-9]+)\s(?P<cid>[0-9]+)\s(?P<aweap>[0-9]+):\s*(?P<text>.*))$', re.IGNORECASE),
 
        # 7:02 Award: 2 4: Burpman gained the CAPTURE award!
        # 7:02 Award: 2 5: Burpman gained the ASSIST award!
        # 7:30 Award: 2 3: Burpman gained the DEFENCE award!
        # 29:15 Award: 2 2: SalaManderDragneL gained the IMPRESSIVE award!
        # 32:08 Award: 2 1: SalaManderDragneL gained the EXCELLENT award!
        # 8:36 Award: 10 1: Karamel is a fake gained the EXCELLENT award!
        re.compile(r'^(?P<action>Award):\s+(?P<cid>[0-9]+)\s+(?P<awardtype>[0-9]+):\s+(?P<data>(?P<name>.+) gained the (?P<awardname>\w+) award!)$', re.IGNORECASE),
 
        #
        re.compile(r'^(?P<action>[a-z]+):\s*(?P<data>(?P<cid>[0-9]+):\s*(?P<text>.*))$', re.IGNORECASE),
        re.compile(r'^(?P<action>[a-z]+):\s*(?P<data>(?P<cid>[0-9]+)\s(?P<text>.*))$', re.IGNORECASE),
 
        # 81:16 tell: grosbedo to courgette: !help
        # 81:16 say: grosbedo: !help
        re.compile(r'^(?P<action>tell):\s(?P<data>(?P<name>.+) to (?P<aname>.+): (?P<text>.*))$', re.IGNORECASE),
 
        # 19:33 sayteam: UnnamedPlayer: ahahaha
        re.compile(r'^(?P<action>sayteam):\s(?P<data>(?P<name>.+): (?P<text>.*))$', re.IGNORECASE),
 
        # 46:37 Item: 4 team_CTF_redflag
        # 54:52 Item: 2 weapon_plasmagun
        re.compile(r'^(?P<action>Item):\s+(?P<cid>[0-9]+)\s+(?P<data>.*)$', re.IGNORECASE),
 
        #
        # Falling through?
        # 1:05 ClientConnect: 3
        # 1:05 ClientUserinfoChanged: 3 guid\CAB616192CB5652375401264987A23D0\n\xlr8or\t\0\model\wq_male2/red\g_redteam\\g_blueteam\\hc\100\w\0\l\0\tt\0\tl\0
        re.compile(r'^(?P<action>[a-z_]\w*):\s*(?P<data>.*)$', re.IGNORECASE)
    )
 
    #map: dm_fort
    #num score ping name            lastmsg address               qport rate
    #--- ----- ---- --------------- ------- --------------------- ----- -----
    #  1     1    0 TheMexican^7          100 bot                       0 16384
    #  2     1    0 Sentenza^7             50 bot                       0 16384
    #  3     3   37 xlr8or^7                0 145.99.135.227:27960   3598 25000
    _regPlayer = re.compile(r'^(?P<slot>[0-9]+)\s+(?P<score>[0-9-]+)\s+(?P<ping>[0-9]+)\s+(?P<name>.*?)\s+(?P<last>[0-9]+)\s+(?P<ip>[0-9.]+)\s+(?P<qport>[0-9]+)\s+(?P<rate>[0-9]+)$', re.I)
    _reColor = re.compile(r'(\^.)|[\x00-\x20]|[\x7E-\xff]')
 
    # 7:44 Exit: Capturelimit hit.
    # 7:44 red:8  blue:0
    # 7:44 score: 63  ping: 81  client: 2 ^2^^0Pha^7nt^2om^7^^0Boo
    # 7:44 score: 0  ping: 0  client: 1 Sarge
    _reTeamScores = re.compile(r'^red:(?P<RedScore>.+)\s+blue:(?P<BlueScore>.+)$', re.I)
    _rePlayerScore = re.compile(r'^score:\s+(?P<score>[0-9]+)\s+ping:\s+(?P<ping>[0-9]+|CNCT|ZMBI)\s+client:\s+(?P<slot>[0-9]+)\s+(?P<name>.*)$', re.I)
 
 
    # Ban #1: 200.200.200.200/32
    _reBanList = re.compile(r'^Ban #(?P<cid>[0-9]+):\s+(?P<ip>[0-9]+.[0-9]+.[0-9]+.[0-9]+)/(?P<range>[0-9]+)$', re.I)
 
    PunkBuster = None
 
 
    ##  means of death
    #===========================================================================
    MOD_UNKNOWN = 0
    MOD_SHOTGUN = 1
    MOD_GAUNTLET = 2
    MOD_MACHINEGUN = 3
    MOD_GRENADE = 4
    MOD_GRENADE_SPLASH = 5
    MOD_ROCKET = 6
    MOD_ROCKET_SPLASH = 7
    MOD_PLASMA = 8
    MOD_PLASMA_SPLASH = 9
    MOD_RAILGUN = 10
    MOD_LIGHTNING = 11
    MOD_BFG = 12
    MOD_BFG_SPLASH = 13
    MOD_WATER = 14
    MOD_SLIME = 15
    MOD_LAVA = 16
    MOD_CRUSH = 17
    MOD_TELEFRAG = 18
    MOD_FALLING = 19
    MOD_SUICIDE = 20
    MOD_TARGET_LASER = 21
    MOD_TRIGGER_HURT = 22
    # #ifdef MISSIONPACK
    MOD_NAIL = 23
    MOD_CHAINGUN = 24
    MOD_PROXIMITY_MINE = 25
    MOD_KAMIKAZE = 26
    MOD_JUICED = 27
    # #endif
    MOD_GRAPPLE = 28
    #===========================================================================
 
 
    ## meansOfDeath to be considered suicides
    Suicides = (
        MOD_WATER,
        MOD_SLIME,
        MOD_LAVA,
        MOD_CRUSH,
        MOD_FALLING,
        MOD_SUICIDE,
        MOD_TRIGGER_HURT,
    )
#---------------------------------------------------------------------------------------------------
 
    def startup(self):
 
        # registering a ioquake3 specific event
        self.Events.createEvent('EVT_GAME_FLAG_RETURNED', 'Flag returned')
 
        # add the world client
        self.clients.newClient('1022', guid='WORLD', name='World', hide=True, pbid='WORLD')
 
        # get map from the status rcon command
        map_name = self.getMap()
        if map_name:
            self.game.mapName = map_name
            self.info('map is: %s'%self.game.mapName)
 
        # get gamepaths/vars
        try:
            fs_game = self.getCvar('fs_game').getString()
            if fs_game == '':
                fs_game = 'baseoa'
            self.game.fs_game = fs_game
            self.game.modName = fs_game
            self.debug('fs_game: %s' % self.game.fs_game)
        except:
            self.game.fs_game = None
            self.game.modName = None
            self.warning("Could not query server for fs_game")
 
        try:
            self.game.fs_basepath = self.getCvar('fs_basepath').getString().rstrip('/')
            self.debug('fs_basepath: %s' % self.game.fs_basepath)
        except:
            self.game.fs_basepath = None
            self.warning("Could not query server for fs_basepath")
 
        try:
            self.game.fs_homepath = self.getCvar('fs_homepath').getString().rstrip('/')
            self.debug('fs_homepath: %s' % self.game.fs_homepath)
        except:
            self.game.fs_homepath = None
            self.warning("Could not query server for fs_homepath")
 
        try:
            self.game.gameType = self.defineGameType(self.getCvar('g_gametype').getString())
            self.debug('g_gametype: %s' % self.game.gameType)
        except:
            self.game.gameType = None
            self.warning("Could not query server for g_gametype")
 
        # initialize connected clients
        self.info('discover connected clients')
        plist = self.getPlayerList()
        for cid, c in plist.iteritems():
            userinfostring = self.queryClientUserInfoByCid(cid)
            if userinfostring:
                self.OnClientuserinfochanged(None, userinfostring)
 
 
#---------------------------------------------------------------------------------------------------
 
    # Added for debugging and identifying/catching log lineparts
    def getLineParts(self, line):
        line = re.sub(self._lineClear, '', line, 1)
 
        for f in self._lineFormats:
            m = re.match(f, line)
            if m:
                self.debug('XLR--------> line matched %s' % f.pattern)
                break
 
        if m:
            client = None
            target = None
 
            try:
                action =  m.group('action').lower()
            except IndexError:
                # special case for damage lines where no action group can be set
                action = 'damage'
 
            return (m, action, m.group('data').strip(), client, target)
        elif '------' not in line:
            self.verbose('XLR--------> line did not match format: %s' % line)
 
#---------------------------------------------------------------------------------------------------
 
 
    def OnClientconnect(self, action, data, match=None):
        client = self.clients.getByCID(data)
        self.debug('OnClientConnect: %s, %s' % (data, client))
        return b3.events.Event(b3.events.EVT_CLIENT_JOIN, None, client)
 
    # Parse Userinfo
    def OnClientuserinfochanged(self, action, data, match=None):
        if data is None: # if the client disconnected and we are trying to force the server to give us an id, we end up with an empty data object, so we just return and everything should be fine (the slot should already be removed ln 336)
            return
        bclient = self.parseUserInfo(data)
        self.verbose('Parsed user info %s' % bclient)
        if bclient:
            cid = bclient['cid']
 
            if cid in self._connectingSlots:
                self.debug('client on slot %s is already being connected' % cid)
                return
 
            self._connectingSlots.append(cid)
            client = self.clients.getByCID(cid)
 
            if client:
                # update existing client
                for k, v in bclient.iteritems():
                    setattr(client, k, v)
            else:
                if not bclient.has_key('name'):
                    bclient['name'] = self._empty_name_default
 
                if bclient.has_key('guid'):
                    guid = bclient['guid']
                else:
                    if bclient.has_key('skill'):
                        guid = 'BOT-' + str(cid)
                        self.verbose('BOT connected!')
                        self.clients.newClient(cid, name=bclient['name'], ip='0.0.0.0', state=b3.STATE_ALIVE, guid=guid, data={ 'guid' : guid }, team=bclient['team'], money=20)
                        self._connectingSlots.remove(cid)
                        return None
                    else:
                        self.info('we are missing the guid but this is not a bot either, dumpuser')
                        self._connectingSlots.remove(cid)
                        self.OnClientuserinfochanged(None, self.queryClientUserInfoByCid(cid))
                        return
 
                if not bclient.has_key('ip'):
                    infoclient = self.parseUserInfo(self.queryClientUserInfoByCid(cid))
                    if 'ip' in infoclient:
                        bclient['ip'] = infoclient['ip']
                    else:
                        self.warning('failed to get client ip')
 
                if bclient.has_key('ip'):
                    self.clients.newClient(cid, name=bclient['name'], ip=bclient['ip'], state=b3.STATE_ALIVE, guid=guid, data={ 'guid' : guid }, team=bclient['team'], money=20)
                else:
                    self.warning('failed to get connect client')
 
            self._connectingSlots.remove(cid)
 
        return None
 
    # disconnect
    def OnKill(self, action, data, match=None):
        self.debug('OnKill: %s (%s)'%(match.group('aweap'),match.group('text')))
 
        victim = self.getByCidOrJoinPlayer(match.group('cid'))
        if not victim:
            self.debug('No victim')
            #self.OnClientuserinfochanged(action, data, match)
            return None
 
        weapon = match.group('aweap')
        if not weapon:
            self.debug('No weapon')
            return None
 
        ## Fix attacker
        if match.group('aweap') in self.Suicides:
            # those kills should be considered suicides
            self.debug('OnKill: Fixed attacker, suicide detected: %s' %match.group('text'))
            attacker = victim
        else:
            attacker = self.getByCidOrJoinPlayer(match.group('acid'))
        ## end fix attacker
 
        if not attacker:
            self.debug('No attacker')
            return None
 
        dType = match.group('text').split()[-1:][0]
        if not dType:
            self.debug('No damageType, weapon: %s' % weapon)
            return None
 
        event = b3.events.EVT_CLIENT_KILL
 
        # fix event for team change and suicides and tk
        if attacker.cid == victim.cid:
            event = b3.events.EVT_CLIENT_SUICIDE
        elif attacker.team != b3.TEAM_UNKNOWN and attacker.team != b3.TEAM_FREE and attacker.team == victim.team:
            event = b3.events.EVT_CLIENT_KILL_TEAM
 
        # if not defined we need a general hitloc (for xlrstats)
        if not hasattr(victim, 'hitloc'):
            victim.hitloc = 'body'
 
        victim.state = b3.STATE_DEAD
        #self.verbose('OnKill Victim: %s, Attacker: %s, Weapon: %s, Hitloc: %s, dType: %s' % (victim.name, attacker.name, weapon, victim.hitloc, dType))
        # need to pass some amount of damage for the teamkill plugin - 100 is a kill
        return b3.events.Event(event, (100, weapon, victim.hitloc, dType), attacker, victim)
 
    def OnClientdisconnect(self, action, data, match=None):
        client = self.clients.getByCID(data)
        if client: client.disconnect()
        return None
 
    # startgame
    def OnInitgame(self, action, data, match=None):
        self.debug('OnInitgame: %s' % data)
        options = re.findall(r'\\([^\\]+)\\([^\\]+)', data)
 
        for o in options:
            if o[0] == 'mapname':
                self.game.mapName = o[1]
            elif o[0] == 'g_gametype':
                self.game.gameType = self.defineGameType(o[1])
            elif o[0] == 'fs_game':
                self.game.modName = o[1]
            else:
                #self.debug('%s = %s' % (o[0],o[1]))
                setattr(self.game, o[0], o[1])
 
        self.verbose('...self.console.game.gameType: %s' % self.game.gameType)
        self.game.startRound()
 
        self.debug('Synchronizing client info')
        self.clients.sync()
 
        return b3.events.Event(b3.events.EVT_GAME_ROUND_START, self.game)
 
 
    def OnSayteam(self, action, data, match=None):
        # Teaminfo does not exist in the sayteam logline, so we can't know in which team the user is in. So we set him in a -1 void team.
        client = self.clients.getByExactName(match.group('name'))
 
        if not client:
            self.verbose('No Client Found')
            return None
 
        data = match.group('text')
        client.name = match.group('name')
        return b3.events.Event(b3.events.EVT_CLIENT_TEAM_SAY, data, client, -1)
 
 
    def OnTell(self, action, data, match=None):
        #5:27 tell: woekele to XLR8or: test
 
        client = self.clients.getByExactName(match.group('name'))
        tclient = self.clients.getByExactName(match.group('aname'))
 
        if not client:
            self.verbose('No Client Found')
            return None
 
        data = match.group('text')
        if data and ord(data[:1]) == 21:
            data = data[1:]
 
        client.name = match.group('name')
        return b3.events.Event(b3.events.EVT_CLIENT_PRIVATE_SAY, data, client, tclient)
 
    # Action
    def OnAction(self, cid, actiontype, data, match=None):
        #Need example
        client = self.clients.getByCID(cid)
        if not client:
            self.debug('No client found')
            return None
        self.verbose('OnAction: %s: %s %s' % (client.name, actiontype, data) )
        return b3.events.Event(b3.events.EVT_CLIENT_ACTION, actiontype, client)
 
    def OnItem(self, action, data, match=None):
        client = self.getByCidOrJoinPlayer(match.group('cid'))
        if client:
            return b3.events.Event(b3.events.EVT_CLIENT_ITEM_PICKUP, match.group('data'), client)
        return None
 
    def OnCtf(self, action, data, match=None):
        # 1:25 CTF: 1 2 2: Sarge returned the BLUE flag!
        # 1:16 CTF: 1 1 3: Sarge fragged RED's flag carrier!
        # 6:55 CTF: 2 1 2: Burpman returned the RED flag!
        # 7:02 CTF: 2 2 1: Burpman captured the BLUE flag!
        # 2:12 CTF: 3 1 0: Tanisha got the RED flag!
        # 2:12 CTF: 3 2 0: Tanisha got the BLUE flag!
 
        cid = match.group('cid')
        client = self.getByCidOrJoinPlayer(match.group('cid'))
        flagteam = self.getTeam(match.group('fid'))
        flagcolor = match.group('color')
        action_types = {
            '0': 'flag_taken',
            '1': 'flag_captured',
            '2': 'flag_returned',
            '3': 'flag_carrier_kill',
        }
        try:
            action_id = action_types[match.group('type')]
        except KeyError:
            action_id = 'flag_action_' + match.group('type')
            self.debug('unknown CTF action type: %s (%s)' % (match.group('type'), match.group('data')))
        self.debug('CTF Event: %s from team %s %s by %s' %(action_id, flagcolor, flagteam, client.name))
        if action_id == 'flag_returned':
            return b3.events.Event(b3.events.EVT_GAME_FLAG_RETURNED, flagcolor)
        else:
            return self.OnAction(cid, action_id, data)
            #return b3.events.Event(b3.events.EVT_CLIENT_ACTION, action_id, client)
 
    def OnAward(self, action, data, match=None):
        ## Award: <cid> <awardtype>: <name> gained the <awardname> award!
        # 7:02 Award: 2 4: Burpman gained the CAPTURE award!
        # 7:02 Award: 2 5: Burpman gained the ASSIST award!
        # 7:30 Award: 2 3: Burpman gained the DEFENCE award!
        # 29:15 Award: 2 2: SalaManderDragneL gained the IMPRESSIVE award!
        # 32:08 Award: 2 1: SalaManderDragneL gained the EXCELLENT award!
        # 8:36 Award: 10 1: Karamel is a fake gained the EXCELLENT award!
        client = self.getByCidOrJoinPlayer(match.group('cid'))
        action_type = 'award_%s' % match.group('awardname')
        return b3.events.Event(b3.events.EVT_CLIENT_ACTION, action_type, client)
 
 
#---------------------------------------------------------------------------------------------------
 
    def parseUserInfo(self, info):
        #ClientUserinfoChanged: 0 n\Courgette\t\0\model\sarge/classic\hmodel\sarge/classic\g_redteam\\g_blueteam\\c1\2\c2\7\hc\100\w\0\l\0\tt\0\tl\0\id\201AB4BBC40B4EC7445B49CE82D209EC
        playerID, info = string.split(info, ' ', 1)
 
        if info[:1] != '\\':
            info = '\\' + info
 
        options = re.findall(r'\\([^\\]+)\\([^\\]+)', info)
 
        data = {}
        for o in options:
            data[o[0]] = o[1]
 
        data['cid'] = playerID
 
        if data.has_key('n'):
            data['name'] = data['n']
 
        t = -1
        if data.has_key('team'):
            t = data['team']
        elif data.has_key('t'):
            t = data['t']
 
        data['team'] = self.getTeam(t)
 
 
        if data.has_key('id'):
            data['guid'] = data['id']
            del data['id']
        if data.has_key('cl_guid'):
            data['guid'] = data['cl_guid']
 
        return data
 
 
    def getTeam(self, team):
        team = str(team).lower() # We convert to a string and lower the case because there is a problem when trying to detect numbers if it's not a string (weird)
        if team == 'free' or team == '0':
            #self.debug('Team is Free (no team)')
            result = b3.TEAM_FREE
        elif team == 'red' or team == '1':
            #self.debug('Team is Red')
            result = b3.TEAM_RED
        elif team == 'blue' or team == '2':
            #self.debug('Team is Blue')
            result = b3.TEAM_BLUE
        elif team == 'spectator' or team == '3':
            #self.debug('Team is Spectator')
            result = b3.TEAM_SPEC
        else:
            #self.debug('Team is Unknown')
            result = b3.TEAM_UNKNOWN
 
        #self.debug('getTeam(%s) -> %s' % (team, result))
        return result
 
    # Translate the gameType to a readable format (also for teamkill plugin!)
    def defineGameType(self, gameTypeInt):
 
        _gameType = ''
        _gameType = str(gameTypeInt)
        #self.debug('gameTypeInt: %s' % gameTypeInt)
 
        if gameTypeInt == '0':
            _gameType = 'dm'        # Free for all
        elif gameTypeInt == '1':
            _gameType = 'du'        # Tourney
        elif gameTypeInt == '3':
            _gameType = 'tdm'       # Team Deathmatch
        elif gameTypeInt == '4':
            _gameType = 'ctf'        # Capture The Flag
        elif gameTypeInt == '8':
            _gameType = 'el'        # Elimination
        elif gameTypeInt == '9':
            _gameType = 'ctfel'        # CTF Elimination
        elif gameTypeInt == '10':
            _gameType = 'lms'        # Last Man Standing
        elif gameTypeInt == '11':
            _gameType = 'del'        # Double Domination
        elif gameTypeInt == '12':
            _gameType = 'dom'        # Domination
 
        #self.debug('_gameType: %s' % _gameType)
        return _gameType
 
    def getMaps(self):
        if self._maplist is not None:
            return self._maplist
 
        data = self.write('fdir *.bsp')
        if not data:
            return []
 
        mapregex = re.compile(r'^maps/(?P<map>.+)\.bsp$', re.I)
        maps = []
        for line in data.split('\n'):
            m = re.match(mapregex, line.strip())
            if m:
                if m.group('map'):
                    maps.append(m.group('map'))
 
        return maps
 
    def getNextMap(self):
        data = self.write('nextmap')
        nextmap = self.findNextMap(data)
        if nextmap:
            return nextmap
        else:
            return 'no nextmap set or it is in an unrecognized format !'
 
    def findNextMap(self, data):
        # "nextmap" is: "vstr next4; echo test; vstr aupo3; map oasago2"
        # the last command in the line is the one that decides what is the next map
        # in a case like : map oasago2; echo test; vstr nextmap6; vstr nextmap3
        # the parser will recursively look each last vstr var, and if it can't find a map, fallback to the last map command
        self.debug('Extracting nextmap name from: %s' % (data))
        nextmapregex = re.compile(r'.*("|;)\s*((?P<vstr>vstr (?P<vstrnextmap>[a-z0-9_]+))|(?P<map>map (?P<mapnextmap>[a-z0-9_]+)))', re.IGNORECASE)
        m = re.match(nextmapregex, data)
        if m:
            if m.group('map'):
                self.debug('Found nextmap: %s' % (m.group('mapnextmap')))
                return m.group('mapnextmap')
            elif m.group('vstr'):
                self.debug('Nextmap is redirecting to var: %s' % (m.group('vstrnextmap')))
                data = self.write(m.group('vstrnextmap'))
                result = self.findNextMap(data) # recursively dig into the vstr vars to find the last map called
                if result: # if a result was found in a deeper level, then we return it to the upper level, until we get back to the root level
                    return result
                else: # if none could be found, then try to find a map command in the current string
                    nextmapregex = re.compile(r'.*("|;)\s*(?P<map>map (?P<mapnextmap>[a-z0-9_]+))"', re.IGNORECASE)
                    m = re.match(nextmapregex, data)
                    if m.group('map'):
                        self.debug('Found nextmap: %s' % (m.group('mapnextmap')))
                        return m.group('mapnextmap')
                    else: # if none could be found, we go up a level by returning None (remember this is done recursively)
                        self.debug('No nextmap found in this string !')
                        return None
        else:
            self.debug('No nextmap found in this string !')
            return None
 
    def rotateMap(self):
        """\
        load the next map/level
        """
        self.write('vstr nextmap')
 
    def ban(self, client, reason='', admin=None, silent=False, *kwargs):
        self.debug('BAN : client: %s, reason: %s', client, reason)
        if isinstance(client, b3.clients.Client) and not client.guid:
            # client has no guid, kick instead
            return self.kick(client, reason, admin, silent)
        elif isinstance(client, str) and re.match('^[0-9]+$', client):
            self.write(self.getCommand('ban', cid=client, reason=reason))
            return
        elif not client.id:
            # no client id, database must be down, do tempban
            self.error('Q3AParser.ban(): no client id, database must be down, doing tempban')
            return self.tempban(client, reason, '1d', admin, silent)
 
        if admin:
            fullreason = self.getMessage('banned_by', self.getMessageVariables(client=client, reason=reason, admin=admin))
        else:
            fullreason = self.getMessage('banned', self.getMessageVariables(client=client, reason=reason))
 
        if client.cid is None:
            # ban by ip, this happens when we !permban @xx a player that is not connected
            self.debug('EFFECTIVE BAN : %s',self.getCommand('banByIp', ip=client.ip, reason=reason))
            self.write(self.getCommand('banByIp', ip=client.ip, reason=reason))
        else:
            # ban by cid
            self.debug('EFFECTIVE BAN : %s',self.getCommand('ban', cid=client.cid, reason=reason))
            self.write(self.getCommand('ban', cid=client.cid, reason=reason))
 
        if not silent and fullreason != '':
            self.say(fullreason)
 
        self.queueEvent(b3.events.Event(b3.events.EVT_CLIENT_BAN, {'reason': reason, 'admin': admin}, client))
        client.disconnect()
 
    def unban(self, client, reason='', admin=None, silent=False, *kwargs):
        data = self.write(self.getCommand('banlist', cid=-1))
        if not data:
            self.debug('Error : unban cannot be done, no ban list returned')
        else:
            for line in data.split('\n'):
                m = re.match(self._reBanList, line.strip())
                if m:
                    if m.group('ip') == client.ip:
                        self.write(self.getCommand('unbanByIp', cid=m.group('cid'), reason=reason))
                        self.debug('EFFECTIVE UNBAN : %s',self.getCommand('unbanByIp', cid=m.group('cid')))
 
                if admin:
                    fullreason = self.getMessage('unbanned_by', self.getMessageVariables(client=client, reason=reason, admin=admin))
                else:
                    fullreason = self.getMessage('unbanned', self.getMessageVariables(client=client, reason=reason))
 
                if not silent and fullreason != '':
                    self.say(fullreason)
 
    def getPlayerPings(self):
        data = self.write('status')
        if not data:
            return {}
 
        players = {}
        for line in data.split('\n'):
            m = re.match(self._regPlayer, line.strip())
            if m:
                if m.group('ping') == 'ZMBI':
                    # ignore them, let them not bother us with errors
                    pass
                else:
                    players[str(m.group('slot'))] = int(m.group('ping'))
 
        return players
 
    def sync(self):
        plist = self.getPlayerList()
        mlist = {}
 
        for cid, c in plist.iteritems():
            client = self.getByCidOrJoinPlayer(cid)
            if client:
                if client.guid and c.has_key('guid'):
                    if client.guid == c['guid']:
                        # player matches
                        self.debug('in-sync %s == %s', client.guid, c['guid'])
                        mlist[str(cid)] = client
                    else:
                        self.debug('no-sync %s <> %s', client.guid, c['guid'])
                        client.disconnect()
                elif client.ip and c.has_key('ip'):
                    if client.ip == c['ip']:
                        # player matches
                        self.debug('in-sync %s == %s', client.ip, c['ip'])
                        mlist[str(cid)] = client
                    else:
                        self.debug('no-sync %s <> %s', client.ip, c['ip'])
                        client.disconnect()
                else:
                    self.debug('no-sync: no guid or ip found.')
 
        return mlist
 
    def connectClient(self, ccid):
        players = self.getPlayerList()
        self.verbose('connectClient() = %s' % players)
 
        for cid, p in players.iteritems():
            #self.debug('cid: %s, ccid: %s, p: %s' %(cid, ccid, p))
            if int(cid) == int(ccid):
                self.debug('Client found in status/playerList')
                return p
 
 
    def getByCidOrJoinPlayer(self, cid):
        client = self.clients.getByCID(cid)
        if client:
            return client
        else:
            userinfostring = self.queryClientUserInfoByCid(cid)
            if userinfostring:
                self.OnClientuserinfochanged(None, userinfostring)
            return self.clients.getByCID(cid)
 
 
    def queryClientUserInfoByCid(self, cid):
        """
        : dumpuser 5
        Player 5 is not on the server
 
        ]\rcon dumpuser 0
        userinfo
        --------
        ip                  81.56.143.41
        cg_cmdTimeNudge     0
        cg_delag            0
        cg_scorePlums       1
        cl_voip             0
        cg_predictItems     1
        cl_anonymous        0
        sex                 male
        handicap            100
        color2              7
        color1              2
        team_headmodel      sarge/classic
        team_model          sarge/classic
        headmodel           sarge/classic
        model               sarge/classic
        snaps               20
        rate                25000
        name                Courgette
        teamtask            0
        cl_guid             201AB4BBC40B4EC7445B49CE82D209EC
        teamoverlay         0
        """
        data = self.write('dumpuser %s' % cid)
        if not data:
            return None
 
        if data.split('\n')[0] != "userinfo":
            self.debug("dumpuser %s returned : %s" % (cid, data))
            return None
 
        datatransformed = "%s " % cid
        for line in data.split('\n'):
            if line.strip() == "userinfo" or line.strip() == "--------":
                continue
 
            var = line[:20].strip()
            val = line[20:].strip()
            datatransformed += "\\%s\\%s" % (var, val)
 
        return datatransformed