# BigBrotherBot(B3) (www.bigbrotherbot.net) # Copyright (C) 2010 # # 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 # # 15.01.2011 - 1.0.0 - Freelander, Courgette # * Initial release # 22.01.2011 - 1.0.1 - Freelander # * Do not try to authenticate [3arc]democlient # * Inherits from cod5 parser now to handle actions # 01.02.2011 - 1.0.2 - Freelander # * Force glogsync to 1 on every round start as it may be lost after a # server restart/crash # 01.02.2011 - 1.0.3 - Just a baka # * Pre-Match Logic # 08.02.2011 - 1.0.4 - Just a baka # * Reworked Pre-Match logic to reflect latest changes to cod7http # 02.03.2011 - 1.0.5 -Bravo17 # * Added test to make sure cod7http still running # * Tidied up startup console output # 02.04.2011 - 1.0.6 - Freelander # * onK: Fix for suicide events to be handled correctly by XLRstats # * Set playercount to 4 in pre-match logic # 09.04.2011 - 1.0.7 - Courgette # * reflect that cid are not converted to int anymore in the clients module # 14.04.2011 - 1.0.8 - Freelander # * Fixed rcon set command that was changed as setadmindvar in CoD7 # 25.05.2011 - 1.1 - Courgette # * kick commands now sends reason # 30.12.2011 - 1.2 - Bravo17 # * New client will now join Auth queue if slot shows as 'Disconnected' in Auth queue # 25.07.2012 - 1.2.1 - Courgette # * Make sure the cod7http plugin is loaded # ## @file # CoD7 Parser __author__ = 'Freelander, Courgette, Just a baka, Bravo17' __version__ = '1.2.1' import re import string import threading import b3.parsers.cod7_rcon as rcon import b3.parsers.cod5 import os class Cod7Parser(b3.parsers.cod5.Cod5Parser): gameName = 'cod7' IpsOnly = False _guidLength = 5 OutputClass = rcon.Cod7Rcon _usePreMatchLogic = True _preMatch = False _elFound = True _igBlockFound = False _sgFound = False _logTimer = 0 _logTimerOld = 0 _cod7httpplugin = None _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['say'] = 'say %(prefix)s %(message)s' _commands['set'] = 'setadmindvar %(name)s "%(value)s"' _commands['kick'] = 'clientkick %(cid)s "%(reason)s"' _commands['ban'] = 'banclient %(cid)s' _commands['unban'] = 'unbanuser "%(name)s"' _commands['tempban'] = 'clientkick %(cid)s "%(reason)s"' """\ Next actions need translation to the EVT_CLIENT_ACTION (Treyarch has a different approach on actions) While IW put all EVT_CLIENT_ACTION in the A; action, Treyarch creates a different action for each EVT_CLIENT_ACTION. """ _actionMap = ( 'AD', # Actor Damage (dogs) 'VD' # Vehicle Damage ) #num score ping guid name lastmsg address qport rate #--- ----- ---- -------------------------------- --------------- ------- --------------------- ----- ----- # 4 0 23 blablablabfa218d4be29e7168c637be ^1XLR^78^9or[^7^7 0 135.94.165.296:63564 25313 25000 _regPlayer = re.compile(r'^(?P<slot>[0-9]+)\s+(?P<score>[0-9-]+)\s+(?P<ping>[0-9]+)\s+(?P<guid>[0-9]+)\s+(?P<name>.*?)\s+(?P<last>[0-9]+)\s+(?P<ip>[0-9.]+):(?P<port>[0-9-]+)(?P<qportsep>[-\s]+)(?P<qport>[0-9-]+)\s+(?P<rate>[0-9]+)$', re.I) _regPlayerWithDemoclient = re.compile(r'^(?P<slot>[0-9]+)\s+(?P<score>[0-9-]+)\s+(?P<ping>[0-9]+)\s+(?P<guid>[0-9]+)\s+(?P<name>.*?)\s+(?P<last>[0-9]+)\s+(?P<ip>[0-9.]+|unknown):?(?P<port>[0-9-]+)?(?P<qportsep>[-\s]+)(?P<qport>[0-9-]+)\s+(?P<rate>[0-9]+)$', re.I) def startup(self): """Implements some necessary tasks on initial B3 start.""" # add the world client client = self.clients.newClient('-1', guid='WORLD', name='World', hide=True, pbid='WORLD') self._cod7httpplugin = self.getPlugin('cod7http') if self._cod7httpplugin is None: self.critical("cannot found cod7http plugin") raise SystemExit(220) # get map from the status rcon command map = self.getMap() if map: self.game.mapName = map self.info('map is: %s'%self.game.mapName) if self.config.has_option('server', 'use_prematch_logic'): self._usePreMatchLogic = self.config.getboolean('server', 'use_prematch_logic') # Pre-Match Logic part 1 if self._usePreMatchLogic: self._regPlayer, self._regPlayerWithDemoclient = self._regPlayerWithDemoclient, self._regPlayer playerList = self.getPlayerList() self._regPlayer, self._regPlayerWithDemoclient = self._regPlayerWithDemoclient, self._regPlayer if len(playerList) >= 4: self.verbose('PREMATCH OFF: PlayerCount >=4: not a Pre-Match') self._preMatch = False elif '0' in playerList and playerList['0']['guid'] == '0': self.verbose('PREMATCH OFF: Got a democlient presence: not a Pre-Match') self._preMatch = False else: self.verbose('PREMATCH ON: PlayerCount < 4, got no democlient presence. Defaulting to a pre-match.') self._preMatch = True else: self._preMatch = False # Force g_logsync self.debug('Forcing server cvar g_logsync to %s and turning UNIX timestamp log timers off.' % self._logSync) self.write('g_logsync %s' % self._logSync) self.write('g_logTimeStampInSeconds 0') self.setVersionExceptions() self.debug('Parser started.') def parseLine(self, line): """Called from parseLine method in Parser class to introduce pre-match logic and action mapping """ m = self.getLineParts(line) if not m: return False match, action, data, client, target = m func = 'On%s' % string.capwords(action).replace(' ','') # Timer (in seconds) that always reflects the current event's timestamp t = re.match(self._lineTime, line) if t: self._logTimerOld = self._logTimer self._logTimer = int(t.group('minutes')) * 60 + int(t.group('seconds')) # Pre-Match Logic part 2 # Ignore Pre-Match K/D-events if self._preMatch and (func == 'OnD' or func == 'OnK' or func == 'OnAd' or func == 'OnVd'): self.verbose('PRE-MATCH: Ignoring kill/damage.') return False # Prevent OnInitgame from being called twice due to server-side initGame doubling elif func == 'OnInitgame': if not self._igBlockFound: self._igBlockFound = True self.verbose('Found 1st InitGame from block') elif self._logTimerOld <= self._logTimer: self.verbose('Found 2nd InitGame from block, ignoring') return False # ExitLevel means there will be Pre-Match right after the mapload elif self._usePreMatchLogic and func == 'OnExitlevel': self._preMatch = True self.debug('PRE-MATCH ON: found ExitLevel') self._elFound = True self._igBlockFound = False # If we track ShutdownGame events, we could detect sudden server restarts and re-matches elif func == 'OnShutdowngame': self._sgFound = True self._igBlockFound = False # Round switch (InitGame after ShutdownGame, but there was no ExitLevel): if self._preMatch and not self._elFound and self._igBlockFound and self._sgFound and self._logTimerOld <= self._logTimer: self.preMatch = False self.debug('PRE-MATCH OFF: found a round change.') self._igBlockFound = False self._sgFound = False # Timer reset elif self._logTimerOld > self._logTimer: self.debug('Old timer: %s / New timer: %s' % (self._logTimerOld, self._logTimer)) if self._usePreMatchLogic: self._preMatch = True self.debug('PRE-MATCH ON: Server crash/restart detected.') else: self.debug('Server crash/restart detected.') self._elFound = False self._igBlockFound = False self._sgFound = False # Payload self.write('setadmindvar g_logsync %s' % self._logSync) self.write('setadmindvar g_logTimeStampInSeconds 0') # Initgame after ExitLevel else: self._elFound = False self._sgFound = False #self.debug("-==== FUNC!!: " + func) if hasattr(self, func): func = getattr(self, func) event = func(action, data, match) if event: self.queueEvent(event) elif action in self._eventMap: self.queueEvent(b3.events.Event( self._eventMap[action], data, client, target )) # Addition for cod7 actionMapping elif action in self._actionMap: self.translateAction(action, data, match) else: self.queueEvent(b3.events.Event( b3.events.EVT_UNKNOWN, str(action) + ': ' + str(data), client, target )) def OnJ(self, action, data, match=None): """Client join""" codguid = match.group('guid') cid = match.group('cid') name = match.group('name') if codguid == '0' and cid == '0' and name == '[3arc]democlient': self.verbose('Authentication not required for [3arc]democlient. Aborting Join.') self._preMatch = 0 return None if len(codguid) < self._guidLength: # invalid guid self.verbose2('Invalid GUID: %s' %codguid) codguid = None client = self.getClient(match) if client: self.verbose2('ClientObject already exists') # lets see if the name/guids match for this client, prevent player mixups after mapchange (not with PunkBuster enabled) if not self.PunkBuster: if self.IpsOnly: # this needs testing since the name cleanup code may interfere with this next condition if name != client.name: self.debug('This is not the correct client (%s <> %s), disconnecting' %(name, client.name)) client.disconnect() return None else: self.verbose2('client.name in sync: %s == %s' %(name, client.name)) else: if codguid != client.guid: self.debug('This is not the correct client (%s <> %s), disconnecting' %(codguid, client.guid)) client.disconnect() return None else: self.verbose2('client.guid in sync: %s == %s' %(codguid, client.guid)) # update existing client client.state = b3.STATE_ALIVE # possible name changed client.name = name # Join-event for mapcount reasons and so forth return b3.events.Event(b3.events.EVT_CLIENT_JOIN, None, client) else: if self._counter.get(cid) and self._counter.get(cid) != 'Disconnected': self.verbose('cid: %s already in authentication queue. Aborting Join.' %cid) return None self._counter[cid] = 1 t = threading.Timer(2, self.newPlayer, (cid, codguid, name)) t.start() self.debug('%s connected, waiting for Authentication...' %name) self.debug('Our Authentication queue: %s' % self._counter) # kill def OnK(self, action, data, match=None): victim = self.clients.getByGUID(match.group('guid')) if not victim: self.debug('No victim %s' % match.groupdict()) self.OnJ(action, data, match) return None attacker = self.clients.getByGUID(match.group('aguid')) if not attacker: if match.group('acid') == '-1' or match.group('aname') == 'world': self.verbose('World kill') attacker = self.getClient(attacker=match) else: self.debug('No attacker %s' % match.groupdict()) return None # COD5 first version doesn't report the team on kill, only use it if it's set # Hopefully the team has been set on another event if match.group('ateam'): attacker.team = self.getTeam(match.group('ateam')) if match.group('team'): victim.team = self.getTeam(match.group('team')) event = b3.events.EVT_CLIENT_KILL if attacker == victim or attacker.cid == '-1': self.verbose('Suicide Detected, attacker.cid: %s, victim.cid: %s' % (attacker.cid, victim.cid)) event = b3.events.EVT_CLIENT_SUICIDE elif attacker.team != b3.TEAM_UNKNOWN and attacker.team and victim.team and attacker.team == victim.team: self.verbose('Team kill detected, %s team killed %s' % (attacker.name, victim.name)) event = b3.events.EVT_CLIENT_KILL_TEAM victim.state = b3.STATE_DEAD return b3.events.Event(event, (float(match.group('damage')), match.group('aweap'), match.group('dlocation'), match.group('dtype')), attacker, victim) def read(self): """read from game server log file""" # Getting the stats of the game log (we are looking for the size) filestats = os.fstat(self.input.fileno()) thread_alive = self._cod7httpplugin.httpThreadalive() if not thread_alive: self.verbose('Cod7Http Plugin has stopped working, restarting') self.restart() # Compare the current cursor position against the current file size, # if the cursor is at a number higher than the game log size, then # there's a problem if self.input.tell() > filestats.st_size: self.debug('Parser: Game log is suddenly smaller than it was before (%s bytes, now %s), the log was probably either rotated or emptied. B3 will now re-adjust to the new size of the log.' % (str(self.input.tell()), str(filestats.st_size)) ) self.input.seek(0, os.SEEK_END) return self.input.readlines() ################################################################### # ALTER THE WAY parser.py work for game logs starting with http:// ################################################################### from b3.parser import Parser # backup original loadArbPlugins originalLoadArbPlugins = Parser.loadArbPlugins def newLoadArbPlugins(self): """Call original loadArbPlugin method from the Parser class then unload the httpytail plugin then load the cod7http plugin instead""" print "running newLoadArbPlugins " ## first, run usual loadArbPlugins originalLoadArbPlugins(self) if self.config.has_option('server','game_log') \ and self.config.get('server','game_log')[0:7] == 'http://' : # undo httpytail load self.screen.write('Unloading : http Plugin\n') self._pluginOrder.remove('httpytail') del self._plugins['httpytail'] # load cod7http p = 'cod7http' self.bot('Loading %s', p) try: pluginModule = self.pluginImport(p) self._plugins[p] = getattr(pluginModule, '%sPlugin' % p.title()) (self) self._pluginOrder.append(p) version = getattr(pluginModule, '__version__', 'Unknown Version') author = getattr(pluginModule, '__author__', 'Unknown Author') self.bot('Plugin %s (%s - %s) loaded', p, version, author) self.screen.write('Loading : COD7 http Plugin\n') self.screen.flush() except Exception, msg: self.critical('Error loading plugin: %s', msg) raise SystemExit('error while loading %s' % p) # make newLoadArbPlugins the default Parser.loadArbPlugins = newLoadArbPlugins