#
# ioUrT Parser for BigBrotherBot(B3) (www.bigbrotherbot.net)
# Copyright (C) 2008 Mark Weirath (xlr8or@xlr8or.com)
#
# 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
# v1.0.3 - Courgette added support for banlist.txt
#          xlr8or added parsing Damage (OnHit)
# v1.0.4 - xlr8or added EVT_CLIENT_TEAM_CHANGE in OnKill
# v1.0.5 - xlr8or added hitloc and damageType info to accomodate XLRstats
# v1.0.6 - Fixed a bug where the parser wouldn't parse the shutdowngame and warmup functions
# v1.0.7 - Better synchronizing and identification of connecting players and zombies
# v1.0.8 - Better Zombie handling (Zombies being a result of: sv_zombietime (default 2 seconds))
#          (Zombie time is the time after a disconnect that the slot cannot be used and thus is in Zombie state)
#          Added functionality to use ip's only, not using the guid at all (experimental)
# v1.0.9 - Try to get the map name at start
#           Provide getPlayerScores method
# v1.0.10 - Modified _reColor so name sanitation is the same as UrT. Here it does more than just remove color.
# v1.0.11 - Courgette - Add getScores  # NOTE: this won't work properly if the server has private slots. see http://forums.urbanterror.net/index.php/topic,9356.0.html
# v1.0.12 - Courgette - Fix regex that failed to parse chat lines when player's name ends with ':'
# v1.0.13 - xlr8or - support for !maps and !nextmap command
# v1.0.14 - xlr8or - better understanding of mapcycle.txt
# v1.0.15 - mindriot - 01-Nov-2008
# * client with empty name ("") resulted in error and B3 not registering client - now given _empty_name_default
# v1.0.16 - xlr8or - added IpCombi. Setting True will replace the last part of the guid with two segments of the ip
#                    Increases security on admins who have cl_guidServerUniq set to 0 in client config (No cloning).
# v1.0.17 - mindriot - 02-Nov-2008
# * _empty_name_default now only given upon client connect, due to possibility of no name specified in ClientUserinfo at any time
# v1.0.19 - xlr8or - Disabled PunkBuster default settings due to recent supportrequests in the forums with missing PB line in b3.xml
#
# v1.1.0 - xlr8or - Added Action Mechanism (event) for B3 v1.1.5+
# v1.1.1 - courgette
# * Debugged Action Mechanism (event) for B3 v1.1.5+
# v1.2.0 - 19/08/2009 - Courgette
# * adds slap, nuke, mute new custom penalty types (can be used in censor or admin plugin)
# * requires admin plugin v1.4+ and parser.py v1.10+
# v1.3.0 - 20/10/2009 - Courgette
# * upon bot start, already connected players are correctly recognized
# v1.4.0 - 26/10/2009 - Courgette
# * when no client is found by cid, try to join the player using /rcon dumpuser <cid>
# v1.5.0 - 11/11/2009 - Courgette
#    * create a new event: EVT_GAME_FLAG_RETURNED which is fired when the flag return because of time
#    * code refactoring
# v1.5.1 - 17/11/2009 - Courgette
#    * harden getNextMap by :
#      o wrapping initial getCvar queries with try:except bloc
#      o requerying required cvar if missing
#      o forcing map list refresh on server reload or round end
# v1.5.2 - 26/11/2009 - Courgette
#    * fix a bug that prevented kills by slap or nuke from firing kill events
# v1.6.1 - 30/11/2009 - Courgette
#    * separate parsing of lines ClientUserInfo and ClientUserInfoChanged to better translate 
#    ClientUserInfoChanged data. Also OnClientUserInfoChanged does not create new client if 
#    cid is unknown.
# v1.6.2 - 05/12/2009 - Courgette
#    * fix _rePlayerScore regexp
#    * on startup, also try to get players' team (which is not given by dumpuser)
# v1.6.3 - 06/12/2009 - Courgette
#    * harden queryClientUserInfoByCid making sure we got a positive response. (Never trust input data...)
#    * fix _rePlayerScore regexp again
# v1.6.4 - 06/12/2009 - Courgette
#    * sync() will retries to get player list up to 4 for times before giving up as
#      sync() after map change too often fail 2 times.
# v1.6.5 - 09/12/2009 - Courgette
#    * different handling of 'name' in OnClientuserinfo. Now log looks less worrying
#    * prevent exception on the rare case where a say line shows no text after cid (hence no regexp match)
# v1.7 - 21/12/2009 - Courgette
#    * add new UrT specific event : EVT_CLIENT_GEAR_CHANGE
# v1.7.1 - 30/12/2009 - Courgette
#    * Say, Sayteam and Saytell lines do not trigger name change anymore and detect the UrT bug described
#      in http://www.bigbrotherbot.net/forums/urt/b3-bot-sometimes-mix-up-client-id%27s/ . Hopefully this
#      definitely fixes the wrong aliases issue.
# v1.7.2 - 30/12/2009 - Courgette
#    * improve say lines slot bug detection for cases where no player exists on slot 0. 
#      Refactor detection code to follow the KISS rule (keep it simple and stupid)
# v1.7.3 - 31/12/2009 - Courgette
#    * fix bug getting client by name when UrT slot 0 bug 
#    * requires clients.py 1.2.8+
# v1.7.4 - 02/01/2010 - Courgette
#    * improve Urt slot bug workaround as it appears it can occur with slot num different than 0
# v1.7.5 - 05/01/2010 - Courgette
#    * fix minor bug in saytell
# v1.7.6 - 16/01/2010 - xlr8or
#    * removed maxRetries=4 keyword from getPlayerList()
# v1.7.7 - 16/01/2010 - Courgette
#    * put back maxRetries=4 keyword from getPlayerList(). @xlr8or: make sure you have the latest
#      q3a.py file (v1.3.1+) for maxRetries to work.
# v1.7.8 - 18/01/2010 - Courgette
#    * update getPlayerList and sync so that connecting players (CNCT) are not ignored.
#      This will allow to use commands like !ci or !kick on hanging players.
# v1.7.9 - 26/01/2010 - xlr8or
#    * moved getMap() to q3a.py
# v1.7.10 - 10/04/2010 - Bakes
#    * bigsay() function can be used by plugins.
# v1.7.11 - 15/04/2010 - Courgette
#    * add debugging info for getNextMap()
# v1.7.12 - 28/05/2010 - xlr8or
#    * connect bots
# v1.7.13 - 07/11/2010 - GrosBedo
#    * messages now support named $variables instead of %s
# v1.7.14 - 08/11/2010 - GrosBedo
#    * messages can now be empty (no message broadcasted on kick/tempban/ban/unban)
# v1.7.15 - 21/12/2010 - SGT
#    * fix CNCT ping error in getPlayersPings
#    * fix incorrect game type for ffa
#    * move getMapList after game initialization
# v1.7.16 - 09/04/2011 - Courgette
#    * reflect that cid are not converted to int anymore in the clients module
# v1.7.17 - 03/05/2011 - Courgette
#     * reflect changes in inflictCustomPenalty method signature
# v1.8.0 - 31/05/2011 - Courgette
#     * Damage event now carry correct damage points
#     * Damage event weapon code is now the same as the one used for Kill events
# v1.8.1 / 1.8.2 - 01/06/2011 - Courgette
#     * fix Damage points
#     * when game log provides hit info, Kill event will use last damage points instead of 100
# v1.9.0 - 2011-06-04 - Courgette
#     * makes use of the new pluginsStarted parser hook
# v1.10.0 - 2011-06-05 - Courgette
#     * change data format for EVT_CLIENT_BAN events
# 14/06/2011 - 1.11.0 - Courgette
#     * cvar code moved to q3a AbstractParser
# 12/09/2011 - 1.11.1 - Courgette
#     * EVT_CLIENT_JOIN event is now triggered when player actually join a team
#     * the call to self.clients.sync() that was made each round is now made on game init and in its own thread
# 29/09/2011 - 1.11.2 - Courgette
#     * fix MOD_TELEFRAG attacker on kill event to prevent people from being considered
#       as tkers in such cases.
# 15/10/2011 - 1.11.3 - Courgette
#     * better team recognition of existing players at B3 start
# 15/11/2011 - 1.11.4 - Courgette
#     * players's team get refreshed after unpausing the bot (useful when used with FTP and B3 lose the connection for a while)
# 03/03/2012 - 1.11.5 - SGT
#     * Create Survivor Winner Event
#     * Create Unban event
#     * fix issue with OnSay when something like this come and the match couldn't find the name group, say: 7 -crespino-:
# 08/04/2012 - 1.12 - Courgette
#     * fixes rotatemap() - thanks to Beber888
#     * refactor unban()
#     * changeMap() can now provide suggestions
# 05/05/2012 - 1.13 - Courgette
#     * fixes issue xlr8or/big-brother-bot#87 - missing ip when trying to auth a client crashes the bot
# 19/05/2012 - 1.13.1 - Courgette
#     * fixes issue with kill events when killed by UT_MOD_SLAPPED, UT_MOD_NUKED, MOD_TELEFRAG
# 07/07/2012 - 1.13.2 - Courgette
#     * ensures the config file has option 'game_log' in section 'server'
# 12/08/2012 - 1.13.3 - Courgette
#     * fix !nextmap bug when the mapcycle file contains empty lines
# 19/10/2012 - 1.14 - Courgette
#     * improve finding the exact map in getMapsSoundingLike. Also improves changeMap() behavior as a consequence
# 26/11/2012 - 1.15 - Courgette
#     * protect some of the Client object property
#
__author__  = 'xlr8or, Courgette'
__version__ = '1.15'
 
import re, string, time, os, thread
from b3.parsers.q3a.abstractParser import AbstractParser
from b3.functions import soundex, levenshteinDistance, getStuffSoundingLike
import b3
import b3.events
 
#----------------------------------------------------------------------------------------------------------------------------------------------
class Iourt41Parser(AbstractParser):
    gameName = 'iourt41'
    IpsOnly = False
    IpCombi = False
    _maplist = None
 
    _settings = {}
    _settings['line_length'] = 65
    _settings['min_wrap_length'] = 100
 
    _empty_name_default = 'EmptyNameDefault'
 
    _commands = {}
    _commands['broadcast'] = '%(prefix)s^7 %(message)s'
    _commands['message'] = 'tell %(cid)s %(prefix)s ^3[pm]^7 %(message)s'
    _commands['deadsay'] = 'tell %(cid)s %(prefix)s [DEAD]^7 %(message)s'
    _commands['say'] = 'say %(prefix)s %(message)s'
    _commands['saybig'] = 'bigtext "%(prefix)s %(message)s"'
 
    _commands['set'] = 'set %(name)s "%(value)s"'
    _commands['kick'] = 'clientkick %(cid)s'
    _commands['ban'] = 'addip %(cid)s'
    _commands['tempban'] = 'clientkick %(cid)s'
    _commands['banByIp'] = 'addip %(ip)s'
    _commands['unbanByIp'] = 'removeip %(ip)s'
    _commands['slap'] = 'slap %(cid)s'
    _commands['nuke'] = 'nuke %(cid)s'
    _commands['mute'] = 'mute %(cid)s %(seconds)s'
 
    _eventMap = {
        #'warmup' : b3.events.EVT_GAME_WARMUP,
        #'shutdowngame' : b3.events.EVT_GAME_ROUND_END
    }
 
    # remove the time off of the line
    _lineClear = re.compile(r'^(?:[0-9:]+\s?)?')
    #0:00 ClientUserinfo: 0:
 
    _lineFormats = (
        #Generated with ioUrbanTerror v4.1:
        #Hit: 12 7 1 19: BSTHanzo[FR] hit ercan in the Helmet
        #Hit: 13 10 0 8: Grover hit jacobdk92 in the Head
        re.compile(r'^(?P<action>[a-z]+):\s(?P<data>(?P<cid>[0-9]+)\s(?P<acid>[0-9]+)\s(?P<hitloc>[0-9]+)\s(?P<aweap>[0-9]+):\s+(?P<text>.*))$', re.IGNORECASE),
        #re.compile(r'^(?P<action>[a-z]+):\s(?P<data>(?P<cid>[0-9]+)\s(?P<acid>[0-9]+)\s(?P<hitloc>[0-9]+)\s(?P<aweap>[0-9]+):\s+(?P<text>(?P<aname>[^:])\shit\s(?P<name>[^:])\sin\sthe(?P<locname>.*)))$', re.IGNORECASE),
 
        #6:37 Kill: 0 1 16: XLR8or killed =lvl1=Cheetah by UT_MOD_SPAS
        #2:56 Kill: 14 4 21: Qst killed Leftovercrack by UT_MOD_PSG1
        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),
        #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>(?P<aname>[^:])\skilled\s(?P<name>[^:])\sby\s(?P<modname>.*)))$', re.IGNORECASE),
 
        #Processing chats and tell events...
        #5:39 saytell: 15 16 repelSteeltje: nno
        #5:39 saytell: 15 15 repelSteeltje: nno
        re.compile(r'^(?P<action>[a-z]+):\s(?P<data>(?P<cid>[0-9]+)\s(?P<acid>[0-9]+)\s(?P<name>[^ ]+):\s+(?P<text>.*))$', re.IGNORECASE),
 
        # We're not using tell in this form so this one is disabled
        #5:39 tell: repelSteeltje to B!K!n1: nno
        #re.compile(r'^(?P<action>[a-z]+):\s+(?P<data>(?P<name>[^:]+)\s+to\s+(?P<aname>[^:]+):\s+(?P<text>.*))$', re.IGNORECASE),
 
        #3:53 say: 8 denzel: lol
        #15:37 say: 9 .:MS-T:.BstPL: this name is quite a challenge
        #2:28 sayteam: 12 New_UrT_Player_v4.1: woekele
        #16:33 Flag: 2 0: team_CTF_redflag
        #re.compile(r'^(?P<action>[a-z]+):\s(?P<data>(?P<cid>[0-9]+)\s(?P<name>[^ ]+):\s+(?P<text>.*))$', re.IGNORECASE),
        # SGT: fix issue with OnSay when something like this come and the match could'nt find the name group
        # say: 7 -crespino-:
        re.compile(r'^(?P<action>[a-z]+):\s(?P<data>(?P<cid>[0-9]+)\s(?P<name>[^ ]+):\s*(?P<text>.*))$', re.IGNORECASE),
 
        #15:42 Flag Return: RED
        #15:42 Flag Return: BLUE
        re.compile(r'^(?P<action>Flag Return):\s(?P<data>(?P<color>.+))$', re.IGNORECASE),
 
        #Bombmode actions:
        #3:06 Bombholder is 2
        re.compile(r'^(?P<action>Bombholder)(?P<data>\sis\s(?P<cid>[0-9]))$', re.IGNORECASE),
        #was planted, was defused, was tossed, has been collected (doh, how gramatically correct!)
        #2:13 Bomb was tossed by 2
        #2:32 Bomb was planted by 2
        #3:01 Bomb was defused by 3!
        #2:17 Bomb has been collected by 2
        re.compile(r'^(?P<action>Bomb)\s(?P<data>(was|has been)\s(?P<subaction>[a-z]+)\sby\s(?P<cid>[0-9]+).*)$', re.IGNORECASE),
 
        #Falling thru? Item stuff and so forth
        re.compile(r'^(?P<action>[a-z]+):\s(?P<data>.*)$', re.IGNORECASE),
        #Shutdowngame and Warmup... the one word lines
        re.compile(r'^(?P<action>[a-z]+):$', re.IGNORECASE)
    )
 
    # map: ut4_casa
    # num score ping name            lastmsg address               qport rate
    # --- ----- ---- --------------- ------- --------------------- ----- -----
    #   2     0   19 ^1XLR^78^8^9or^7        0 145.99.135.227:27960  41893  8000  # player with a live ping
    #   4     0 CNCT Dz!k^7                450 83.175.191.27:64459   50308 20000  # connecting player (or inbetween rounds)
    #   9     0 ZMBI ^7                   1900 81.178.80.68:27960    10801  8000  # zombies (need to be disconnected!)
    _regPlayer = re.compile(r'^(?P<slot>[0-9]+)\s+(?P<score>[0-9-]+)\s+(?P<ping>[0-9]+|CNCT|ZMBI)\s+(?P<name>.*?)\s+(?P<last>[0-9]+)\s+(?P<ip>[0-9.]+):(?P<port>[0-9-]+)\s+(?P<qport>[0-9]+)\s+(?P<rate>[0-9]+)$', re.I)
    _reColor = re.compile(r'(\^.)|[\x00-\x20]|[\x7E-\xff]')
 
    # Map: ut4_algiers
    # Players: 8
    # Scores: R:97 B:98
    # 0:  FREE k:0 d:0 ping:0
    # 4: yene RED k:16 d:8 ping:50 92.104.110.192:63496
    _reTeamScores = re.compile(r'^Scores:\s+R:(?P<RedScore>.+)\s+B:(?P<BlueScore>.+)$', re.I)
    _rePlayerScore = re.compile(r'^(?P<slot>[0-9]+): (?P<name>.*) (?P<team>RED|BLUE|SPECTATOR|FREE) k:(?P<kill>[0-9]+) d:(?P<death>[0-9]+) ping:(?P<ping>[0-9]+|CNCT|ZMBI)( (?P<ip>[0-9.]+):(?P<port>[0-9-]+))?$', re.I) # NOTE: this won't work properly if the server has private slots. see http://forums.urbanterror.net/index.php/topic,9356.0.html
 
 
    PunkBuster = None
 
    ## kill modes
    MOD_WATER='1'
    MOD_LAVA='3'
    MOD_TELEFRAG='5'
    MOD_FALLING='6'
    MOD_SUICIDE='7'
    MOD_TRIGGER_HURT='9'
    MOD_CHANGE_TEAM='10'
    UT_MOD_KNIFE='12'
    UT_MOD_KNIFE_THROWN='13'
    UT_MOD_BERETTA='14'
    UT_MOD_DEAGLE='15'
    UT_MOD_SPAS='16'
    UT_MOD_UMP45='17'
    UT_MOD_MP5K='18'
    UT_MOD_LR300='19'
    UT_MOD_G36='20'
    UT_MOD_PSG1='21'
    UT_MOD_HK69='22'
    UT_MOD_BLED='23'
    UT_MOD_KICKED='24'
    UT_MOD_HEGRENADE='25'
    UT_MOD_SR8='28'
    UT_MOD_AK103='30'
    UT_MOD_SPLODED='31'
    UT_MOD_SLAPPED='32'
    UT_MOD_BOMBED='33'
    UT_MOD_NUKED='34'
    UT_MOD_NEGEV='35'
    UT_MOD_HK69_HIT='37'
    UT_MOD_M4='38'
    UT_MOD_FLAG='39'
    UT_MOD_GOOMBA='40'
 
    ## weapons id on Hit: lines are different than the one
    ## on the Kill: lines. Here the translation table
    hitweapon2killweapon = {
        1: UT_MOD_KNIFE,
        2: UT_MOD_BERETTA,
        3: UT_MOD_DEAGLE,
        4: UT_MOD_SPAS,
        5: UT_MOD_MP5K,
        6: UT_MOD_UMP45,
        8: UT_MOD_LR300,
        9: UT_MOD_G36,
        10: UT_MOD_PSG1,
        14: UT_MOD_SR8,
        15: UT_MOD_AK103,
        17: UT_MOD_NEGEV,
        19: UT_MOD_M4,
        21: UT_MOD_HEGRENADE,
        22: UT_MOD_KNIFE_THROWN,
    }
 
    """ From data provided by Garreth http://bit.ly/jf4QXc on http://bit.ly/krwBCv :
 
                                Head(0) Helmet(1)     Torso(2)     Kevlar(3)     Arms(4)    Legs(5)    Body(6)    Killed
    MOD_TELEFRAG='5'             0        0             0             0             0         0         0         0
    UT_MOD_KNIFE='12'           100      60            44            35            20        20        44        100
    UT_MOD_KNIFE_THROWN='13'    100      60            44            35            20        20        44        100
    UT_MOD_BERETTA='14'         100      34            30            20            11        11        30        100
    UT_MOD_DEAGLE='15'          100      66            57            38            22        22        57        100
    UT_MOD_SPAS='16'            25       25            25            25            25        25        25        100
    UT_MOD_UMP45='17'           100      51            44            29            17        17        44        100
    UT_MOD_MP5K='18'            50       34            30            20            11        11        30        100
    UT_MOD_LR300='19'           100      51            44            29            17        17        44        100
    UT_MOD_G36='20'             100      51            44            29            17        17        44        100
    UT_MOD_PSG1='21'            100      63            97            63            36        36        97        100
    UT_MOD_HK69='22'            50       50            50            50            50        50        50        100
    UT_MOD_BLED='23'            15       15            15            15            15        15        15        15
    UT_MOD_KICKED='24'          20       20            20            20            20        20        20        100
    UT_MOD_HEGRENADE='25'       50       50            50            50            50        50        50        100
    UT_MOD_SR8='28'             100      100           100           100           50        50        100       100
    UT_MOD_AK103='30'           100      58            51            34            19        19        51        100
    UT_MOD_NEGEV='35'           50       34            30            20            11        11        30        100
    UT_MOD_HK69_HIT='37'        20       20            20            20            20        20        20        100
    UT_MOD_M4='38'              100      51            44            29            17        17        44        100
    UT_MOD_GOOMBA='40'          100      100           100           100           100       100       100       100
    """
    damage = {
        MOD_TELEFRAG: [0, 0, 0, 0, 0, 0, 0, 0],
        UT_MOD_KNIFE: [100, 60, 44, 35, 20, 20, 44, 100],
        UT_MOD_KNIFE_THROWN: [100, 60, 44, 35, 20, 20, 44, 100],
        UT_MOD_BERETTA: [100, 34, 30, 20, 11, 11, 30, 100],
        UT_MOD_DEAGLE: [100, 66, 57, 38, 22, 22, 57, 100],
        UT_MOD_SPAS: [25, 25, 25, 25, 25, 25, 25, 100],
        UT_MOD_UMP45: [100, 51, 44, 29, 17, 17, 44, 100],
        UT_MOD_MP5K: [50, 34, 30, 20, 11, 11, 30, 100],
        UT_MOD_LR300: [100, 51, 44, 29, 17, 17, 44, 100],
        UT_MOD_G36: [100, 51, 44, 29, 17, 17, 44, 100],
        UT_MOD_PSG1: [100, 63, 97, 63, 36, 36, 97, 100],
        UT_MOD_HK69: [50, 50, 50, 50, 50, 50, 50, 100],
        UT_MOD_BLED: [15, 15, 15, 15, 15, 15, 15, 15],
        UT_MOD_KICKED: [20, 20, 20, 20, 20, 20, 20, 100],
        UT_MOD_HEGRENADE: [50, 50, 50, 50, 50, 50, 50, 100],
        UT_MOD_SR8: [100, 100, 100, 100, 50, 50, 100, 100],
        UT_MOD_AK103: [100, 58, 51, 34, 19, 19, 51, 100],
        UT_MOD_NEGEV: [50, 34, 30, 20, 11, 11, 30, 100],
        UT_MOD_HK69_HIT: [20, 20, 20, 20, 20, 20, 20, 100],
        UT_MOD_M4: [100, 51, 44, 29, 17, 17, 44, 100],
        UT_MOD_GOOMBA: [100, 100, 100, 100, 100, 100, 100, 100],
     }
 
    def startup(self):
        if not self.config.has_option('server','game_log'):
            self.critical("your main config file is missing the 'game_log' setting in section 'server'")
            raise SystemExit(220)
 
        # add UrT specific events
        self.Events.createEvent('EVT_GAME_FLAG_RETURNED', 'Flag returned')
        self.Events.createEvent('EVT_CLIENT_GEAR_CHANGE', 'Client gear change')
        self.Events.createEvent('EVT_SURVIVOR_WIN', 'Survivor Winner')
 
        # add the world client
        self.clients.newClient('-1', guid='WORLD', name='World', hide=True, pbid='WORLD')
 
        # PunkBuster for iourt is not supported!
        #if not self.config.has_option('server', 'punkbuster') or self.config.getboolean('server', 'punkbuster'):
        #    self.PunkBuster = b3.parsers.punkbuster.PunkBuster(self)
 
        # 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:
            self.game.fs_game = self.getCvar('fs_game').getString()
        except:
            self.game.fs_game = 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")
 
        self._maplist = self.getMaps()
 
    def pluginsStarted(self):
        # initialize connected clients
        plist = self.getPlayerList()
        for cid in plist.keys():
            userinfostring = self.queryClientUserInfoByCid(cid)
            if userinfostring:
                self.OnClientuserinfo(None, userinfostring)
 
        player_teams = {}
        tries = 0
        while tries < 3:
            try:
                tries += 1
                player_teams = self.getPlayerTeams()
                break
            except Exception as err:
                if tries < 3:
                    self.warning(err)
                else:
                    self.error("cannot fix players teams : %s" % err) 
        for cid in plist.keys():
            client = self.clients.getByCID(cid)
            if client and client.cid in player_teams:
                newteam = player_teams[client.cid]
                if newteam != client.team:
                    self.debug('Fixing client team for %s : %s is now %s' % (client.name, client.team, newteam))
                    setattr(client, 'team', newteam)
 
    def unpause(self):
        self.pluginsStarted() # so we get teams refreshed
        self.clients.sync()
        b3.parser.Parser.unpause(self)
 
    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('line matched %s' % f.pattern)
                break
 
        if m:
            client = None
            target = None
            try:
                data = m.group('data').strip()
            except:
                data = None
            return (m, m.group('action').lower(), data, client, target)
        elif '------' not in line:
            self.verbose('line did not match format: %s' % line)
 
    def parseUserInfo(self, info):
        """Just extract the cid and pairs of key/value without any treatment"""
        #2 \ip\145.99.135.227:27960\challenge\-232198920\qport\2781\protocol\68\battleye\1\name\[SNT]^1XLR^78or\rate\8000\cg_predictitems\0\snaps\20\model\sarge\headmodel\sarge\team_model\james\team_headmodel\*james\color1\4\color2\5\handicap\100\sex\male\cl_anonymous\0\teamtask\0\cl_guid\58D4069246865BB5A85F20FB60ED6F65
        #7 n\[SNT]^1XLR^78or\t\3\r\2\tl\0\f0\\f1\\f2\\a0\0\a1\0\a2\0
        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
        return data
 
 
    def getTeam(self, team):
        if str(team).lower() == 'red':
            team = 1
        elif str(team).lower() == 'blue':
            team = 2
        elif str(team).lower() == 'spectator':
            team = 3
        elif str(team).lower() == 'free':
            team = -1 # will fall back to b3.TEAM_UNKNOWN
 
        team = int(team)
        if team == 1:
            result = b3.TEAM_RED
        elif team == 2:
            result = b3.TEAM_BLUE
        elif team == 3:
            result = b3.TEAM_SPEC
        else:
            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 = 'ffa'
        elif gameTypeInt == '1':   # Dunno what this one is
            _gameType = 'dm'
        elif gameTypeInt == '2':   # Dunno either
            _gameType = 'dm'
        elif gameTypeInt == '3':
            _gameType = 'tdm'
        elif gameTypeInt == '4':
            _gameType = 'ts'
        elif gameTypeInt == '5':
            _gameType = 'ftl'
        elif gameTypeInt == '6':
            _gameType = 'cah'
        elif gameTypeInt == '7':
            _gameType = 'ctf'
        elif gameTypeInt == '8':
            _gameType = 'bm'
 
        #self.debug('_gameType: %s' % _gameType)
        return _gameType
 
    # self.console.broadcast, a variant on self.console.say in UrT. This will print to upper left, the server message area.
    def broadcast(self, msg):
        lines = []
        for line in self.getWrap(msg, self._settings['line_length'], self._settings['min_wrap_length']):
            lines.append(self.getCommand('broadcast', prefix=self.msgPrefix, message=line))
 
        if len(lines):
            self.writelines(lines)
 
    def inflictCustomPenalty(self, type, client, reason=None, duration=None, admin=None, data=None):
        if type == 'slap' and client:
            cmd = self.getCommand('slap', cid=client.cid)
            self.write(cmd)
            if reason:
                client.message("%s" % reason)
            return True
 
        elif type == 'nuke' and client:
            cmd = self.getCommand('nuke', cid=client.cid)
            self.write(cmd)
            if reason:
                client.message("%s" % reason)
            return True
 
        elif type == 'mute' and client:
            if duration is None:
                seconds = 60
            else:
                seconds = round(float(b3.functions.time2minutes(duration) * 60), 0)
 
            # make sure to unmute first
            cmd = self.getCommand('mute', cid=client.cid, seconds=0)
            self.write(cmd)
            # then mute
            cmd = self.getCommand('mute', cid=client.cid, seconds=seconds)
            self.write(cmd)
            if reason:
                client.message("%s" % reason)
            return True
 
        # elif type == 'morron' and client:
            # client.message('you morron')
            # return True
 
 
 
    ###############################################################################################
    #
    #    Events handlers
    #
    ###############################################################################################
 
    # Connect/Join
    def OnClientconnect(self, action, data, match=None):
        self.debug('Client Connected - ready to parse Userinfoline')
        #client = self.clients.getByCID(data)
        #return b3.events.Event(b3.events.EVT_CLIENT_JOIN, None, client)
 
    def OnClientbegin(self, action, data, match=None):
        # we get user info in two parts:
        # 19:42.36 ClientBegin: 4
        client = self.getByCidOrJoinPlayer(data)
        if client:
            return b3.events.Event(b3.events.EVT_CLIENT_JOIN, data=data, client=client)
 
    # Parse Userinfo
    def OnClientuserinfo(self, action, data, match=None):
        #2 \ip\145.99.135.227:27960\challenge\-232198920\qport\2781\protocol\68\battleye\1\name\[SNT]^1XLR^78or\rate\8000\cg_predictitems\0\snaps\20\model\sarge\headmodel\sarge\team_model\james\team_headmodel\*james\color1\4\color2\5\handicap\100\sex\male\cl_anonymous\0\teamtask\0\cl_guid\58D4069246865BB5A85F20FB60ED6F65
        #conecting bot:
        #0 \gear\GMIORAA\team\blue\skill\5.000000\characterfile\bots/ut_chicken_c.c\color\4\sex\male\race\2\snaps\20\rate\25000\name\InviteYourFriends!
        bclient = self.parseUserInfo(data)
 
        if not bclient.has_key('cl_guid') and bclient.has_key('skill'):
            # must be a bot connecting
            self.bot('Bot Connecting!')
            bclient['ip'] = '0.0.0.0'
            bclient['cl_guid'] = 'BOT' + str(bclient['cid'])
 
        if bclient.has_key('name'):
            # remove spaces from name
            bclient['name'] = bclient['name'].replace(' ','')
 
 
        # split port from ip field
        if bclient.has_key('ip'):
            ipPortData = string.split(bclient['ip'], ':', 1)
            bclient['ip'] = ipPortData[0]
            if len(ipPortData) > 1:
                bclient['port'] = ipPortData[1] 
 
        if bclient.has_key('team'):
            bclient['team'] = self.getTeam(bclient['team'])
 
        if bclient.has_key('cl_guid') and not bclient.has_key('pbid') and self.PunkBuster:
            bclient['pbid'] = bclient['cl_guid']
 
        self.verbose('Parsed user info %s' % bclient)
 
        if bclient:
            client = self.clients.getByCID(bclient['cid'])
 
            if client:
                # update existing client
                for k, v in bclient.iteritems():
                    if hasattr(client, 'gear') and k == 'gear' and client.gear != v:
                        self.queueEvent(b3.events.Event(b3.events.EVT_CLIENT_GEAR_CHANGE, v, client))
                    if not k.startswith('_') and k not in ('login', 'password', 'groupBits', 'maskLevel', 'autoLogin', 'greeting'):
                        setattr(client, k, v)
            else:
                #make a new client
                if self.PunkBuster:
                    # we will use punkbuster's guid
                    guid = None
                else:
                    # use io guid
                    if bclient.has_key('cl_guid'):
                        guid = bclient['cl_guid']
                    else:
                        guid = 'unknown'
 
                # v1.0.17 - mindriot - 02-Nov-2008
                if not bclient.has_key('name'):
                    bclient['name'] = self._empty_name_default
 
                if not bclient.has_key('ip'):
                    if guid == 'unknown':
                        # happens when a client is (temp)banned and got kicked so client was destroyed, but
                        # infoline was still waiting to be parsed.
                        self.debug('Client disconnected. Ignoring.')
                        return None
                    else:
                        # see issue xlr8or/big-brother-bot#87 - ip can be missing
                        try:
                            self.debug("missing IP, trying to get ip with 'status'")
                            plist = self.getPlayerList()
                            client_data = plist[bclient['cid']]
                            bclient['ip'] = client_data['ip']
                        except Exception, err:
                            bclient['ip'] = ''
                            self.warning("Failed to get client %s ip address." % bclient['cid'], err)
 
 
 
                nguid = ''
                # overide the guid... use ip's only if self.console.IpsOnly is set True.
                if self.IpsOnly:
                    nguid = bclient['ip']
                # replace last part of the guid with two segments of the ip
                elif self.IpCombi:
                    i = bclient['ip'].split('.')
                    d = len(i[0])+len(i[1])
                    nguid = guid[:-d]+i[0]+i[1]
                # Quake clients don't have a cl_guid, we'll use ip instead
                elif guid == 'unknown':
                    nguid = bclient['ip']
 
                if nguid != '':
                    guid = nguid
 
                client = self.clients.newClient(bclient['cid'], name=bclient['name'], ip=bclient['ip'], state=b3.STATE_ALIVE, guid=guid, data={ 'guid' : guid })
 
        return None
 
    # when userinfo changes
    def OnClientuserinfochanged(self, action, data, match=None):
        #7 n\[SNT]^1XLR^78or\t\3\r\2\tl\0\f0\\f1\\f2\\a0\0\a1\0\a2\0
        parseddata = self.parseUserInfo(data)
 
        if parseddata:
            client = self.clients.getByCID(parseddata['cid'])
 
            if client:
                # update existing client
                if parseddata.has_key('n'):
                    setattr(client, 'name', parseddata['n'])
 
                if parseddata.has_key('t'):
                    team = self.getTeam(parseddata['t'])
                    setattr(client, 'team', team)
 
                    if parseddata.has_key('r'):
                        if team == b3.TEAM_BLUE:
                            setattr(client, 'raceblue', parseddata['r'])
                        elif team == b3.TEAM_RED:
                            setattr(client, 'racered', parseddata['r'])
                    if parseddata.has_key('f0') and parseddata['f0'] is not None \
                            and parseddata.has_key('f1') and parseddata['f1'] is not None \
                            and parseddata.has_key('f2') and parseddata['f2'] is not None :
                        data = "%s,%s,%s" % (parseddata['f0'], parseddata['f1'], parseddata['f2'])
                        if team == b3.TEAM_BLUE:
                            setattr(client, 'funblue', data)
                        elif team == b3.TEAM_RED:
                            setattr(client, 'funred', data)
 
                if parseddata.has_key('a0') and parseddata.has_key('a1') and parseddata.has_key('a2'):
                    setattr(client, 'cg_rgb', "%s %s %s" % (parseddata['a0'], parseddata['a1'], parseddata['a2']))
 
        return None
 
    # damage
    #Hit: 13 10 0 8: Grover hit jacobdk92 in the Head
    #Hit: cid acid hitloc aweap: text
    def OnHit(self, action, data, match=None):
        victim = self.clients.getByCID(match.group('cid'))
        if not victim:
            self.debug('No victim')
            #self.OnClientuserinfo(action, data, match)
            return None
 
        attacker = self.clients.getByCID(match.group('acid'))
        if not attacker:
            self.debug('No attacker')
            return None
 
        event = b3.events.EVT_CLIENT_DAMAGE
 
        if attacker.cid == victim.cid:
            event = b3.events.EVT_CLIENT_DAMAGE_SELF
        elif attacker.team != b3.TEAM_UNKNOWN and attacker.team == victim.team:
            event = b3.events.EVT_CLIENT_DAMAGE_TEAM
 
        hitloc = match.group('hitloc')
        weapon = self._convertHitWeaponToKillWeapon(match.group('aweap'))
        points = self._getDamagePoints(weapon, hitloc)
        event_data = (points, weapon, hitloc)
        victim.data['lastDamageTaken'] = event_data
        #victim.state = b3.STATE_ALIVE
        # need to pass some amount of damage for the teamkill plugin - 15 seems okay
        return self.getEvent(event, event_data, attacker, victim)
 
    # kill
    #6:37 Kill: 0 1 16: XLR8or killed =lvl1=Cheetah by UT_MOD_SPAS
    #6:37 Kill: 7 7 10: Mike_PL killed Mike_PL by MOD_CHANGE_TEAM
    #kill: acid cid aweap: <text>
    def OnKill(self, action, data, match=None):
        # kill modes caracteristics :
        """
        1:      MOD_WATER === exclusive attackers : , 1022(<world>), 0(<non-client>)
        3:      MOD_LAVA === exclusive attackers : , 1022(<world>), 0(<non-client>)
        5:      MOD_TELEFRAG --- normal kill line
        6:      MOD_FALLING === exclusive attackers : , 1022(<world>), 0(<non-client>)
        7:      MOD_SUICIDE ===> attacker is always the victim
        9:      MOD_TRIGGER_HURT === exclusive attackers : , 1022(<world>)
        10:     MOD_CHANGE_TEAM ===> attacker is always the victim
        12:     UT_MOD_KNIFE --- normal kill line
        13:     UT_MOD_KNIFE_THROWN --- normal kill line
        14:     UT_MOD_BERETTA --- normal kill line
        15:     UT_MOD_DEAGLE --- normal kill line
        16:     UT_MOD_SPAS --- normal kill line
        17:     UT_MOD_UMP45 --- normal kill line
        18:     UT_MOD_MP5K --- normal kill line
        19:     UT_MOD_LR300 --- normal kill line
        20:     UT_MOD_G36 --- normal kill line
        21:     UT_MOD_PSG1 --- normal kill line
        22:     UT_MOD_HK69 --- normal kill line
        23:     UT_MOD_BLED --- normal kill line
        24:     UT_MOD_KICKED --- normal kill line
        25:     UT_MOD_HEGRENADE --- normal kill line
        28:     UT_MOD_SR8 --- normal kill line
        30:     UT_MOD_AK103 --- normal kill line
        31:     UT_MOD_SPLODED ===> attacker is always the victim
        32:     UT_MOD_SLAPPED ===> attacker is always the victim
        33:     UT_MOD_BOMBED --- normal kill line
        34:     UT_MOD_NUKED --- normal kill line
        35:     UT_MOD_NEGEV --- normal kill line
        37:     UT_MOD_HK69_HIT --- normal kill line
        38:     UT_MOD_M4 --- normal kill line
        39:     UT_MOD_FLAG === exclusive attackers : , 0(<non-client>)
        40:     UT_MOD_GOOMBA --- normal kill line
        """
        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.OnClientuserinfo(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.UT_MOD_SLAPPED, self.UT_MOD_NUKED, self.MOD_TELEFRAG):
            self.debug('OnKill: slap/nuke => attacker should be None')
            attacker = self.clients.getByCID('-1') # make the attacker 'World'
        elif match.group('aweap') in (self.MOD_WATER,self.MOD_LAVA,self.MOD_FALLING,self.MOD_TRIGGER_HURT,self.UT_MOD_BOMBED,self.UT_MOD_FLAG):
            # those kills should be considered suicides
            self.debug('OnKill: water/lava/falling/trigger_hurt/bombed/flag should be suicides')
            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:
            if weapon == self.MOD_CHANGE_TEAM:
                """
                Do not pass a teamchange event here. That event is passed
                shortly after the kill.
                """
                self.verbose('Team Change Event Caught, exiting')
                return None
            else:
                event = b3.events.EVT_CLIENT_SUICIDE
        elif attacker.team != b3.TEAM_UNKNOWN and attacker.team == victim.team:
            event = b3.events.EVT_CLIENT_KILL_TEAM
 
        # if not logging damage we need a general hitloc (for xlrstats)
        if 'lastDamageTaken' in victim.data:
            lastDamageData = victim.data['lastDamageTaken']
            del victim.data['lastDamageTaken']
        else:
            lastDamageData = (100, weapon, '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 self.getEvent(event, (lastDamageData[0], weapon, lastDamageData[2], dType), attacker, victim)
 
    # disconnect
    def OnClientdisconnect(self, action, data, match=None):
        client = self.clients.getByCID(data)
        if client: client.disconnect()
        return None
 
#--- Action Mechanism (new in B3 version 1.1.5) --------------------------------
    def OnFlag(self, action, data, match=None):
        #Flag: 1 2: team_CTF_blueflag
        #Flag: <_cid> <_subtype:0/1/2>: <text>
        _cid = match.group('cid')
        _subtype = int(match.group('name'))
        data = match.group('text')
 
        if _subtype == 0:
            _actiontype = 'flag_dropped'
        elif _subtype == 1:
            _actiontype = 'flag_returned'
        elif _subtype == 2:
            _actiontype = 'flag_captured'
        else:
            return None
        return self.OnAction(_cid, _actiontype, data)
 
    def OnFlagReturn(self, action, data, match=None):
        #Flag Return: RED
        #Flag Return: BLUE
        #Flag Return: <color>
        color = match.group('color')
        return b3.events.Event(b3.events.EVT_GAME_FLAG_RETURNED, color)
 
    def OnBomb(self, action, data, match=None):
        _cid = match.group('cid')
        _subaction = match.group('subaction')
        if _subaction == 'planted':
            _actiontype = 'bomb_planted'
        elif _subaction == 'defused':
            _actiontype = 'bomb_defused'
        elif _subaction == 'tossed':
            _actiontype = 'bomb_tossed'
        elif _subaction == 'collected':
            _actiontype = 'bomb_collected'
        else:
            return None
        return self.OnAction(_cid, _actiontype, data)
 
    def OnBombholder(self, action, data, match=None):
        _cid = match.group('cid')
        _actiontype = 'bomb_holder_spawn'
        return self.OnAction(_cid, _actiontype, data)
 
    # 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)
 
    # item
    def OnItem(self, action, data, match=None):
        #Item: 3 ut_item_helmet
        #Item: 0 team_CTF_redflag
        cid, item = string.split(data, ' ', 1)
        client = self.getByCidOrJoinPlayer(cid)
        if client:
            #correct flag/bomb-pickups
            if 'flag' in item or 'bomb' in item:
                self.verbose('Itempickup corrected to action: %s' %item)
                return self.OnAction(cid, item, data)
            #self.verbose('OnItem: %s picked up %s' % (client.name, item) )
            return b3.events.Event(b3.events.EVT_CLIENT_ITEM_PICKUP, item, client)
        return None
 
    # survivor winner
    def OnSurvivorwinner(self, action, data, match=None):
        #SurvivorWinner: Blue
        #SurvivorWinner: Red
        self.debug('EVENT: OnSurvivorwinner')
        return b3.events.Event(b3.events.EVT_SURVIVOR_WIN, data)  
 
#-------------------------------------------------------------------------------
    # say
    def OnSay(self, action, data, match=None):
        #3:53 say: 8 denzel: lol
 
        if match is None:
            return
 
        name = self.stripColors(match.group('name'))
        cid = int(match.group('cid'))
        client = self.getByCidOrJoinPlayer(match.group('cid'))
 
        if not client or client.name != name:
            self.debug('UrT bug spotted. Trying to get client by name')
            client = self.clients.getByName(name)
 
        if not client:
            self.verbose('No Client Found!')
            return None
 
        self.verbose('Client Found: %s on slot %s' % (client.name, client.cid))
 
        data = match.group('text')
 
        #removal of weird characters
        if data and ord(data[:1]) == 21:
            data = data[1:]
 
        return b3.events.Event(b3.events.EVT_CLIENT_SAY, data, client)
 
 
    # sayteam
    def OnSayteam(self, action, data, match=None):
        #2:28 sayteam: 12 New_UrT_Player_v4.1: wokele
        if match is None:
            return
 
        name = self.stripColors(match.group('name'))
        cid = int(match.group('cid'))
        client = self.getByCidOrJoinPlayer(match.group('cid'))
 
        if not client or client.name != name:
            self.debug('UrT bug spotted. Trying to get client by name')
            client = self.clients.getByName(name)
 
        if not client:
            self.verbose('No Client Found!')
            return None
 
        self.verbose('Client Found: %s on slot %s' % (client.name, client.cid))
 
        data = match.group('text')
        if data and ord(data[:1]) == 21:
            data = data[1:]
 
        return b3.events.Event(b3.events.EVT_CLIENT_TEAM_SAY, data, client, client.team)
 
 
    # saytell
    def OnSaytell(self, action, data, match=None):
        #5:39 saytell: 15 16 repelSteeltje: nno
        #5:39 saytell: 15 15 repelSteeltje: nno
 
        #data = match.group('text')
        #if not len(data) >= 2 and not (data[:1] == '!' or data[:1] == '@') and match.group('cid') == match.group('acid'):
        #    return None
 
        if match is None:
            return
 
        name = self.stripColors(match.group('name'))
        cid = int(match.group('cid'))
        client = self.getByCidOrJoinPlayer(match.group('cid'))
        tclient = self.clients.getByCID(match.group('acid'))
 
        if not client or client.name != name:
            self.debug('UrT bug spotted. Trying to get client by name')
            client = self.clients.getByName(name)
 
        if not client:
            self.verbose('No Client Found!')
            return None
 
        self.verbose('Client Found: %s on slot %s' % (client.name, client.cid))
 
        data = match.group('text')
        if data and ord(data[:1]) == 21:
            data = data[1:]
 
        return b3.events.Event(b3.events.EVT_CLIENT_PRIVATE_SAY, data, client, tclient)
 
 
 
    # tell
    def OnTell(self, action, data, match=None):
        #5:27 tell: woekele to XLR8or: test
        #We'll use saytell instead
        return None
        """-------------------------------------------------------------------------------
        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)
        -------------------------------------------------------------------------------"""
 
    # endmap/shutdown
    def OnShutdowngame(self, action, data=None, match=None):
        self.debug('EVENT: OnShutdowngame')
        self.game.mapEnd()
        # self.clients.sync()
        # self.debug('Synchronizing client info')
        self._maplist = None # when UrT server reloads, newly uploaded maps get available: force refresh
        return b3.events.Event(b3.events.EVT_GAME_EXIT, data)
 
    # Startgame
    def OnInitgame(self, action, data, match=None):
        self.debug('EVENT: OnInitgame')
        options = re.findall(r'\\([^\\]+)\\([^\\]+)', data)
 
        # capturelimit / fraglimit / timelimit
        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]
            elif o[0] == 'capturelimit':
                self.game.captureLimit = o[1]
            elif o[0] == 'fraglimit':
                self.game.fragLimit = o[1]
            elif o[0] == 'timelimit':
                self.game.timeLimit = o[1]
            else:
                setattr(self.game, o[0], o[1])
 
        self.verbose('...self.console.game.gameType: %s' % self.game.gameType)
        self.game.startMap()
        self.game.rounds = 0
        thread.start_new_thread(self.clients.sync, ())
        return b3.events.Event(b3.events.EVT_GAME_ROUND_START, self.game)
 
 
    # Warmup
    def OnWarmup(self, action, data=None, match=None):
        self.debug('EVENT: OnWarmup')
        self.game.rounds = 0
        return b3.events.Event(b3.events.EVT_GAME_WARMUP, data)
 
    # Start Round
    def OnInitround(self, action, data, match=None):
        self.debug('EVENT: OnInitround')
        options = re.findall(r'\\([^\\]+)\\([^\\]+)', data)
 
        # capturelimit / fraglimit / timelimit
        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]
            elif o[0] == 'capturelimit':
                self.game.captureLimit = o[1]
            elif o[0] == 'fraglimit':
                self.game.fragLimit = o[1]
            elif o[0] == 'timelimit':
                self.game.timeLimit = o[1]
            else:
                setattr(self.game, o[0], o[1])
 
        self.verbose('...self.console.game.gameType: %s' % self.game.gameType)
        self.game.startRound()
 
        return b3.events.Event(b3.events.EVT_GAME_ROUND_START, self.game)
 
 
    ###############################################################################################
    #
    #    B3 Parser interface implementation
    #
    ###############################################################################################
 
    def saybig(self, msg):
        lines = []
        for line in self.getWrap(msg, self._settings['line_length'], self._settings['min_wrap_length']):
            lines.append(self.getCommand('saybig', prefix=self.msgPrefix, message=line))
 
        if len(lines):
            self.writelines(lines)
 
    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)
 
        if admin:
            admin.message('^3banned^7: ^1%s^7 (^2@%s^7). His last ip (^1%s^7) has been added to banlist'%(client.exactName, client.id, client.ip))
 
        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):
        self.debug('EFFECTIVE UNBAN : %s',self.getCommand('unbanByIp', ip=client.ip, reason=reason))
        cmd = self.getCommand('unbanByIp', ip=client.ip, reason=reason)
        # UrT adds multiple instances to banlist.txt Make sure we remove up to 5 duplicates in a separate thread
        self.writelines([cmd, cmd, cmd, cmd, cmd])
        if admin:
            admin.message('^3Unbanned^7: ^1%s^7 (^2@%s^7). His last ip (^1%s^7) has been removed from banlist. Trying to remove duplicates...' % (client.exactName, client.id, client.ip))
        self.queueEvent(b3.events.Event(b3.events.EVT_CLIENT_UNBAN, admin, client))
 
    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:
                    try:
                        players[str(m.group('slot'))] = int(m.group('ping'))
                    except:
                        players[str(m.group('slot'))] = 999
        return players
 
    def sync(self):
        self.debug('Synchronizing client info')
        plist = self.getPlayerList(maxRetries=4)
        mlist = {}
 
        for cid, c in plist.iteritems():
            client = self.getByCidOrJoinPlayer(cid)
            if client:
                # Disconnect the zombies first
                if c['ping'] == 'ZMBI':
                    self.debug('slot is in state zombie: %s - ignoring', c['ip'])
                    # client.disconnect()
                elif 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 rotateMap(self):
        self.say('^7Changing to next map')
        time.sleep(1)
        self.write('cyclemap')
 
    def changeMap(self, map_name):
        """\
        load a given map/level
        return a list of suggested map names in cases it fails to recognize the map that was provided
        """
        rv = self.getMapsSoundingLike(map_name)
        if isinstance(rv, basestring):
            self.say('^7Changing map to %s' % rv)
            time.sleep(1)
            self.write('map %s' % rv)
        else:
            return rv
 
    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
 
 
    ###############################################################################################
    #
    #    Other methods
    #
    ###############################################################################################
 
    def getNextMap(self):
        # let's first check if a vote passed for the next map
        nmap = self.getCvar('g_nextmap').getString()
        self.debug('g_nextmap: %s' % nmap)
        if nmap != "":
            if nmap[:4] == 'ut4_': nmap = nmap[4:]
            elif nmap[:3] == 'ut_': nmap = nmap[3:]
            return nmap.title()
 
        # seek the next map from the mapcyle file
        if not self.game.mapName: return None
 
        mapcycle = self.getCvar('g_mapcycle').getString()
        if self.game.fs_game is None:
            try:
                self.game.fs_game = self.getCvar('fs_game').getString().rstrip('/')
            except:
                self.game.fs_game = None
                self.warning("Could not query server for fs_game")
        if self.game.fs_basepath is None:
            try:
                self.game.fs_basepath = self.getCvar('fs_basepath').getString().rstrip('/')
            except:
                self.game.fs_basepath = None
                self.warning("Could not query server for fs_basepath")
        mapfile = self.game.fs_basepath + '/' + self.game.fs_game + '/' + mapcycle
        if not os.path.isfile(mapfile):
            self.debug('could not read mapcycle file at %s' % mapfile)
            if self.game.fs_homepath is None:
                try:
                    self.game.fs_homepath = self.getCvar('fs_homepath').getString().rstrip('/')
                except:
                    self.game.fs_homepath = None
                    self.warning("Could not query server for fs_homepath")
            mapfile = self.game.fs_homepath + '/' + self.game.fs_game + '/' + mapcycle
        if not os.path.isfile(mapfile):
            self.debug('could not read mapcycle file at %s' % mapfile)
            self.error("Unable to find mapcycle file %s" % mapcycle)
            return None
 
        cyclemapfile = open(mapfile, 'r')
 
        re_comment_line = re.compile(r"""^\s*(//.*)?$""")
        lines = filter(lambda x: not re_comment_line.match(x), cyclemapfile.readlines())
        #self.debug(lines)
        if not len(lines):
            return None
 
        # get maps
        maps = []
        try:
            while True:
                tmp = lines.pop(0).strip()
                if tmp[0] == '{':
                    while tmp[0] != '}':
                        tmp = lines.pop(0).strip()
                    tmp = lines.pop(0).strip()
                maps.append(tmp)
        except IndexError:
            pass
 
        #self.debug(maps)
 
        if len(maps) == 0:
            return None
 
        firstmap = maps[0]
 
        # find current map
        #currentmap = self.game.mapName.strip().lower() # this fails after a cyclemap
        currentmap = self.getCvar('mapname').value
        try:
            tmp = maps.pop(0)
            while currentmap != tmp:
                tmp = maps.pop(0)
            if currentmap == tmp:
                #self.debug('found current map %s' % currentmap)
                #self.debug(maps)
                if len(maps) > 0:
                    return maps.pop(0)
                else:
                    return firstmap
        except IndexError:
            return firstmap
 
    def getMapsSoundingLike(self, mapname):
        """ return a valid mapname.
        If no exact match is found, then return close candidates as a list
        """
        wanted_map = mapname.lower()
        supportedMaps = self.getMaps()
        if wanted_map in supportedMaps:
            return wanted_map
 
        cleaned_supportedMaps = {}
        for map_name in supportedMaps:
            cleaned_supportedMaps[re.sub("^ut4?_", '', map_name, count=1)] = map_name
 
        if wanted_map in cleaned_supportedMaps:
            return cleaned_supportedMaps[wanted_map]
 
        cleaned_wanted_map = re.sub("^ut4?_", '', wanted_map, count=1)
 
        matches = [cleaned_supportedMaps[match] for match in getStuffSoundingLike(cleaned_wanted_map, cleaned_supportedMaps.keys())]
        if len(matches) == 1:
            # one match, get the map id
            return matches[0]
        else:
            # multiple matches, provide suggestions
            return matches
 
 
    def getTeamScores(self):
        data = self.write('players')
        if not data:
            return None
 
        line = data.split('\n')[2]
 
        m = re.match(self._reTeamScores, line.strip())
        if m:
            return [int(m.group('RedScore')), int(m.group('BlueScore'))]
 
        return None
 
    def getScores(self):
        """
        NOTE: this won't work properly if the server has private slots. see http://forums.urbanterror.net/index.php/topic,9356.0.html
        """
        data = self.write('players')
        if not data:
            return None
 
 
        scores = {'red':None, 'blue':None, 'players':{}}
 
        line = data.split('\n')[2]
        m = re.match(self._reTeamScores, line.strip())
        if m:
            scores['red'] = int(m.group('RedScore'))
            scores['blue'] = int(m.group('BlueScore'))
 
        for line in data.split('\n')[3:]:
            m = re.match(self._rePlayerScore, line.strip())
            if m:
                scores['players'][int(m.group('slot'))] = {'kills':int(m.group('kill')), 'deaths':int(m.group('death'))}
 
        return scores
 
 
    def queryClientUserInfoByCid(self, cid):
        """
        : dumpuser 5
        Player 5 is not on the server
 
        : dumpuser 3
        userinfo
        --------
        ip                  62.235.246.103:27960
        name                Shinki
        racered             2
        raceblue            2
        rate                8000
        ut_timenudge        0
        cg_rgb              255 0 255
        cg_predictitems     0
        cg_physics          1
        gear                GLJAXUA
        cl_anonymous        0
        sex                 male
        handicap            100
        color2              5
        color1              4
        team_headmodel      *james
        team_model          james
        headmodel           sarge
        model               sarge
        snaps               20
        teamtask            0
        cl_guid             8982B13A8DCEE4C77A32E6AC4DD7EEDF
        weapmodes           00000110220000020002
 
        """
        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))
            self.debug('client %s probably disconnected, but its character is still hanging in game...' % cid)
            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
 
    def getByCidOrJoinPlayer(self, cid):
        client = self.clients.getByCID(cid)
        if client:
            return client
        else:
            userinfostring = self.queryClientUserInfoByCid(cid)
            if userinfostring:
                self.OnClientuserinfo(None, userinfostring)
            return self.clients.getByCID(cid)
 
    def getPlayerTeams(self):
        """return a dict having cid as keys and a B3 team as value for
        as many slots as we can get a team for.
 
        /rcon players
        Map: ut4_heroic_beta1
        Players: 16
        Scores: R:51 B:92
        0:  FREE k:0 d:0 ping:0
        0:  FREE k:0 d:0 ping:0
        2: Anibal BLUE k:24 d:11 ping:69 90.47.240.44:27960
        3: kasper01 RED k:6 d:28 ping:56 93.22.173.133:27960
        4: notorcan RED k:16 d:10 ping:51 86.206.51.250:27960
        5: laCourge SPECTATOR k:0 d:0 ping:48 81.56.143.41:27960
        6: fundy_kill BLUE k:6 d:9 ping:50 92.129.99.62:27960
        7: brillko BLUE k:25 d:11 ping:56 85.224.201.172:27960
        8: -Tuxmania- BLUE k:16 d:7 ping:48 81.231.39.32:27960
        9: j.i.goe RED k:1 d:4 ping:51 86.218.69.81:27960
        10: EasyRider RED k:10 d:12 ping:53 85.176.137.142:27960
        11: Ferd75 BLUE k:4 d:8 ping:48 90.3.171.84:27960
        12: frag4#Gost0r RED k:11 d:16 ping:74 79.229.27.54:27960
        13: {'OuT'}ToinetoX RED k:6 d:13 ping:67 81.48.189.135:27960
        14: GibsonSG BLUE k:-1 d:2 ping:37 84.60.3.67:27960
        15: Kjeldor BLUE k:16 d:9 ping:80 85.246.3.196:50851
 
        NOTE: this won't work fully if the server has private slots. see http://forums.urbanterror.net/index.php/topic,9356.0.html
        """
        player_teams = {}
        letters2slots = {'A': '0', 'C': '2', 'B': '1', 'E': '4', 'D': '3', 'G': '6', 'F': '5', 'I': '8', 'H': '7', 'K': '10', 'J': '9', 'M': '12', 'L': '11', 'O': '14', 'N': '13', 'Q': '16', 'P': '15', 'S': '18', 'R': '17', 'U': '20', 'T': '19', 'W': '22', 'V': '21', 'Y': '24', 'X': '23', 'Z': '25'}
 
        players_data = self.write('players')
        for line in players_data.split('\n')[3:]:
            self.debug(line.strip())
            m = re.match(self._rePlayerScore, line.strip())
            if m and line.strip() != '0:  FREE k:0 d:0 ping:0':
                cid = m.group('slot')
                team = self.getTeam(m.group('team'))
                player_teams[cid] = team
 
        g_blueteamlist = self.getCvar('g_blueteamlist')
        if g_blueteamlist:
            for letter in g_blueteamlist.getString():
                player_teams[letters2slots[letter]] = b3.TEAM_BLUE
 
        g_redteamlist = self.getCvar('g_redteamlist')
        if g_redteamlist:
            for letter in g_redteamlist.getString():
                player_teams[letters2slots[letter]] = b3.TEAM_RED
        return player_teams
 
    def _getDamagePoints(self, weapon, hitloc):
        try:
            points = self.damage[weapon][int(hitloc)]
            self.debug("_getDamagePoints(%s, %s) -> %s" % (weapon, hitloc, points))
            return points
        except KeyError, err:
            self.warning("_getDamagePoints(%s, %s) cannot find value : %s" % (weapon, hitloc, err))
            return 15
 
    def _convertHitWeaponToKillWeapon(self, hitweapon_id):
        """on Hit: lines identifiers for weapons are different than
        the one on Kill: lines"""
        try:
            return self.hitweapon2killweapon[int(hitweapon_id)]
        except KeyError, err:
            self.warning("unknown weapon id on Hit line: %s", err)
            return None
 
 
"""
#----- Actions -----------------------------------------------------------------
Item: 0 team_CTF_redflag -> Flag Taken/picked up
Flag: 0 0: team_CTF_blueflag -> Flag Dropped
Flag: 0 1: team_CTF_blueflag -> Flag Returned
Flag: 0 2: team_CTF_blueflag -> Flag Captured
 
Bombholder is 5 -> Spawn with the bomb
Bomb was planted by 5
Bomb was defused by 6!
Bomb was tossed by 4 -> either manually or by being killed
Bomb has been collected by 6 -> Picking up a tossed bomb
 
#----- Connection Info ---------------------------------------------------------
A little documentation on the ClientSlot states in relation to ping positions in the status response
 
UrT ClientSlot states:
CS_FREE,     // can be reused for a new connection
CS_ZOMBIE,   // client has been disconnected, but don't reuse
             // connection for a couple seconds
CS_CONNECTED // has been assigned to a client_t, but no gamestate yet
CS_PRIMED,   // gamestate has been sent, but client hasn't sent a usercmd
CS_ACTIVE    // client is fully in game
 
Snippet 1:
if (cl->state == CS_CONNECTED)
            Com_Printf ("CNCT ");
        else if (cl->state == CS_ZOMBIE)
            Com_Printf ("ZMBI ");
        else
        {
            ping = cl->ping < 9999 ? cl->ping : 9999;
            Com_Printf ("%4i ", ping);
        }
 
Snippet 2:
if (cl->state == CS_ZOMBIE && cl->lastPacketTime < zombiepoint) {
  // using the client id cause the cl->name is empty at this point
  Com_DPrintf( "Going from CS_ZOMBIE to CS_FREE for client %d\n", i );
  cl->state = CS_FREE; // can now be reused
}
 
#----- Available variables defined on Init -------------------------------------
081027 14:53:22 DEBUG   EVENT: OnInitgame
081027 14:53:22 VERBOSE ...self.console.game.sv_allowdownload: 0
081027 14:53:22 VERBOSE ...self.console.game.g_matchmode: 0
081027 14:53:22 VERBOSE ...self.console.game.sv_maxclients: 16
081027 14:53:22 VERBOSE ...self.console.game.sv_floodprotect: 1
081027 14:53:22 VERBOSE ...self.console.game.g_warmup: 15
081027 14:53:22 VERBOSE ...self.console.game.captureLimit: 0
081027 14:53:22 VERBOSE ...self.console.game.sv_hostname:   ^1[SNT]^7 TDM #4 Dungeon (B3)
081027 14:53:22 VERBOSE ...self.console.game.g_followstrict: 1
081027 14:53:22 VERBOSE ...self.console.game.fragLimit: 0
081027 14:53:22 VERBOSE ...self.console.game.timeLimit: 15
081027 14:53:22 VERBOSE ...self.console.game.g_cahtime: 60
081027 14:53:22 VERBOSE ...self.console.game.g_swaproles: 0
081027 14:53:22 VERBOSE ...self.console.game.g_roundtime: 3
081027 14:53:22 VERBOSE ...self.console.game.g_bombexplodetime: 40
081027 14:53:22 VERBOSE ...self.console.game.g_bombdefusetime: 10
081027 14:53:22 VERBOSE ...self.console.game.g_hotpotato: 2
081027 14:53:22 VERBOSE ...self.console.game.g_waverespawns: 0
081027 14:53:22 VERBOSE ...self.console.game.g_redwave: 15
081027 14:53:22 VERBOSE ...self.console.game.g_bluewave: 15
081027 14:53:22 VERBOSE ...self.console.game.g_respawndelay: 3
081027 14:53:22 VERBOSE ...self.console.game.g_suddendeath: 1
081027 14:53:22 VERBOSE ...self.console.game.g_maxrounds: 0
081027 14:53:22 VERBOSE ...self.console.game.g_friendlyfire: 1
081027 14:53:22 VERBOSE ...self.console.game.g_allowvote: 536870920
081027 14:53:22 VERBOSE ...self.console.game.g_armbands: 0
081027 14:53:22 VERBOSE ...self.console.game.g_survivorrule: 0
081027 14:53:22 VERBOSE ...self.console.game.g_gear: 0
081027 14:53:22 VERBOSE ...self.console.game.g_deadchat: 1
081027 14:53:22 VERBOSE ...self.console.game.g_maxGameClients: 0
081027 14:53:22 VERBOSE ...self.console.game.sv_dlURL: sweetopia.snt.utwente.nl/xlr
081027 14:53:22 VERBOSE ...self.console.game.sv_maxPing: 250
081027 14:53:22 VERBOSE ...self.console.game.sv_minPing: 0
081027 14:53:22 VERBOSE ...self.console.game.sv_maxRate: 0
081027 14:53:22 VERBOSE ...self.console.game.sv_minRate: 0
081027 14:53:22 VERBOSE ...self.console.game.dmflags: 0
081027 14:53:22 VERBOSE ...self.console.game.version: ioq3 1.35urt linux-i386 Dec 20 2007
081027 14:53:22 VERBOSE ...self.console.game.protocol: 68
081027 14:53:22 VERBOSE ...self.console.game.mapName: ut4_turnpike
081027 14:53:22 VERBOSE ...self.console.game.sv_privateClients: 4
081027 14:53:22 VERBOSE ...self.console.game. Admin:  XLR8or
081027 14:53:22 VERBOSE ...self.console.game. Email: admin@xlr8or.com
081027 14:53:22 VERBOSE ...self.console.game.gamename: q3ut4
081027 14:53:22 VERBOSE ...self.console.game.g_needpass: 1
081027 14:53:22 VERBOSE ...self.console.game.g_enableDust: 0
081027 14:53:22 VERBOSE ...self.console.game.g_enableBreath: 0
081027 14:53:22 VERBOSE ...self.console.game.g_antilagvis: 0
081027 14:53:22 VERBOSE ...self.console.game.g_survivor: 0
081027 14:53:22 VERBOSE ...self.console.game.g_enablePrecip: 2
081027 14:53:22 VERBOSE ...self.console.game.g_modversion: 4.1
081027 14:53:22 VERBOSE ...self.console.game.gameType: tdm
 
"""