├── .gitignore ├── .noserc ├── .pylintrc ├── .travis.yml ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── requirements.txt ├── setup.py ├── srcds ├── __init__.py ├── events │ ├── __init__.py │ ├── csgo.py │ └── generic.py ├── logparser.py ├── objects.py └── rcon.py └── test ├── __init__.py └── events ├── __init__.py ├── test_csgo.py └── test_generic.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | MANIFEST 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | -------------------------------------------------------------------------------- /.noserc: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity=3 3 | with-doctest=1 4 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | profile=no 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore=CVS 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | 25 | [MESSAGES CONTROL] 26 | 27 | # Enable the message, report, category or checker with the given id(s). You can 28 | # either give multiple identifier separated by comma (,) or put this option 29 | # multiple time. 30 | #enable= 31 | 32 | # Disable the message, report, category or checker with the given id(s). You 33 | # can either give multiple identifier separated by comma (,) or put this option 34 | # multiple time (only on the command line, not in the configuration file where 35 | # it should appear only once). 36 | disable=R0903,R0913,E0611 37 | 38 | 39 | [REPORTS] 40 | 41 | # Set the output format. Available formats are text, parseable, colorized, msvs 42 | # (visual studio) and html. You can also give a reporter class, eg 43 | # mypackage.mymodule.MyReporterClass. 44 | output-format=text 45 | 46 | # Include message's id in output 47 | include-ids=no 48 | 49 | # Include symbolic ids of messages in output 50 | symbols=no 51 | 52 | # Put messages in a separate file for each module / package specified on the 53 | # command line instead of printing them on stdout. Reports (if any) will be 54 | # written in a file name "pylint_global.[txt|html]". 55 | files-output=no 56 | 57 | # Tells whether to display a full report or only the messages 58 | reports=yes 59 | 60 | # Python expression which should return a note less than 10 (10 is the highest 61 | # note). You have access to the variables errors warning, statement which 62 | # respectively contain the number of errors / warnings messages and the total 63 | # number of statements analyzed. This is used by the global evaluation report 64 | # (RP0004). 65 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 66 | 67 | # Add a comment according to your evaluation note. This is used by the global 68 | # evaluation report (RP0004). 69 | comment=no 70 | 71 | 72 | [BASIC] 73 | 74 | # Required attributes for module, separated by a comma 75 | required-attributes= 76 | 77 | # List of builtins function names that should not be used, separated by a comma 78 | bad-functions=map,filter,apply,input 79 | 80 | # Regular expression which should only match correct module names 81 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 82 | 83 | # Regular expression which should only match correct module level names 84 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 85 | 86 | # Regular expression which should only match correct class names 87 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 88 | 89 | # Regular expression which should only match correct function names 90 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 91 | 92 | # Regular expression which should only match correct method names 93 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 94 | 95 | # Regular expression which should only match correct instance attribute names 96 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 97 | 98 | # Regular expression which should only match correct argument names 99 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 100 | 101 | # Regular expression which should only match correct variable names 102 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 103 | 104 | # Regular expression which should only match correct list comprehension / 105 | # generator expression variable names 106 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 107 | 108 | # Good variable names which should always be accepted, separated by a comma 109 | good-names=i,j,k,ex,Run,_ 110 | 111 | # Bad variable names which should always be refused, separated by a comma 112 | bad-names=foo,bar,baz,toto,tutu,tata 113 | 114 | # Regular expression which should only match functions or classes name which do 115 | # not require a docstring 116 | no-docstring-rgx=__.*__ 117 | 118 | 119 | [FORMAT] 120 | 121 | # Maximum number of characters on a single line. 122 | max-line-length=120 123 | 124 | # Maximum number of lines in a module 125 | max-module-lines=1000 126 | 127 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 128 | # tab). 129 | indent-string=' ' 130 | 131 | 132 | [MISCELLANEOUS] 133 | 134 | # List of note tags to take in consideration, separated by a comma. 135 | notes=FIXME,XXX,TODO 136 | 137 | 138 | [SIMILARITIES] 139 | 140 | # Minimum lines number of a similarity. 141 | min-similarity-lines=4 142 | 143 | # Ignore comments when computing similarities. 144 | ignore-comments=yes 145 | 146 | # Ignore docstrings when computing similarities. 147 | ignore-docstrings=yes 148 | 149 | # Ignore imports when computing similarities. 150 | ignore-imports=no 151 | 152 | 153 | [TYPECHECK] 154 | 155 | # Tells whether missing members accessed in mixin class should be ignored. A 156 | # mixin class is detected if its name ends with "mixin" (case insensitive). 157 | ignore-mixin-members=yes 158 | 159 | # List of classes names for which member attributes should not be checked 160 | # (useful for classes with attributes dynamically set). 161 | ignored-classes=SQLObject 162 | 163 | # When zope mode is activated, add a predefined set of Zope acquired attributes 164 | # to generated-members. 165 | zope=no 166 | 167 | # List of members which are set dynamically and missed by pylint inference 168 | # system, and so shouldn't trigger E0201 when accessed. Python regular 169 | # expressions are accepted. 170 | generated-members=REQUEST,acl_users,aq_parent 171 | 172 | 173 | [VARIABLES] 174 | 175 | # Tells whether we should check for unused import in __init__ files. 176 | init-import=no 177 | 178 | # A regular expression matching the beginning of the name of dummy variables 179 | # (i.e. not used). 180 | dummy-variables-rgx=_|dummy 181 | 182 | # List of additional names supposed to be defined in builtins. Remember that 183 | # you should avoid to define new builtins when possible. 184 | additional-builtins= 185 | 186 | 187 | [CLASSES] 188 | 189 | # List of interface methods to ignore, separated by a comma. This is used for 190 | # instance to not check methods defines in Zope's Interface base class. 191 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 192 | 193 | # List of method names used to declare (i.e. assign) instance attributes. 194 | defining-attr-methods=__init__,__new__,setUp 195 | 196 | # List of valid names for the first argument in a class method. 197 | valid-classmethod-first-arg=cls 198 | 199 | # List of valid names for the first argument in a metaclass class method. 200 | valid-metaclass-classmethod-first-arg=mcs 201 | 202 | 203 | [DESIGN] 204 | 205 | # Maximum number of arguments for function / method 206 | max-args=5 207 | 208 | # Argument names that match this expression will be ignored. Default to name 209 | # with leading underscore 210 | ignored-argument-names=_.* 211 | 212 | # Maximum number of locals for function / method body 213 | max-locals=15 214 | 215 | # Maximum number of return / yield for function / method body 216 | max-returns=6 217 | 218 | # Maximum number of branch for function / method body 219 | max-branchs=12 220 | 221 | # Maximum number of statements in function / method body 222 | max-statements=50 223 | 224 | # Maximum number of parents for a class (see R0901). 225 | max-parents=7 226 | 227 | # Maximum number of attributes for a class (see R0902). 228 | max-attributes=7 229 | 230 | # Minimum number of public methods for a class (see R0903). 231 | min-public-methods=2 232 | 233 | # Maximum number of public methods for a class (see R0904). 234 | max-public-methods=20 235 | 236 | 237 | [IMPORTS] 238 | 239 | # Deprecated modules which should not be used, separated by a comma 240 | deprecated-modules=regsub,string,TERMIOS,Bastion,rexec 241 | 242 | # Create a graph of every (i.e. internal and external) dependencies in the 243 | # given file (report RP0402 must not be disabled) 244 | import-graph= 245 | 246 | # Create a graph of external dependencies in the given file (report RP0402 must 247 | # not be disabled) 248 | ext-import-graph= 249 | 250 | # Create a graph of internal dependencies in the given file (report RP0402 must 251 | # not be disabled) 252 | int-import-graph= 253 | 254 | 255 | [EXCEPTIONS] 256 | 257 | # Exceptions that will emit a warning when being caught. Defaults to 258 | # "Exception" 259 | overgeneral-exceptions=Exception 260 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | - 3.5 5 | - 3.6 6 | 7 | install: 8 | - pip install -r requirements.txt 9 | - pip install . 10 | - pip install coveralls 11 | 12 | script: 13 | - nosetests --with-coverage --cover-package=srcds 14 | 15 | after_success: 16 | - coveralls 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | **Copyright (c) 2013 Peter Rowlands** 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pysrcds 2 | ======= 3 | 4 | Python library for interacting with Source engine dedicated servers. 5 | 6 | [![Build Status](https://travis-ci.org/pmrowla/pysrcds.svg?branch=master)](https://travis-ci.org/pmrowla/pysrcds) 7 | [![Coverage Status](https://coveralls.io/repos/github/pmrowla/pysrcds/badge.svg?branch=master)](https://coveralls.io/github/pmrowla/pysrcds?branch=master) 8 | [![PyPI version](https://badge.fury.io/py/pysrcds.svg)](https://pypi.python.org/pypi/pysrcds/) 9 | 10 | pysrcds provides the functionality to communicate with a dedicated server via 11 | RCON and also provides the ability to parse Source engine logs. There are also 12 | some utility classes that may be useful for developing other Source related 13 | functionality. 14 | 15 | Python 2.7 and Python 3.5+ are supported. 16 | 17 | Installation 18 | ------------ 19 | 20 | ``` 21 | pip install pysrcds 22 | ``` 23 | 24 | 25 | HL Log Parsing 26 | -------------- 27 | 28 | For a log parsing example see [goonpug-trueskill](https://github.com/goonpug/goonpug-trueskill). 29 | 30 | RCON Usage 31 | ---------- 32 | 33 | ```python 34 | from srcds.rcon import RconConnection 35 | 36 | conn = RconConnection('127.0.0.1', port=27015, password='password') 37 | response = conn.exec_command('status') 38 | # Response content can be accessed via str(response) or response.body 39 | # Response content will be a utf-8 encoded string in most cases, but it may depend on the 40 | # server type. 41 | 42 | # For servers that do not support multipart RCON responses like factorio, 43 | # enable the single_packet_mode option 44 | factorio_conn = RconConnection('127.0.0.1', single_packet_mode=True) 45 | ``` 46 | 47 | License 48 | ------- 49 | 50 | pysrcds is distributed under the MIT license. See 51 | [LICENSE.md](https://github.com/pmrowla/pysrcds/blob/master/LICENSE.md) 52 | for more information. 53 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | future 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name='pysrcds', 7 | version='0.1.6.dev', 8 | description='Python library for interacting with Source engine dedicated' 9 | ' servers', 10 | author='Peter Rowlands', 11 | author_email='peter@pmrowla.com', 12 | url='https://github.com/pmrowla/pysrcds', 13 | packages=['srcds', 'srcds/events'], 14 | classifiers=[ 15 | 'Development Status :: 4 - Beta', 16 | 'Intended Audience :: Developers', 17 | 'License :: OSI Approved :: MIT License', 18 | 'Programming Language :: Python', 19 | 'Topic :: Games/Entertainment :: First Person Shooters', 20 | 'Topic :: Software Development :: Libraries :: Python Modules', 21 | 'Programming Language :: Python :: 2.7', 22 | 'Programming Language :: Python :: 3', 23 | 'Programming Language :: Python :: 3.5', 24 | 'Programming Language :: Python :: 3.6', 25 | ], 26 | install_requires=['future'], 27 | long_description=''' 28 | ======= 29 | pysrcds 30 | ======= 31 | 32 | Python library for interacting with Source engine dedicated servers. 33 | 34 | pysrcds provides the functionality to communicate with a dedicated server via 35 | RCON and also provides the ability to parse Source engine logs. There are also 36 | some utility classes that may be useful for developing other Source related 37 | functionality. 38 | 39 | License 40 | ======= 41 | 42 | pysrcds is distributed under the MIT license. 43 | ''', 44 | ) 45 | -------------------------------------------------------------------------------- /srcds/__init__.py: -------------------------------------------------------------------------------- 1 | """Python Source dedicated server library""" 2 | -------------------------------------------------------------------------------- /srcds/events/__init__.py: -------------------------------------------------------------------------------- 1 | """pysrcds events package""" 2 | -------------------------------------------------------------------------------- /srcds/events/csgo.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Peter Rowlands 2 | """csgo events module 3 | 4 | Contains event classes for CS:S and CS:GO events 5 | 6 | """ 7 | 8 | from __future__ import absolute_import, unicode_literals 9 | from future.utils import python_2_unicode_compatible 10 | 11 | from .generic import (BaseEvent, PlayerEvent, PlayerTargetEvent, KillEvent, 12 | AttackEvent) 13 | 14 | 15 | @python_2_unicode_compatible 16 | class SwitchTeamEvent(PlayerEvent): 17 | 18 | """Player switched team event""" 19 | 20 | regex = ''.join([ 21 | BaseEvent.regex, 22 | r'"(?P.*)<(?P\d*)><(?P[\w:]*)>" ', 23 | r'switched from team <(?P\w*)> to <(?P\w*)>', 24 | ]) 25 | 26 | def __init__(self, timestamp, player_name, uid, steam_id, orig_team, 27 | new_team): 28 | super(SwitchTeamEvent, self).__init__(timestamp, player_name, uid, 29 | steam_id, team=None) 30 | self.orig_team = orig_team 31 | self.new_team = new_team 32 | 33 | def text(self): 34 | player = self.player 35 | player.team = None 36 | msg = ' '.join([ 37 | '"%s"' % player, 38 | 'switched from team <%s> to <%s>' % (self.orig_team, 39 | self.new_team), 40 | ]) 41 | return ' '.join([super(PlayerEvent, self).text(), msg]) 42 | 43 | __str__ = text 44 | 45 | 46 | @python_2_unicode_compatible 47 | class BuyEvent(PlayerEvent): 48 | 49 | """Player buy event""" 50 | 51 | regex = ''.join([ 52 | PlayerEvent.regex, 53 | r'purchased "(?P\w*)"', 54 | ]) 55 | 56 | def __init__(self, timestamp, player_name, uid, steam_id, team, item): 57 | super(BuyEvent, self).__init__(timestamp, player_name, uid, steam_id, 58 | team) 59 | self.item = item 60 | 61 | def text(self): 62 | msg = 'purchased "%s"' % (self.item) 63 | return ' '.join([super(BuyEvent, self).text(), msg]) 64 | 65 | __str__ = text 66 | 67 | 68 | @python_2_unicode_compatible 69 | class ThrowEvent(PlayerEvent): 70 | 71 | """Player threw grenade event""" 72 | 73 | regex = ''.join([ 74 | PlayerEvent.regex, 75 | r'threw (?P\w*) \[(?P-?\d+ -?\d+ -?\d+)\]', 76 | ]) 77 | 78 | def __init__(self, timestamp, player_name, uid, steam_id, team, nade, 79 | location): 80 | if not isinstance(location, tuple) or not len(location) == 3: 81 | raise TypeError('Expected 3-tuple for location') 82 | super(ThrowEvent, self).__init__(timestamp, player_name, uid, steam_id, 83 | team) 84 | self.location = location 85 | self.nade = nade 86 | 87 | def text(self): 88 | msg = 'threw %s [%d %d %d]' % (self.nade, self.location[0], 89 | self.location[1], self.location[2]) 90 | return ' '.join([super(ThrowEvent, self).text(), msg]) 91 | 92 | __str__ = text 93 | 94 | @classmethod 95 | def from_re_match(cls, match): 96 | """Return an event constructed from a self.regex match""" 97 | kwargs = match.groupdict() 98 | location = kwargs['location'].split() 99 | kwargs['location'] = (int(location[0]), int(location[1]), 100 | int(location[2])) 101 | return cls(**kwargs) 102 | 103 | 104 | @python_2_unicode_compatible 105 | class CsgoAssistEvent(PlayerTargetEvent): 106 | 107 | """Player assist event""" 108 | 109 | regex = ''.join([ 110 | BaseEvent.regex, 111 | PlayerTargetEvent.player_regex, 112 | r' assisted killing ', 113 | PlayerTargetEvent.target_regex 114 | ]) 115 | 116 | def __init__(self, timestamp, player_name, player_uid, player_steam_id, 117 | player_team, target_name, target_uid, target_steam_id, 118 | target_team): 119 | super(CsgoAssistEvent, self).__init__(timestamp, player_name, 120 | player_uid, player_steam_id, 121 | player_team, target_name, target_uid, 122 | target_steam_id, target_team) 123 | 124 | def text(self): 125 | msg = '"%s" assisted killing "%s" ' % (self.player, self.target) 126 | return ' '.join([super(CsgoAssistEvent, self).text(), msg]) 127 | 128 | __str__ = text 129 | 130 | 131 | @python_2_unicode_compatible 132 | class CsgoKillEvent(KillEvent): 133 | 134 | """CS:GO specific kill event""" 135 | 136 | regex = ''.join([ 137 | BaseEvent.regex, 138 | PlayerTargetEvent.player_regex, 139 | r'\[(?P-?\d+ -?\d+ -?\d+)\]', 140 | r' killed ', 141 | PlayerTargetEvent.target_regex, 142 | r'\[(?P-?\d+ -?\d+ -?\d+)\]', 143 | r' with "(?P\w*)"', 144 | r'( \(headshot\))?', 145 | ]) 146 | 147 | def __init__(self, timestamp, player_name, player_uid, player_steam_id, 148 | player_team, player_location, target_name, target_uid, 149 | target_steam_id, target_team, target_location, weapon, 150 | headshot=False): 151 | super(CsgoKillEvent, self).__init__(timestamp, player_name, player_uid, 152 | player_steam_id, player_team, 153 | target_name, target_uid, 154 | target_steam_id, target_team, 155 | weapon) 156 | if (not isinstance(player_location, tuple) 157 | or not len(player_location) == 3): 158 | raise TypeError('Expected 3-tuple for player_location') 159 | if (not isinstance(target_location, tuple) 160 | or not len(target_location) == 3): 161 | raise TypeError('Expected 3-tuple for target_location') 162 | self.player_location = player_location 163 | self.target_location = target_location 164 | self.headshot = headshot 165 | 166 | def text(self): 167 | msg = [ 168 | 'L %s:' % (self.timestamp_to_str(self.timestamp)), 169 | '"%s" [%d %d %d]' % (self.player, self.player_location[0], 170 | self.player_location[1], 171 | self.player_location[2]), 172 | 'killed', 173 | '"%s" [%d %d %d]' % (self.target, self.target_location[0], 174 | self.target_location[1], 175 | self.target_location[2]), 176 | 'with "%s"' % (self.weapon), 177 | ] 178 | if self.headshot: 179 | msg.append('(headshot)') 180 | return ' '.join(msg) 181 | 182 | __str__ = text 183 | 184 | @classmethod 185 | def from_re_match(cls, match): 186 | """Return an event constructed from a self.regex match""" 187 | kwargs = match.groupdict() 188 | player_location = kwargs['player_location'].split() 189 | kwargs['player_location'] = (int(player_location[0]), 190 | int(player_location[1]), 191 | int(player_location[2])) 192 | target_location = kwargs['target_location'].split() 193 | kwargs['target_location'] = (int(target_location[0]), 194 | int(target_location[1]), 195 | int(target_location[2])) 196 | if match.string.endswith('(headshot)'): 197 | kwargs['headshot'] = True 198 | return cls(**kwargs) 199 | 200 | 201 | @python_2_unicode_compatible 202 | class CsgoAttackEvent(AttackEvent): 203 | 204 | """CS:GO specific attack event""" 205 | 206 | regex = ''.join([ 207 | BaseEvent.regex, 208 | PlayerTargetEvent.player_regex, 209 | r'\[(?P-?\d+ -?\d+ -?\d+)\]', 210 | r' attacked ', 211 | PlayerTargetEvent.target_regex, 212 | r'\[(?P-?\d+ -?\d+ -?\d+)\]', 213 | r' with "(?P\w*)"', 214 | r' \(damage "(?P\d+)"\)', 215 | r' \(damage_armor "(?P\d+)"\)', 216 | r' \(health "(?P\d+)"\)', 217 | r' \(armor "(?P\d+)"\)', 218 | r' \(hitgroup "(?P[\w ]+)"\)', 219 | ]) 220 | 221 | def __init__(self, timestamp, player_name, player_uid, player_steam_id, 222 | player_team, player_location, target_name, target_uid, 223 | target_steam_id, target_team, target_location, weapon, 224 | damage, damage_armor, health, armor, hitgroup): 225 | super(CsgoAttackEvent, self).__init__(timestamp, player_name, 226 | player_uid, player_steam_id, 227 | player_team, target_name, 228 | target_uid, target_steam_id, 229 | target_team, weapon, damage) 230 | if (not isinstance(player_location, tuple) 231 | or not len(player_location) == 3): 232 | raise TypeError('Expected 3-tuple for player_location') 233 | if (not isinstance(target_location, tuple) 234 | or not len(target_location) == 3): 235 | raise TypeError('Expected 3-tuple for target_location') 236 | self.player_location = player_location 237 | self.target_location = target_location 238 | self.damage_armor = int(damage_armor) 239 | self.health = int(health) 240 | self.armor = int(armor) 241 | self.hitgroup = hitgroup 242 | 243 | def text(self): 244 | msg = [ 245 | 'L %s:' % (self.timestamp_to_str(self.timestamp)), 246 | '"%s" [%d %d %d]' % (self.player, self.player_location[0], 247 | self.player_location[1], 248 | self.player_location[2]), 249 | 'attacked', 250 | '"%s" [%d %d %d]' % (self.target, self.target_location[0], 251 | self.target_location[1], 252 | self.target_location[2]), 253 | 'with "%s"' % (self.weapon), 254 | '(damage "%d")' % (self.damage), 255 | '(damage_armor "%d")' % (self.damage_armor), 256 | '(health "%d")' % (self.health), 257 | '(armor "%d")' % (self.armor), 258 | '(hitgroup "%s")' % (self.hitgroup), 259 | ] 260 | return ' '.join(msg) 261 | 262 | __str__ = text 263 | 264 | @classmethod 265 | def from_re_match(cls, match): 266 | """Return an event constructed from a self.regex match""" 267 | kwargs = match.groupdict() 268 | player_location = kwargs['player_location'].split() 269 | kwargs['player_location'] = (int(player_location[0]), 270 | int(player_location[1]), 271 | int(player_location[2])) 272 | target_location = kwargs['target_location'].split() 273 | kwargs['target_location'] = (int(target_location[0]), 274 | int(target_location[1]), 275 | int(target_location[2])) 276 | return cls(**kwargs) 277 | 278 | 279 | CSGO_EVENTS = [ 280 | SwitchTeamEvent, 281 | BuyEvent, 282 | ThrowEvent, 283 | CsgoAssistEvent, 284 | CsgoKillEvent, 285 | CsgoAttackEvent, 286 | ] 287 | -------------------------------------------------------------------------------- /srcds/events/generic.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Peter Rowlands 2 | """Generic Source engine events module 3 | 4 | Contains event classes for all standard HL/Source server events. 5 | 6 | Complies with the HL Log Standard rev. 1.03 7 | 8 | """ 9 | from __future__ import absolute_import, unicode_literals 10 | from future.utils import python_2_unicode_compatible 11 | 12 | from datetime import datetime 13 | 14 | from ..objects import BasePlayer, SteamId 15 | 16 | 17 | @python_2_unicode_compatible 18 | class BaseEvent(object): 19 | 20 | """Base source event class""" 21 | 22 | regex = ''.join([ 23 | r'^L (?P(0[0-9]|1[0-2])/([0-2][0-9]|3[0-1])/\d{4} - ', 24 | r'([0-1][0-9]|2[0-3])(:[0-5][0-9]|60){2}):\s*', 25 | ]) 26 | 27 | def __init__(self, timestamp): 28 | if isinstance(timestamp, datetime): 29 | self.timestamp = timestamp 30 | else: 31 | self.timestamp = datetime.strptime(timestamp, 32 | '%m/%d/%Y - %H:%M:%S') 33 | 34 | def text(self): 35 | """Return a valid HL Log Standard log entry string""" 36 | return 'L {}:'.format(self.timestamp_to_str(self.timestamp)) 37 | 38 | __str__ = text 39 | 40 | @classmethod 41 | def timestamp_to_str(cls, timestamp): 42 | """Return a valid HL Log Standard timestamp string""" 43 | if not isinstance(timestamp, datetime): 44 | raise TypeError('Expected datetime instance for timestamp') 45 | return timestamp.strftime('%m/%d/%Y - %H:%M:%S') 46 | 47 | @classmethod 48 | def from_re_match(cls, match): 49 | """Return an event constructed from a self.regex match""" 50 | return cls(**match.groupdict()) 51 | 52 | 53 | @python_2_unicode_compatible 54 | class CvarEvent(BaseEvent): 55 | 56 | """Cvar change event""" 57 | 58 | regex = ''.join([ 59 | BaseEvent.regex, 60 | r'Server (cvars (start|end)|cvar "(?P\w*)" = "(?P\w*)")', 61 | ]) 62 | 63 | def __init__(self, timestamp, cvar='', value='', start=False, end=False): 64 | super(CvarEvent, self).__init__(timestamp) 65 | self.cvar = cvar 66 | self.value = value 67 | self.start = start 68 | self.end = end 69 | 70 | def text(self): 71 | if self.start: 72 | msg = 'Server cvars start' 73 | elif self.end: 74 | msg = 'Server cvars end' 75 | else: 76 | msg = 'Server cvar "%s" = "%s"' % (self.cvar, self.value) 77 | return ' '.join([super(CvarEvent, self).text(), msg]) 78 | 79 | __str__ = text 80 | 81 | @classmethod 82 | def from_re_match(cls, match): 83 | """Return an event constructed from a self.regex match""" 84 | kwargs = match.groupdict() 85 | if match.string.find('start') >= 0: 86 | kwargs['start'] = True 87 | elif match.string.find('end') >= 0: 88 | kwargs['end'] = True 89 | return cls(**kwargs) 90 | 91 | 92 | @python_2_unicode_compatible 93 | class LogFileEvent(BaseEvent): 94 | 95 | """Log file change event""" 96 | 97 | regex = ''.join([ 98 | BaseEvent.regex, 99 | r'Log file (closed|started \(file "(?P.*)"\) ', 100 | r'\(game "(?P.*)"\) \(version "(?P.*)"\))', 101 | ]) 102 | 103 | def __init__(self, timestamp, filename='', game='', version='', 104 | started=False, closed=False): 105 | if not started != closed: 106 | raise ValueError('Invalid event') 107 | super(LogFileEvent, self).__init__(timestamp) 108 | self.filename = filename 109 | self.game = game 110 | self.version = version 111 | self.started = started 112 | self.closed = closed 113 | 114 | def text(self): 115 | if self.started: 116 | msg = 'Log file started (file "%s") (game "%s") (version "%s")' % ( 117 | self.filename, self.game, self.version) 118 | else: 119 | msg = 'Log file closed' 120 | return ' '.join([super(LogFileEvent, self).text(), msg]) 121 | 122 | __str__ = text 123 | 124 | @classmethod 125 | def from_re_match(cls, match): 126 | """Return an event constructed from a self.regex match""" 127 | kwargs = match.groupdict() 128 | if match.string.endswith('closed'): 129 | kwargs['closed'] = True 130 | else: 131 | kwargs['started'] = True 132 | return cls(**kwargs) 133 | 134 | 135 | @python_2_unicode_compatible 136 | class ChangeMapEvent(BaseEvent): 137 | 138 | """Map change event""" 139 | 140 | regex = ''.join([ 141 | BaseEvent.regex, 142 | r'(Loading|Started) map "(?P.*?)"( \(CRC "(?P-?\d+)"\))?', 143 | ]) 144 | 145 | def __init__(self, timestamp, mapname, loading=False, started=False, 146 | crc=''): 147 | if not loading != started: 148 | raise ValueError('Invalid event') 149 | super(ChangeMapEvent, self).__init__(timestamp) 150 | self.mapname = mapname 151 | self.loading = loading 152 | self.started = started 153 | self.crc = crc 154 | 155 | def text(self): 156 | if self.loading: 157 | msg = 'Loading map "%s"' % (self.mapname) 158 | else: 159 | msg = 'Started map "%s" (CRC "%s")' % (self.mapname, self.crc) 160 | return ' '.join([super(ChangeMapEvent, self).text(), msg]) 161 | 162 | __str__ = text 163 | 164 | @classmethod 165 | def from_re_match(cls, match): 166 | """Return an event constructed from a self.regex match""" 167 | kwargs = match.groupdict() 168 | if match.string.startswith('Loading'): 169 | kwargs['loading'] = True 170 | else: 171 | kwargs['started'] = True 172 | return cls(**kwargs) 173 | 174 | 175 | @python_2_unicode_compatible 176 | class RconEvent(BaseEvent): 177 | 178 | """Rcon event""" 179 | 180 | regex = ''.join([ 181 | BaseEvent.regex, 182 | r'(Bad )?Rcon: "rcon challenge "(?P\w*)" from ', 183 | r'"(?P\w*):(?P\d{0-5})"', 184 | ]) 185 | 186 | def __init__(self, timestamp, password, address, passed=False): 187 | super(RconEvent, self).__init__(timestamp) 188 | self.password = password 189 | if not isinstance(tuple, address) or len(address) != 2: 190 | raise TypeError('Expected 2-tuple (host, port) for address') 191 | self.address = address 192 | self.passed = passed 193 | 194 | def text(self): 195 | if self.passed: 196 | msg = 'Rcon: "rcon challenge "%s" command" from "%s:%d"' % ( 197 | self.password, self.address[0], self.address[1]) 198 | else: 199 | msg = 'Bad Rcon: "rcon challenge "%s" command" from "%s:%d"' % ( 200 | self.password, self.address[0], self.address[1]) 201 | return ' '.join([super(RconEvent, self).text(), msg]) 202 | 203 | __str__ = text 204 | 205 | @classmethod 206 | def from_re_match(cls, match): 207 | """Return an event constructed from a self.regex match""" 208 | kwargs = match.groupdict() 209 | if not match.string.startswith('Bad'): 210 | kwargs['passed'] = True 211 | return cls(**kwargs) 212 | 213 | 214 | @python_2_unicode_compatible 215 | class PlayerEvent(BaseEvent): 216 | 217 | """Base class for events involving a single player""" 218 | 219 | regex = ''.join([ 220 | BaseEvent.regex, 221 | r'"(?P.*)<(?P\d*)><(?P[\w:]*)>', 222 | r'<(?P\w*)>"\s*', 223 | ]) 224 | 225 | def __init__(self, timestamp, player_name, uid, steam_id, team=''): 226 | super(PlayerEvent, self).__init__(timestamp) 227 | self.player = BasePlayer(player_name, uid, SteamId(steam_id), team) 228 | 229 | def text(self): 230 | msg = '"%s"' % self.player 231 | return ' '.join([super(PlayerEvent, self).text(), msg]) 232 | 233 | __str__ = text 234 | 235 | 236 | @python_2_unicode_compatible 237 | class ConnectionEvent(PlayerEvent): 238 | 239 | """Player connection event""" 240 | 241 | regex = ''.join([ 242 | PlayerEvent.regex, 243 | r'connected, address "((?P
none)|(?P\d+(\.\d+){3}):(?P\d*))"' 244 | ]) 245 | 246 | def __init__(self, timestamp, player_name, uid, steam_id, team, address): 247 | if (address == 'none' or 248 | (isinstance(address, tuple) and len(address) == 2)): 249 | self.address = address 250 | else: 251 | raise TypeError('Expected 2-tuple (host, port) for address: %s', address) 252 | super(ConnectionEvent, self).__init__(timestamp, player_name, uid, 253 | steam_id, team) 254 | 255 | def text(self): 256 | if isinstance(self.address, tuple): 257 | msg = 'connected, address "%s:%d"' % (self.address[0], 258 | self.address[1]) 259 | else: 260 | msg = 'connected, address "%s"' % (self.address) 261 | return ' '.join([super(ConnectionEvent, self).text(), msg]) 262 | 263 | __str__ = text 264 | 265 | @classmethod 266 | def from_re_match(cls, match): 267 | """Return an event constructed from a self.regex match""" 268 | kwargs = match.groupdict() 269 | if kwargs['address'] != 'none': 270 | kwargs['address'] = (kwargs['host'], int(kwargs['port'])) 271 | del kwargs['host'] 272 | del kwargs['port'] 273 | return cls(**kwargs) 274 | 275 | 276 | @python_2_unicode_compatible 277 | class ValidationEvent(PlayerEvent): 278 | 279 | """Player validation event""" 280 | 281 | regex = ''.join([ 282 | PlayerEvent.regex, 283 | r'STEAM USERID validated', 284 | ]) 285 | 286 | def text(self): 287 | msg = 'STEAM USERID validated' 288 | return ' '.join([super(ValidationEvent, self).text(), msg]) 289 | 290 | __str__ = text 291 | 292 | 293 | @python_2_unicode_compatible 294 | class EnterGameEvent(PlayerEvent): 295 | 296 | """Player entered game event""" 297 | 298 | regex = ''.join([ 299 | PlayerEvent.regex, 300 | r'entered the game', 301 | ]) 302 | 303 | def text(self): 304 | msg = 'entered the game' 305 | return ' '.join([super(EnterGameEvent, self).text(), msg]) 306 | 307 | __str__ = text 308 | 309 | 310 | @python_2_unicode_compatible 311 | class DisconnectionEvent(PlayerEvent): 312 | 313 | """Player disconnected event""" 314 | 315 | regex = ''.join([ 316 | PlayerEvent.regex, 317 | r'disconnected', 318 | ]) 319 | 320 | def text(self): 321 | msg = 'disconnected' 322 | return ' '.join([super(DisconnectionEvent, self).text(), msg]) 323 | 324 | __str__ = text 325 | 326 | 327 | @python_2_unicode_compatible 328 | class KickEvent(PlayerEvent): 329 | 330 | """Player kicked by console event""" 331 | 332 | regex = ''.join([ 333 | BaseEvent.regex, 334 | r'Kick: "(?P.*)<(?P\d*)><(?P[\w:]*)>', 335 | r'<(?P\w*)>" was kicked by "Console" ', 336 | r'\(message "(?P.*)"\)', 337 | ]) 338 | 339 | def __init__(self, timestamp, player_name, uid, steam_id, team, message): 340 | super(KickEvent, self).__init__(timestamp, player_name, uid, 341 | steam_id, team) 342 | self.message = message 343 | 344 | def text(self): 345 | return ' '.join([ 346 | 'L %s:' % (self.timestamp_to_str(self.timestamp)), 347 | 'Kick: "%s"' % (self.player), 348 | 'was kicked by "Console" (message "%s")' % (self.message), 349 | ]) 350 | 351 | __str__ = text 352 | 353 | 354 | @python_2_unicode_compatible 355 | class SuicideEvent(PlayerEvent): 356 | 357 | """Player suicide event""" 358 | 359 | regex = ''.join([ 360 | PlayerEvent.regex, 361 | r'committed suicide with "(?P\w*)"', 362 | ]) 363 | 364 | def __init__(self, timestamp, player_name, uid, steam_id, team, weapon): 365 | super(SuicideEvent, self).__init__(timestamp, player_name, uid, 366 | steam_id, team) 367 | self.weapon = weapon 368 | 369 | def text(self): 370 | msg = 'committed suicide with "%s"' % (self.weapon) 371 | return ' '.join([super(SuicideEvent, self).text(), msg]) 372 | 373 | __str__ = text 374 | 375 | 376 | @python_2_unicode_compatible 377 | class TeamSelectionEvent(PlayerEvent): 378 | 379 | """Player team select event""" 380 | 381 | regex = ''.join([ 382 | PlayerEvent.regex, 383 | r'joined team "(?P\w*)"', 384 | ]) 385 | 386 | def __init__(self, timestamp, player_name, uid, steam_id, team, 387 | new_team): 388 | super(TeamSelectionEvent, self).__init__(timestamp, player_name, uid, 389 | steam_id, team) 390 | self.new_team = new_team 391 | 392 | def text(self): 393 | msg = 'joined team "%s"' % (self.new_team) 394 | return ' '.join([super(TeamSelectionEvent, self).text(), msg]) 395 | 396 | __str__ = text 397 | 398 | 399 | @python_2_unicode_compatible 400 | class RoleSelectionEvent(PlayerEvent): 401 | 402 | """Player role select event""" 403 | 404 | regex = ''.join([ 405 | PlayerEvent.regex, 406 | r'changed role to "(?P\w*)"', 407 | ]) 408 | 409 | def __init__(self, timestamp, player_name, uid, steam_id, team, 410 | role): 411 | super(RoleSelectionEvent, self).__init__(timestamp, player_name, uid, 412 | steam_id, team) 413 | self.role = role 414 | 415 | def text(self): 416 | msg = 'changed role to "%s"' % (self.role) 417 | return ' '.join([super(RoleSelectionEvent, self).text(), msg]) 418 | 419 | __str__ = text 420 | 421 | 422 | @python_2_unicode_compatible 423 | class ChangeNameEvent(PlayerEvent): 424 | 425 | """Player name changed event""" 426 | 427 | regex = ''.join([ 428 | PlayerEvent.regex, 429 | r'changed name to "(?P.*)"', 430 | ]) 431 | 432 | def __init__(self, timestamp, player_name, uid, steam_id, team, 433 | new_name): 434 | super(ChangeNameEvent, self).__init__(timestamp, player_name, uid, 435 | steam_id, team) 436 | self.new_name = new_name 437 | 438 | def text(self): 439 | msg = 'changed name to "%s"' % (self.new_name) 440 | return ' '.join([super(ChangeNameEvent, self).text(), msg]) 441 | 442 | __str__ = text 443 | 444 | 445 | @python_2_unicode_compatible 446 | class PlayerTargetEvent(BaseEvent): 447 | 448 | """Base class for events involving two players""" 449 | 450 | player_regex = ''.join([ 451 | r'"(?P.*)<(?P\d*)>', 452 | r'<(?P[\w:]*)><(?P\w*)>"\s*', 453 | ]) 454 | target_regex = ''.join([ 455 | r'"(?P.*)<(?P\d*)>', 456 | r'<(?P[\w:]*)><(?P\w*)>"\s*', 457 | ]) 458 | 459 | def __init__(self, timestamp, player_name, player_uid, player_steam_id, 460 | player_team, target_name, target_uid, target_steam_id, 461 | target_team): 462 | super(PlayerTargetEvent, self).__init__(timestamp) 463 | self.player = BasePlayer(player_name, player_uid, 464 | SteamId(player_steam_id), player_team) 465 | self.target = BasePlayer(target_name, target_uid, 466 | SteamId(target_steam_id), target_team) 467 | 468 | 469 | @python_2_unicode_compatible 470 | class KillEvent(PlayerTargetEvent): 471 | 472 | """Player killed event""" 473 | 474 | regex = ''.join([ 475 | BaseEvent.regex, 476 | PlayerTargetEvent.player_regex, 477 | r' killed ', 478 | PlayerTargetEvent.target_regex, 479 | r' with "(?P\w*)"', 480 | ]) 481 | 482 | def __init__(self, timestamp, player_name, player_uid, player_steam_id, 483 | player_team, target_name, target_uid, target_steam_id, 484 | target_team, weapon): 485 | super(KillEvent, self).__init__(timestamp, player_name, player_uid, 486 | player_steam_id, player_team, 487 | target_name, target_uid, 488 | target_steam_id, target_team) 489 | self.weapon = weapon 490 | 491 | def text(self): 492 | msg = '"%s" killed "%s" with "%s"' % (self.player, self.target, 493 | self.weapon) 494 | return ' '.join([super(KillEvent, self).text(), msg]) 495 | 496 | __str__ = text 497 | 498 | 499 | @python_2_unicode_compatible 500 | class AttackEvent(PlayerTargetEvent): 501 | 502 | """Player attacked event""" 503 | 504 | regex = ''.join([ 505 | BaseEvent.regex, 506 | PlayerTargetEvent.player_regex, 507 | r' attacked ', 508 | PlayerTargetEvent.target_regex, 509 | r' with "(?P\w*)" \(damage "(?P\d*)"\)', 510 | ]) 511 | 512 | def __init__(self, timestamp, player_name, player_uid, player_steam_id, 513 | player_team, target_name, target_uid, target_steam_id, 514 | target_team, weapon, damage): 515 | super(AttackEvent, self).__init__(timestamp, player_name, player_uid, 516 | player_steam_id, player_team, 517 | target_name, target_uid, 518 | target_steam_id, target_team) 519 | self.weapon = weapon 520 | self.damage = int(damage) 521 | 522 | def text(self): 523 | msg = '"%s" attacked "%s" with "%s" (damage "%d")' % (self.player, 524 | self.target, 525 | self.weapon, 526 | self.damage) 527 | return ' '.join([super(AttackEvent, self).text(), msg]) 528 | 529 | __str__ = text 530 | 531 | 532 | @python_2_unicode_compatible 533 | class PlayerActionEvent(PlayerEvent): 534 | 535 | """Player triggered action event""" 536 | 537 | regex = ''.join([ 538 | PlayerEvent.regex, 539 | r'triggered "(?P.*?)"', 540 | ]) 541 | 542 | def __init__(self, timestamp, player_name, uid, steam_id, team, 543 | action): 544 | super(PlayerActionEvent, self).__init__(timestamp, player_name, uid, 545 | steam_id, team) 546 | self.action = action 547 | 548 | def text(self): 549 | msg = 'triggered "%s"' % (self.action) 550 | return ' '.join([super(PlayerActionEvent, self).text(), msg]) 551 | 552 | __str__ = text 553 | 554 | 555 | @python_2_unicode_compatible 556 | class TeamActionEvent(BaseEvent): 557 | 558 | """Team triggered action event""" 559 | 560 | regex = ''.join([ 561 | BaseEvent.regex, 562 | r'Team "(?P\w*?)" triggered "(?P.*?)"', 563 | ]) 564 | 565 | def __init__(self, timestamp, team, action): 566 | super(TeamActionEvent, self).__init__(timestamp) 567 | self.team = team 568 | self.action = action 569 | 570 | def text(self): 571 | msg = 'Team "%s" triggered "%s"' % (self.team, self.action) 572 | return ' '.join([super(TeamActionEvent, self).text(), msg]) 573 | 574 | __str__ = text 575 | 576 | 577 | @python_2_unicode_compatible 578 | class WorldActionEvent(BaseEvent): 579 | 580 | """World triggered action event""" 581 | 582 | regex = ''.join([ 583 | BaseEvent.regex, 584 | r'World triggered "(?P.*?)"', 585 | ]) 586 | 587 | def __init__(self, timestamp, action): 588 | super(WorldActionEvent, self).__init__(timestamp) 589 | self.action = action 590 | 591 | def text(self): 592 | msg = 'World triggered "%s"' % (self.action) 593 | return ' '.join([super(WorldActionEvent, self).text(), msg]) 594 | 595 | __str__ = text 596 | 597 | 598 | @python_2_unicode_compatible 599 | class ChatEvent(PlayerEvent): 600 | 601 | """Chat event""" 602 | 603 | regex = ''.join([ 604 | PlayerEvent.regex, 605 | r'say(_team)? "(?P.*?)"', 606 | ]) 607 | 608 | def __init__(self, timestamp, player_name, uid, steam_id, team, 609 | message, say_team=False): 610 | super(ChatEvent, self).__init__(timestamp, player_name, uid, steam_id, 611 | team) 612 | self.say_team = say_team 613 | self.message = message 614 | 615 | def text(self): 616 | if self.say_team: 617 | msg = 'say_team "%s"' % (self.message) 618 | else: 619 | msg = 'say "%s"' % (self.message) 620 | return ' '.join([super(ChatEvent, self).text(), msg]) 621 | 622 | __str__ = text 623 | 624 | @classmethod 625 | def from_re_match(cls, match): 626 | """Return an event constructed from a self.regex match""" 627 | kwargs = match.groupdict() 628 | if match.string.find('say_team') >= 0: 629 | kwargs['say_team'] = True 630 | return cls(**kwargs) 631 | 632 | 633 | @python_2_unicode_compatible 634 | class TeamAllianceEvent(BaseEvent): 635 | 636 | """Team alliance event""" 637 | 638 | regex = ''.join([ 639 | BaseEvent.regex, 640 | r'Team "(?P\w*?)" formed alliance with "(?P\w*?)"', 641 | ]) 642 | 643 | def __init__(self, timestamp, team_a, team_b): 644 | super(TeamAllianceEvent, self).__init__(timestamp) 645 | self.team_a = team_a 646 | self.team_b = team_b 647 | 648 | def text(self): 649 | msg = 'Team "%s" formed alliance with "%s"' % (self.team_a, 650 | self.team_b) 651 | return ' '.join([super(TeamAllianceEvent, self).text(), msg]) 652 | 653 | __str__ = text 654 | 655 | 656 | @python_2_unicode_compatible 657 | class RoundEndTeamEvent(BaseEvent): 658 | 659 | """Round end team score report event""" 660 | 661 | regex = ''.join([ 662 | BaseEvent.regex, 663 | r'Team "(?P\w*?)" scored "(?P\d+)" with ', 664 | r'"(?P\d+)" players', 665 | ]) 666 | 667 | def __init__(self, timestamp, team, score, num_players): 668 | super(RoundEndTeamEvent, self).__init__(timestamp) 669 | self.team = team 670 | self.score = int(score) 671 | self.num_players = int(num_players) 672 | 673 | def text(self): 674 | msg = 'Team "%s" scored "%d" with "%d" players' % (self.team, 675 | self.score, 676 | self.num_players) 677 | return ' '.join([super(RoundEndTeamEvent, self).text(), msg]) 678 | 679 | __str__ = text 680 | 681 | 682 | @python_2_unicode_compatible 683 | class PrivateChatEvent(PlayerTargetEvent): 684 | 685 | """Private Chat event""" 686 | 687 | regex = ''.join([ 688 | BaseEvent.regex, 689 | PlayerTargetEvent.player_regex, 690 | r' tell ', 691 | PlayerTargetEvent.target_regex, 692 | r' message "(?P.*)"', 693 | ]) 694 | 695 | def __init__(self, timestamp, player_name, player_uid, player_steam_id, 696 | player_team, target_name, target_uid, target_steam_id, 697 | target_team, message): 698 | super(PrivateChatEvent, self).__init__(timestamp, player_name, 699 | player_uid, player_steam_id, 700 | player_team, target_name, 701 | target_uid, target_steam_id, 702 | target_team) 703 | self.message = message 704 | 705 | def text(self): 706 | msg = '"%s" tell "%s" message "%s"' % (self.player, self.target, 707 | self.message) 708 | return ' '.join([super(PrivateChatEvent, self).text(), msg]) 709 | 710 | __str__ = text 711 | 712 | 713 | @python_2_unicode_compatible 714 | class RoundEndPlayerEvent(PlayerEvent): 715 | 716 | """Round end player score report event""" 717 | 718 | regex = ''.join([ 719 | BaseEvent.regex, 720 | r'Player "(?P.*)<(?P\d*)><(?P[\w:]*)>', 721 | r'<(?P\w*)>"\s*', 722 | r'scored "(?P\d+)"', 723 | ]) 724 | 725 | def __init__(self, timestamp, player_name, uid, steam_id, team, score): 726 | super(RoundEndPlayerEvent, self).__init__(timestamp, player_name, uid, 727 | steam_id, team) 728 | self.score = int(score) 729 | 730 | def text(self): 731 | return ' '.join([ 732 | 'L %s:' % (self.timestamp_to_str(self.timestamp)), 733 | 'Player "%s"' % (self.player), 734 | 'scored "%d"' % (self.score), 735 | ]) 736 | 737 | __str__ = text 738 | 739 | 740 | @python_2_unicode_compatible 741 | class WeaponSelectEvent(PlayerEvent): 742 | 743 | """Player selected weapon event""" 744 | 745 | regex = ''.join([ 746 | PlayerEvent.regex, 747 | r'selected weapon "(?P\w*)"', 748 | ]) 749 | 750 | def __init__(self, timestamp, player_name, uid, steam_id, team, 751 | weapon): 752 | super(WeaponSelectEvent, self).__init__(timestamp, player_name, uid, 753 | steam_id, team) 754 | self.weapon = weapon 755 | 756 | def text(self): 757 | msg = 'selected weapon "%s"' % (self.weapon) 758 | return ' '.join([super(WeaponSelectEvent, self).text(), msg]) 759 | 760 | __str__ = text 761 | 762 | 763 | @python_2_unicode_compatible 764 | class WeaponPickupEvent(PlayerEvent): 765 | 766 | """Player picked up weapon event""" 767 | 768 | regex = ''.join([ 769 | PlayerEvent.regex, 770 | r'acquired weapon "(?P\w*)"', 771 | ]) 772 | 773 | def __init__(self, timestamp, player_name, uid, steam_id, team, 774 | weapon): 775 | super(WeaponPickupEvent, self).__init__(timestamp, player_name, uid, 776 | steam_id, team) 777 | self.weapon = weapon 778 | 779 | def text(self): 780 | msg = 'acquired weapon "%s"' % (self.weapon) 781 | return ' '.join([super(WeaponPickupEvent, self).text(), msg]) 782 | 783 | __str__ = text 784 | 785 | 786 | STANDARD_EVENTS = [ 787 | CvarEvent, 788 | LogFileEvent, 789 | ChangeMapEvent, 790 | RconEvent, 791 | ConnectionEvent, 792 | ValidationEvent, 793 | EnterGameEvent, 794 | DisconnectionEvent, 795 | KickEvent, 796 | SuicideEvent, 797 | TeamSelectionEvent, 798 | RoleSelectionEvent, 799 | ChangeNameEvent, 800 | KillEvent, 801 | AttackEvent, 802 | PlayerActionEvent, 803 | TeamActionEvent, 804 | WorldActionEvent, 805 | ChatEvent, 806 | TeamAllianceEvent, 807 | RoundEndTeamEvent, 808 | PrivateChatEvent, 809 | RoundEndPlayerEvent, 810 | WeaponSelectEvent, 811 | WeaponPickupEvent, 812 | ] 813 | -------------------------------------------------------------------------------- /srcds/logparser.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Peter Rowlands 2 | """ 3 | Source server log parsing module 4 | 5 | Complies with the HL Log Standard rev. 1.03 6 | 7 | """ 8 | 9 | from __future__ import division, absolute_import 10 | import os 11 | import re 12 | from collections import deque 13 | 14 | from .events import generic 15 | 16 | 17 | class UnknownEventError(Exception): 18 | pass 19 | 20 | 21 | class SourceLogParser(object): 22 | 23 | """HL Log Standard parser class""" 24 | 25 | def __init__(self, default_events=True, skip_unknowns=True): 26 | self.events = deque() 27 | self.events_types = [] 28 | self.skip_unknowns = skip_unknowns 29 | if default_events: 30 | self.add_event_types(generic.STANDARD_EVENTS) 31 | 32 | def add_event_types(self, event_types=[]): 33 | """Add event types""" 34 | for cls in event_types: 35 | regex = re.compile(cls.regex, re.U) 36 | self.events_types.append((regex, cls)) 37 | 38 | def parse_line(self, line): 39 | """Parse a single log line""" 40 | line = line.strip() 41 | for (regex, cls) in self.events_types: 42 | match = regex.match(line) 43 | if match: 44 | event = cls.from_re_match(match) 45 | self.events.append(event) 46 | return 47 | if not self.skip_unknowns: 48 | raise UnknownEventError('Could not parse event: %s' % line) 49 | 50 | def read(self, filename): 51 | """Read in a log file""" 52 | fd = open(filename) 53 | for line in fd.readlines(): 54 | self.parse_line(line) 55 | fd.close() 56 | 57 | def write(self, fileobject): 58 | """Write the events back to a file object""" 59 | lines = [] 60 | for event in self.events: 61 | lines.append(str(self.event)) 62 | fileobject.write(os.linesep.join(lines)) 63 | -------------------------------------------------------------------------------- /srcds/objects.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Peter Rowlands 2 | """ 3 | Source server objects module 4 | 5 | Contains base classes for all standard Source server objects. 6 | 7 | """ 8 | 9 | from __future__ import division, unicode_literals 10 | from future.utils import python_2_unicode_compatible 11 | 12 | import re 13 | 14 | 15 | STEAM_ACCOUNT_UNIVERSE = { 16 | 'individual': 0, 17 | 'public': 1, 18 | 'beta': 2, 19 | 'internal': 3, 20 | 'dev': 4, 21 | 'rc': 5, 22 | } 23 | 24 | STEAM_ACCOUNT_TYPE = { 25 | 'invalid': 0, 26 | 'individual': 1, 27 | 'multiseat': 2, 28 | 'gameserver': 3, 29 | 'anongameserver': 4, 30 | 'pending': 5, 31 | 'contentserver': 6, 32 | 'clan': 7, 33 | 'chat': 8, 34 | 'p2p_superseeder': 9, 35 | 'anonuser': 10, 36 | } 37 | 38 | 39 | @python_2_unicode_compatible 40 | class SteamId(object): 41 | 42 | """Steam ID class""" 43 | 44 | def __init__(self, steam_id, id_type=STEAM_ACCOUNT_TYPE['individual']): 45 | """Initialize a SteamId object 46 | 47 | Args: 48 | steam_id: A valid SteamID. Accepts a string in STEAM_X:Y:Z format, 49 | or as a 64-bit integer. 50 | 51 | """ 52 | self.is_bot = False 53 | self.is_console = False 54 | if isinstance(steam_id, int): 55 | (self.id_number, 56 | self.y_part, 57 | self.instance, 58 | self.id_type, 59 | self.universe) = self.split_id64(steam_id) 60 | else: 61 | if str(steam_id) == u'BOT': 62 | self.is_bot = True 63 | elif str(steam_id) == u'Console': 64 | self.is_console = True 65 | else: 66 | pattern = ''.join([ 67 | r'STEAM_(?P[0-5]):(?P\d+):', 68 | r'(?P\d+)', 69 | ]) 70 | match = re.match(pattern, steam_id, re.I | re.U) 71 | if not match: 72 | raise ValueError('Invalid string steam_id: %s' % steam_id) 73 | self.universe = int(match.groupdict()['universe']) 74 | self.instance = 1 75 | self.y_part = int(match.groupdict()['y_part']) 76 | self.id_number = int(match.groupdict()['id_number']) 77 | self.id_type = id_type 78 | 79 | def __str__(self): 80 | if self.is_bot: 81 | return u'BOT' 82 | elif self.is_console: 83 | return u'Console' 84 | else: 85 | return self.id64_to_str(self.id64()) 86 | 87 | def id64(self): 88 | """Return the SteamID64 for this ID""" 89 | if self.is_bot or self.is_console: 90 | return 0 91 | id64 = self.id_number * 2 92 | id64 += self.y_part 93 | id64 |= self.instance << 32 94 | id64 |= self.id_type << 52 95 | id64 |= self.universe << 56 96 | return id64 97 | 98 | @classmethod 99 | def id64_to_str(cls, id64, universe=STEAM_ACCOUNT_UNIVERSE['public']): 100 | """Convert a SteamID64 to a STEAM_X:Y:Z string""" 101 | (id_number, y_part, instance, id_type, universe) = SteamId.split_id64(id64) 102 | return u'STEAM_%d:%d:%d' % (universe, y_part, id_number) 103 | 104 | @classmethod 105 | def split_id64(cls, id64): 106 | """Return a tuple of (id, y_part, instance, type, universe)""" 107 | y_part = id64 % 2 108 | id_number = (id64 & 0xffffffff - y_part) // 2 109 | instance = (id64 & 0x000fffff00000000) >> 32 110 | id_type = (id64 & 0x00f0000000000000) >> 52 111 | universe = (id64 & 0xff00000000000000) >> 56 112 | return (id_number, y_part, instance, id_type, universe) 113 | 114 | 115 | @python_2_unicode_compatible 116 | class BasePlayer(object): 117 | 118 | """Source player object""" 119 | 120 | def __init__(self, name, uid, steam_id, team=u''): 121 | if not isinstance(steam_id, SteamId): 122 | raise TypeError('Expected type SteamId for steam_id') 123 | self.name = name 124 | self.uid = int(uid) 125 | self.steam_id = steam_id 126 | if team is None: 127 | team = u'' 128 | self.team = team 129 | 130 | def __str__(self): 131 | msg = [ 132 | self.name, 133 | '<%d>' % self.uid, 134 | '<%s>' % self.steam_id, 135 | ] 136 | if self.team is not None: 137 | msg.append(u'<%s>' % self.team) 138 | return ''.join(msg) 139 | -------------------------------------------------------------------------------- /srcds/rcon.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Peter Rowlands 2 | """Source server RCON communications module""" 3 | 4 | import struct 5 | import socket 6 | import itertools 7 | 8 | 9 | # Packet types 10 | SERVERDATA_AUTH = 3 11 | SERVERDATA_AUTH_RESPONSE = 2 12 | SERVERDATA_EXECCOMMAND = 2 13 | SERVERDATA_RESPONSE_VALUE = 0 14 | 15 | 16 | class RconPacket(object): 17 | """RCON packet""" 18 | 19 | def __init__(self, pkt_id=0, pkt_type=-1, body=''): 20 | self.pkt_id = pkt_id 21 | self.pkt_type = pkt_type 22 | self.body = body 23 | 24 | def __str__(self): 25 | """Return the body string.""" 26 | return self.body 27 | 28 | def size(self): 29 | """Return the pkt_size field for this packet.""" 30 | return len(self.body) + 10 31 | 32 | def pack(self): 33 | """Return the packed version of the packet.""" 34 | return struct.pack('<3i{0}s'.format(len(self.body) + 2), 35 | self.size(), self.pkt_id, self.pkt_type, 36 | bytearray(self.body, 'utf-8')) 37 | 38 | 39 | class RconConnection(object): 40 | """RCON client to server connection""" 41 | 42 | def __init__(self, server, port=27015, password='', single_packet_mode=False): 43 | """Construct an RconConnection. 44 | 45 | Parameters: 46 | server (str) server hostname or IP address 47 | port (int) server port number 48 | password (str) server RCON password 49 | single_packet_mode (bool) set to True for servers which do not hand 0-length SERVERDATA_RESPONSE_VALUE 50 | requests (i.e. Factorio). 51 | 52 | """ 53 | self.server = server 54 | self.port = port 55 | self.single_packet_mode = single_packet_mode 56 | self._sock = socket.create_connection((server, port)) 57 | self.pkt_id = itertools.count(1) 58 | self._authenticate(password) 59 | 60 | def _authenticate(self, password): 61 | """Authenticate with the server using the given password.""" 62 | auth_pkt = RconPacket(next(self.pkt_id), SERVERDATA_AUTH, password) 63 | self._send_pkt(auth_pkt) 64 | # The server should respond with a SERVERDATA_RESPONSE_VALUE followed by SERVERDATA_AUTH_RESPONSE. 65 | # Note that some server types omit the initial SERVERDATA_RESPONSE_VALUE packet. 66 | auth_resp = self.read_response(auth_pkt) 67 | if auth_resp.pkt_type == SERVERDATA_RESPONSE_VALUE: 68 | auth_resp = self.read_response() 69 | if auth_resp.pkt_type != SERVERDATA_AUTH_RESPONSE: 70 | raise RconError('Received invalid auth response packet') 71 | if auth_resp.pkt_id == -1: 72 | raise RconAuthError('Bad password') 73 | 74 | def exec_command(self, command): 75 | """Execute the given RCON command. 76 | 77 | Parameters: 78 | command (str) the RCON command string (ex. "status") 79 | 80 | Returns the response body 81 | """ 82 | cmd_pkt = RconPacket(next(self.pkt_id), SERVERDATA_EXECCOMMAND, 83 | command) 84 | self._send_pkt(cmd_pkt) 85 | resp = self.read_response(cmd_pkt, True) 86 | return resp.body 87 | 88 | def _send_pkt(self, pkt): 89 | """Send one RCON packet over the connection. 90 | 91 | Raises: 92 | RconSizeError if the size of the specified packet is > 4096 bytes 93 | """ 94 | if pkt.size() > 4096: 95 | raise RconSizeError('pkt_size > 4096 bytes') 96 | data = pkt.pack() 97 | self._sock.sendall(data) 98 | 99 | def _recv_pkt(self): 100 | """Read one RCON packet""" 101 | while True: 102 | header = self._sock.recv(struct.calcsize('<3i')) 103 | if len(header) != 0: 104 | break 105 | 106 | (pkt_size, pkt_id, pkt_type) = struct.unpack('<3i', header) 107 | body = self._sock.recv(pkt_size - 8) 108 | return RconPacket(pkt_id, pkt_type, body) 109 | 110 | def read_response(self, request=None, multi=False): 111 | """Return the next response packet. 112 | 113 | Parameters: 114 | request (RconPacket) if request is provided, read_response() will check that the response ID matches the 115 | specified request ID 116 | multi (bool) set to True if read_response() should check for a multi packet response. If the current 117 | RconConnection has single_packet_mode enabled, this parameter is ignored. 118 | 119 | Raises: 120 | RconError if an error occurred while receiving the server response 121 | """ 122 | if request and not isinstance(request, RconPacket): 123 | raise TypeError('Expected RconPacket type for request') 124 | if not self.single_packet_mode and multi: 125 | if not request: 126 | raise ValueError('Must specify a request packet in order to' 127 | ' read a multi-packet response') 128 | response = self._read_multi_response(request) 129 | else: 130 | response = self._recv_pkt() 131 | if not self.single_packet_mode and response.pkt_type not in (SERVERDATA_RESPONSE_VALUE, SERVERDATA_AUTH_RESPONSE): 132 | raise RconError('Recieved unexpected RCON packet type') 133 | if request and response.pkt_id != request.pkt_id: 134 | raise RconError('Response ID does not match request ID') 135 | return response 136 | 137 | def _read_multi_response(self, req_pkt): 138 | """Return concatenated multi-packet response.""" 139 | chk_pkt = RconPacket(next(self.pkt_id), SERVERDATA_RESPONSE_VALUE) 140 | self._send_pkt(chk_pkt) 141 | # According to the Valve wiki, a server will mirror a 142 | # SERVERDATA_RESPONSE_VALUE packet and then send an additional response 143 | # packet with an empty body. So we should concatenate any packets until 144 | # we receive a response that matches the ID in chk_pkt 145 | body_parts = [] 146 | while True: 147 | response = self._recv_pkt() 148 | if response.pkt_type != SERVERDATA_RESPONSE_VALUE: 149 | raise RconError('Received unexpected RCON packet type') 150 | if response.pkt_id == chk_pkt.pkt_id: 151 | break 152 | elif response.pkt_id != req_pkt.pkt_id: 153 | raise RconError('Response ID does not match request ID') 154 | body_parts.append(response.body) 155 | # Read and ignore the extra empty body response 156 | self._recv_pkt() 157 | return RconPacket(req_pkt.pkt_id, SERVERDATA_RESPONSE_VALUE, 158 | ''.join(str(body_parts))) 159 | 160 | 161 | class RconError(Exception): 162 | """Generic RCON error.""" 163 | pass 164 | 165 | 166 | class RconAuthError(RconError): 167 | """Raised if an RCON Authentication error occurs.""" 168 | pass 169 | 170 | 171 | class RconSizeError(RconError): 172 | """Raised when an RCON packet is an illegal size.""" 173 | pass 174 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmrowla/pysrcds/cf4caa6abd7756e76ab3e613b183f163fd8d88cc/test/__init__.py -------------------------------------------------------------------------------- /test/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmrowla/pysrcds/cf4caa6abd7756e76ab3e613b183f163fd8d88cc/test/events/__init__.py -------------------------------------------------------------------------------- /test/events/test_csgo.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Peter Rowlands 2 | """Tests for srcds.events.csgo""" 3 | 4 | 5 | from srcds.events import csgo 6 | 7 | from .test_generic import check_event 8 | 9 | 10 | def test_switch_team_event(): 11 | """Test SwitchTeamEvent""" 12 | log_line = ''.join([ 13 | 'L 01/21/2013 - 23:07:24: "Charmander<19>" ', 14 | 'switched from team to ', 15 | ]) 16 | check_event(csgo.SwitchTeamEvent, log_line) 17 | 18 | 19 | def test_buy_event(): 20 | """Test BuyEvent""" 21 | log_line = ''.join([ 22 | 'L 01/12/2013 - 00:57:01: "foobar<21>" ', 23 | 'purchased "defuser"', 24 | ]) 25 | check_event(csgo.BuyEvent, log_line) 26 | 27 | 28 | def test_throw_event(): 29 | """Test ThrowEvent""" 30 | log_line = ''.join([ 31 | 'L 01/12/2013 - 00:57:01: "foobar<21>" ', 32 | 'threw hegrenade [-1879 2651 33]', 33 | ]) 34 | check_event(csgo.ThrowEvent, log_line) 35 | 36 | 37 | def test_csgo_kill_event(): 38 | """Test CsgoKillEvent""" 39 | log_line = ''.join([ 40 | 'L 01/12/2013 - 01:01:01: "foo<32>" ', 41 | '[-761 -836 196] killed "bar<38>" ', 42 | '[-793 -848 130] with "glock"', 43 | ]) 44 | event = check_event(csgo.CsgoKillEvent, log_line) 45 | assert not event.headshot 46 | log_line = ''.join([ 47 | 'L 01/12/2013 - 01:01:01: "foo<32>" ', 48 | '[-761 -836 196] killed "bar<38>" ', 49 | '[-793 -848 130] with "glock" (headshot)', 50 | ]) 51 | event = check_event(csgo.CsgoKillEvent, log_line) 52 | assert event.headshot 53 | 54 | def test_csgo_attack_event(): 55 | """Test CsgoAttackEvent""" 56 | log_line = ''.join([ 57 | 'L 01/12/2013 - 01:01:14: "foo<30>" [254 -370 7]', 58 | ' attacked "bar<33>" [-428 -843 114] ', 59 | 'with "m4a1" (damage "21") (damage_armor "4") (health "45") ', 60 | '(armor "87") (hitgroup "right arm")', 61 | ]) 62 | check_event(csgo.CsgoAttackEvent, log_line) 63 | -------------------------------------------------------------------------------- /test/events/test_generic.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Peter Rowlands 2 | """Tests for srcds.events.generic""" 3 | 4 | from __future__ import unicode_literals 5 | 6 | import re 7 | 8 | from srcds.events import generic 9 | 10 | 11 | def check_event(cls, log_line): 12 | match = re.match(cls.regex, log_line) 13 | assert match 14 | event = cls.from_re_match(match) 15 | assert event 16 | assert str(event) == log_line 17 | return event 18 | 19 | 20 | def test_base_event(): 21 | """Test BaseEvent""" 22 | log_line = 'L 01/11/2013 - 16:57:49:' 23 | check_event(generic.BaseEvent, log_line) 24 | 25 | 26 | def test_cvar_event(): 27 | """Test CvarEvent""" 28 | log_line = 'L 01/11/2013 - 16:57:49: Server cvars start' 29 | event = check_event(generic.CvarEvent, log_line) 30 | assert event.start and not event.end 31 | log_line = 'L 01/11/2013 - 16:57:49: Server cvars end' 32 | event = check_event(generic.CvarEvent, log_line) 33 | assert not event.start and event.end 34 | log_line = 'L 01/11/2013 - 16:57:49: Server cvar "foo" = "bar"' 35 | event = check_event(generic.CvarEvent, log_line) 36 | assert event.cvar == 'foo' and event.value == 'bar' 37 | 38 | 39 | def test_log_file_event(): 40 | """Test LogFileEvent""" 41 | log_line = ''.join([ 42 | 'L 01/10/2013 - 22:46:06: Log file started (file ', 43 | '"logfiles/L066_228_036_149_27015_201301102246_001.log") ', 44 | '(game "/opt/steam/csgo-ds/csgo") (version "5177")', 45 | ]) 46 | event = check_event(generic.LogFileEvent, log_line) 47 | assert event.started and not event.closed 48 | log_line = 'L 01/10/2013 - 23:15:21: Log file closed' 49 | event = check_event(generic.LogFileEvent, log_line) 50 | assert not event.started and event.closed 51 | 52 | 53 | def test_change_map_event(): 54 | """Test ChangeMapEvent""" 55 | # TODO find an example for this 56 | pass 57 | 58 | 59 | def test_rcon_event(): 60 | """Test RconEvent""" 61 | # TODO find an example for this 62 | pass 63 | 64 | 65 | def test_connection_event(): 66 | """Test ConnectionEvent""" 67 | # test bot connection 68 | log_line = ''.join([ 69 | 'L 01/11/2013 - 16:57:58: "Dave<3><>" connected, ', 70 | 'address "none"', 71 | ]) 72 | check_event(generic.ConnectionEvent, log_line) 73 | log_line = ''.join([ 74 | 'L 01/12/2013 - 00:57:01: "foobar<21><>" ', 75 | 'connected, address "12.34.56.78:27005"', 76 | ]) 77 | check_event(generic.ConnectionEvent, log_line) 78 | 79 | 80 | def test_validation_event(): 81 | """Test ValidationEvent""" 82 | log_line = ''.join([ 83 | 'L 01/12/2013 - 00:57:01: "foobar<21><>" ', 84 | 'STEAM USERID validated', 85 | ]) 86 | check_event(generic.ValidationEvent, log_line) 87 | 88 | 89 | def test_diconnection_event(): 90 | """Test DisconnectionEvent""" 91 | log_line = ''.join([ 92 | 'L 01/12/2013 - 00:57:01: "foobar<21><>" ', 93 | 'disconnected', 94 | ]) 95 | check_event(generic.DisconnectionEvent, log_line) 96 | 97 | 98 | def test_kick_event(): 99 | """Test KickEvent""" 100 | log_line = ''.join([ 101 | 'L 01/12/2013 - 00:57:01: Kick: "foobar<21><>" ', 102 | 'was kicked by "Console" (message "")', 103 | ]) 104 | check_event(generic.KickEvent, log_line) 105 | 106 | 107 | def test_suicide_event(): 108 | """Test SuicideEvent""" 109 | log_line = ''.join([ 110 | 'L 01/12/2013 - 00:57:01: "foobar<21><>" ', 111 | 'committed suicide with "hegrenade"', 112 | ]) 113 | check_event(generic.SuicideEvent, log_line) 114 | 115 | 116 | def test_team_selection_event(): 117 | """Test TeamSelectionEvent""" 118 | log_line = ''.join([ 119 | 'L 01/12/2013 - 00:57:01: "foobar<21><>" ', 120 | 'joined team "Spectators"', 121 | ]) 122 | check_event(generic.TeamSelectionEvent, log_line) 123 | 124 | 125 | def test_role_selection_event(): 126 | """Test RoleSelectionEvent""" 127 | log_line = ''.join([ 128 | 'L 01/12/2013 - 00:57:01: "foobar<21>" ', 129 | 'changed role to "medic"', 130 | ]) 131 | check_event(generic.RoleSelectionEvent, log_line) 132 | 133 | 134 | def test_change_name_event(): 135 | """Test ChangeNameEvent""" 136 | log_line = ''.join([ 137 | 'L 01/12/2013 - 00:57:01: "foobar<21><>" ', 138 | 'changed name to "baz"', 139 | ]) 140 | check_event(generic.ChangeNameEvent, log_line) 141 | 142 | 143 | def test_player_target_event(): 144 | """Test PlayerTargetEvent""" 145 | player = '"foobar<21>"' 146 | assert re.match(generic.PlayerTargetEvent.player_regex, player) 147 | assert re.match(generic.PlayerTargetEvent.target_regex, player) 148 | 149 | 150 | def test_kill_event(): 151 | """Test KillEvent""" 152 | log_line = ''.join([ 153 | 'L 01/12/2013 - 01:01:01: "foo<32>" ', 154 | 'killed "bar<38>" with "glock"', 155 | ]) 156 | check_event(generic.KillEvent, log_line) 157 | 158 | 159 | def test_attack_event(): 160 | """Test AttackEvent""" 161 | log_line = ''.join([ 162 | 'L 01/12/2013 - 01:01:01: "foo<32>" ', 163 | 'attacked "bar<38>" with "glock" ', 164 | '(damage "50")', 165 | ]) 166 | check_event(generic.AttackEvent, log_line) 167 | 168 | 169 | def test_player_action_event(): 170 | """Test PlayerActionEvent""" 171 | log_line = ''.join([ 172 | 'L 01/12/2013 - 00:57:01: "foobar<21><>" ', 173 | 'triggered "baz"', 174 | ]) 175 | check_event(generic.PlayerActionEvent, log_line) 176 | 177 | 178 | def test_team_action_event(): 179 | """Test TeamActionEvent""" 180 | log_line = ''.join([ 181 | 'L 01/12/2013 - 00:57:01: Team "TERRORIST" ', 182 | 'triggered "foo"', 183 | ]) 184 | check_event(generic.TeamActionEvent, log_line) 185 | 186 | 187 | def test_world_action_event(): 188 | """Test WorldActionEvent""" 189 | log_line = ''.join([ 190 | 'L 01/12/2013 - 00:57:01: World triggered "Round_End"', 191 | ]) 192 | check_event(generic.WorldActionEvent, log_line) 193 | 194 | 195 | def test_chat_event(): 196 | """Test ChatEvent""" 197 | log_line = ''.join([ 198 | 'L 01/12/2013 - 00:57:01: "foobar<21><>" ', 199 | 'say "baz"', 200 | ]) 201 | check_event(generic.ChatEvent, log_line) 202 | log_line = ''.join([ 203 | 'L 01/12/2013 - 00:57:01: "foobar<21><>" ', 204 | 'say_team "baz"', 205 | ]) 206 | check_event(generic.ChatEvent, log_line) 207 | 208 | 209 | def test_team_alliance_event(): 210 | """Test TeamAllianceEvent""" 211 | log_line = ''.join([ 212 | 'L 01/12/2013 - 00:57:01: Team "TERRORIST" ', 213 | 'formed alliance with "CT"', 214 | ]) 215 | check_event(generic.TeamAllianceEvent, log_line) 216 | 217 | 218 | def test_round_end_team_event(): 219 | """Test RoundEndTeamEvent""" 220 | log_line = ''.join([ 221 | 'L 01/12/2013 - 00:57:01: Team "TERRORIST" ', 222 | 'scored "2" with "5" players', 223 | ]) 224 | check_event(generic.RoundEndTeamEvent, log_line) 225 | 226 | 227 | def test_private_chat_event(): 228 | """Test PrivateChatEvent""" 229 | log_line = ''.join([ 230 | 'L 01/12/2013 - 01:01:01: "foo<32>" ', 231 | 'tell "bar<38>" ', 232 | 'message "baz"', 233 | ]) 234 | check_event(generic.PrivateChatEvent, log_line) 235 | 236 | 237 | def test_round_end_player_event(): 238 | """Test RoundEndPlayerEvent""" 239 | log_line = ''.join([ 240 | 'L 01/12/2013 - 00:57:01: Player "foobar<21>" ', 241 | 'scored "4"', 242 | ]) 243 | check_event(generic.RoundEndPlayerEvent, log_line) 244 | 245 | 246 | def weapon_select_event(): 247 | """Test WeaponSelectEvent""" 248 | log_line = ''.join([ 249 | 'L 01/12/2013 - 00:57:01: "foobar<21>" ', 250 | 'selected weapon "glock"', 251 | ]) 252 | check_event(generic.WeaponSelectEvent, log_line) 253 | 254 | 255 | def weapon_pickup_event(): 256 | """Test WeaponPickupEvent""" 257 | log_line = ''.join([ 258 | 'L 01/12/2013 - 00:57:01: "foobar<21>" ', 259 | 'acquired weapon "glock"', 260 | ]) 261 | check_event(generic.WeaponPickupEvent, log_line) 262 | --------------------------------------------------------------------------------