· 9 years ago · Dec 13, 2016, 09:10 PM
1#
2# BigBrotherBot(B3) (www.bigbrotherbot.net)
3# Copyright (C) 2008 Mark Weirath (xlr8or@xlr8or.com)
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18#
19# CHANGELOG
20#
21# 1.0.3 - Courgette - added support for banlist.txt
22# xlr8or - added parsing Damage (on_hit)
23# 1.0.4 - xlr8or - added EVT_CLIENT_TEAM_CHANGE in on_kill
24# 1.0.5 - xlr8or - added hitloc and damageType info to accomodate XLRstats
25# 1.0.6 - xlr8or - fixed a bug where the parser wouldn't parse the shutdowngame and warmup functions
26# 1.0.7 - xlr8or - better synchronizing and identification of connecting players and zombies
27# 1.0.8 - xlr8or - better Zombie handling (Zombies being a result of: sv_zombietime (default 2
28# seconds)). (Zombie time is the time after a disconnect that the slot cannot be
29# used and thus is in Zombie state)
30# - dded functionality to use ip's only, not using the guid at all (experimental)
31# 1.0.9 - xlr8or - try to get the map name at start
32# - provide get_player_scores method
33# 1.0.10 - Courgette - modified _recolor so name sanitation is the same as UrT
34# here it does more than just remove color
35# 1.0.11 - Courgette - Add get_scores # NOTE: this won't work properly if the server has private slots.
36# 1.0.12 - Courgette - Fix regex that failed to parse chat lines when player's name ends with ':'
37# 1.0.13 - xlr8or - support for !maps and !nextmap command
38# 1.0.14 - xlr8or - better understanding of mapcycle.txt
39# 1.0.15 - mindriot - client with empty name ("") resulted in error and B3 not registering
40# client - now given _empty_name_default
41# 1.0.16 - xlr8or - added ipcombi. Setting True will replace the last part of the guid with two
42# segments of the ip: increases security on admins who have cl_guidServerUniq set
43# to 0 in client config (No cloning).
44# 1.0.17 - mindriot - empty_name_default now only given upon client connect, due to possibility of no
45# name specified in ClientUserinfo at any time
46# 1.0.19 - xlr8or - disabled PunkBuster default settings due to recent supportrequests in the forums
47# with missing PB line in b3.xml
48# 1.1.0 - xlr8or - added action mechanism (event) for B3 v1.1.5+
49# 1.1.1 - Courgette - debugged Action Mechanism (event) for B3 v1.1.5+
50# 19/08/2009 - 1.2.0 - Courgette - adds slap, nuke, mute new custom penalty types (can be used in censor or
51# admin plugin)
52# - requires admin plugin v1.4+ and parser.py v1.10+
53# 20/10/2009 - 1.3.0 - Courgette - upon bot start, already connected players are correctly recognized
54# 26/10/2009 - 1.4.0 - Courgette - when no client is found by cid, try to join the player using /rcon dumpuser <cid>
55# 11/11/2009 - 1.5.0 - Courgette - create a new event: EVT_GAME_FLAG_RETURNED which is fired when the flag
56# return because of time code refactoring
57# 17/11/2009 - 1.5.1 - Courgette - harden get_nextmap by :
58# * wrapping initial getcvar queries with try:except bloc
59# * requerying required cvar if missing
60# * forcing map list refresh on server reload or round end
61# 26/11/2009 - 1.5.2 - Courgette - fix a bug that prevented kills by slap or nuke from firing kill events
62# 30/11/2009 - 1.6.1 - Courgette - separate parsing of lines ClientUserInfo and ClientUserInfoChanged to better
63# translate ClientUserInfoChanged data. Also OnClientUserInfoChanged does not create
64# new client if cid is unknown
65# 05/12/2009 - 1.6.2 - Courgette - fix _replayerscore regexp
66# - on startup, also try to get players' team (which is not given by dumpuser)
67# 06/12/2009 - 1.6.3 - Courgette - harden query_client_userinfo_by_cid making sure we got a positive response. (Never
68# trust input data...)
69# - fix _replayerscore regexp again
70# 06/12/2009 - 1.6.4 - Courgette - sync() will retries to get player list up to 4 for times before giving up as
71# sync() after map change too often fail 2 times.
72# 09/12/2009 - 1.6.5 - Courgette - different handling of 'name' in OnClientuserinfo. Now log looks less worrying
73# - prevent exception on the rare case where a say line shows no text after cid (hence
74# no regexp match)
75# 21/12/2009 - 1.7 - Courgette - add new UrT specific event : EVT_CLIENT_GEAR_CHANGE
76# 30/12/2009 - 1.7.1 - Courgette - Say, Sayteam and Saytell lines do not trigger name change anymore and detect
77# the UrT bug described in
78# http://www.bigbrotherbot.net/forums/urt/b3-bot-sometimes-mix-up-client-id%27s/ .
79# Hopefully this definitely fixes the wrong aliases issue
80# 30/12/2009 - 1.7.2 - Courgette - improve say lines slot bug detection for cases where no player exists on slot 0
81# - refactor detection code to follow the KISS rule (keep it simple and stupid)
82# 31/12/2009 - 1.7.3 - Courgette - fix bug getting client by name when UrT slot 0 bug
83# - requires clients.py 1.2.8+
84# 02/01/2010 - 1.7.4 - Courgette - improve Urt slot bug workaround as it appears it can occur with slot num
85# different than 0
86# 05/01/2010 - 1.7.5 - Courgette - fix minor bug in saytell
87# 16/01/2010 - 1.7.6 - xlr8or - removed max_retries=4 keyword from get_player_list()
88# 16/01/2010 - 1.7.7 - Courgette - put back max_retries=4 keyword from get_player_list(). @xlr8or: make sure you have
89# the latest q3a.py file (v1.3.1+) for max_retries to work.
90# 18/01/2010 - 1.7.8 - Courgette - update get_player_list and sync so that connecting players (CNCT) are not ignored.
91# This will allow to use commands like !ci or !kick on hanging players.
92# 26/01/2010 - 1.7.9 - xlr8or - moved get_map() to q3a.py
93# 10/04/2010 - 1.7.10 - Bakes - bigsay() function can be used by plugins.
94# 15/04/2010 - 1.7.11 - Courgette - add debugging info for get_nextmap()
95# 28/05/2010 - 1.7.12 - xlr8or - connect bots
96# 07/11/2010 - 1.7.13 - GrosBedo - messages now support named $variables instead of %s
97# 08/11/2010 - 1.7.14 - GrosBedo - messages can now be empty (no message broadcasted on kick/tempban/ban/unban)
98# 21/12/2010 - 1.7.15 - SGT - fix CNCT ping error in getPlayersPings
99# - fix incorrect game type for ffa
100# - move getMapList after game initialization
101# 09/04/2011 - 1.7.16 - Courgette - reflect that cid are not converted to int anymore in the clients module
102# 03/05/2011 - 1.7.17 - Courgette - reflect changes in inflict_custom_penalty method signature
103# 31/05/2011 - 1.8.0 - Courgette - damage event now carry correct damage points
104# - damage event weapon code is now the same as the one used for Kill events
105# 01/06/2011 - 1.8.1 - Courgette - fix Damage points
106# - when game log provides hit info, Kill event will use last dmg points instead of 100
107# 04/06/2011 - 1.9.0 - Courgette - makes use of the new plugins_started parser hook
108# 05/06/2011 - 1.10.0 - Courgette - change data format for EVT_CLIENT_BAN events
109# 14/06/2011 - 1.11.0 - Courgette - cvar code moved to q3a AbstractParser
110# 12/09/2011 - 1.11.1 - Courgette - EVT_CLIENT_JOIN event is now triggered when player actually join a team
111# - the call to self.clients.sync() that was made each round is now made on game
112# init and in its own thread
113# 29/09/2011 - 1.11.2 - Courgette - fix MOD_TELEFRAG attacker on kill event to prevent people from being considered
114# as tkers in such cases.
115# 15/10/2011 - 1.11.3 - Courgette - better team recognition of existing players at B3 start
116# 15/11/2011 - 1.11.4 - Courgette - players's team get refreshed after unpausing the bot (useful when used with FTP
117# and B3 lose the connection for a while)
118# 03/03/2012 - 1.11.5 - SGT - create Survivor Winner event
119# - create Unban event
120# - fix issue with on_say when something like this come and the match couldn't find
121# the name group, say: 7 -crespino-
122# 08/04/2012 - 1.12 - Courgette - fixes rotatemap() - thanks to Beber888
123# - refactor unban()
124# - change_map() can now provide suggestions
125# 05/05/2012 - 1.13 - Courgette - fixes issue xlr8or/big-brother-bot#87 - missing ip when trying to auth a
126# client crashes the bot
127# 19/05/2012 - 1.13.1 - Courgette - fixes issue with kill events when killed by UT_MOD_SLAPPED,
128# UT_MOD_NUKED, MOD_TELEFRAG
129# 07/07/2012 - 1.13.2 - Courgette - ensures the config file has option 'game_log' in section 'server'
130# 12/08/2012 - 1.13.3 - Courgette - fix !nextmap bug when the mapcycle file contains empty lines
131# 19/10/2012 - 1.14 - Courgette - improve finding the exact map in get_maps_sounding_like. Also improves change_map()
132# behavior as a consequence
133# 26/11/2012 - 1.15 - Courgette - protect some of the Client object property
134# 08/04/2013 - 1.16 - Courgette - add EVT_BOMB_EXPLODED event
135# 14/07/2013 - 1.17 - Courgette - add hitlocation constants : HL_HEAD, HL_HELMET and HL_TORSO
136# 10/08/2013 - 1.18 - Fenix - change get_nextmap to use CVARs only (no more mapcycle file parsing)
137# 13/01/2014 - 1.19 - Fenix - PEP8 coding standards
138# - correctly set the client bot flag upon new client connection
139# 14/04/2014 - 1.20 - Fenix - rewritten regular expressions on multiline: respect PEP8 constraint
140# - use get_event_id method to obtain event ids: remove some warnings
141# 02/06/2014 - 1.20.1 - Fenix - fixed reColor regex stripping whitespaces between words
142# 18/07/2014 - 1.21 - Fenix - updated parser to comply with the new get_wrap implementation
143# - general parser cleanup
144# - reformat changelog to it can actually be read :)
145# - removed _settings dict re-declaration: was the same of the AbstractParser
146# - updated rcon command patterns
147# 03/08/2014 - 1.22 - Fenix - syntax cleanup
148# 13/09/2014 - 1.23 - Fenix - added new event: EVT_SENTRY_KILL
149# 02/10/2014 - 1.24 - Fenix - fixed regression introduced in 1.22
150# 15/01/2015 - 1.25 - Fenix - fixed another regression introduced in 1.22
151# 27/01/2015 - 1.26 - Thomas LEVEIL - `fdir *.bsp` waits for a game server response for 15s
152# 15/06/2015 - 1.27 - Fenix - fixed client userinfo parsing!
153# 26/07/2015 - 1.28 - Fenix - queue EVT_GAME_ROUND_END when survivor winner is triggered
154
155__author__ = 'xlr8or, Courgette, Fenix'
156__version__ = '1.28'
157
158import b3
159import b3.events
160import b3.clients
161import b3.parser
162import re
163import string
164import time
165import thread
166
167from b3.functions import getStuffSoundingLike
168from b3.functions import prefixText
169from b3.parsers.q3a.abstractParser import AbstractParser
170
171
172class Iourt41Parser(AbstractParser):
173
174 gameName = 'iourt41'
175
176 IpsOnly = False
177 IpCombi = False
178 PunkBuster = None
179
180 _logSync = 2
181 _maplist = None
182 _empty_name_default = 'EmptyNameDefault'
183
184 _commands = {
185 'ban': 'addip %(cid)s',
186 'banByIp': 'addip %(ip)s',
187 'broadcast': '%(message)s',
188 'kick': 'clientkick %(cid)s',
189 'message': 'tell %(cid)s %(message)s',
190 'moveToTeam': 'forceteam %(cid)s %(team)s',
191 'mute': 'mute %(cid)s %(seconds)s',
192 'nuke': 'nuke %(cid)s',
193 'say': 'say %(message)s',
194 'saybig': 'bigtext "%(message)s"',
195 'set': 'set %(name)s "%(value)s"',
196 'slap': 'slap %(cid)s',
197 'tempban': 'clientkick %(cid)s',
198 'unbanByIp': 'removeip %(ip)s',
199 }
200
201 _eventMap = {
202 #'warmup' : b3.events.EVT_GAME_WARMUP,
203 #'shutdowngame' : b3.events.EVT_GAME_ROUND_END
204 }
205
206 # remove the time off of the line
207 _lineClear = re.compile(r'^(?:[0-9:]+\s?)?')
208
209 _lineFormats = (
210 # Generated with ioUrbanTerror v4.1:
211 # Hit: 12 7 1 19: BSTHanzo[FR] hit ercan in the Helmet
212 # Hit: 13 10 0 8: Grover hit jacobdk92 in the Head
213 re.compile(r'^(?P<action>[a-z]+):\s'
214 r'(?P<data>'
215 r'(?P<cid>[0-9]+)\s'
216 r'(?P<acid>[0-9]+)\s'
217 r'(?P<hitloc>[0-9]+)\s'
218 r'(?P<aweap>[0-9]+):\s+'
219 r'(?P<text>.*))$', re.IGNORECASE),
220
221 # 6:37 Kill: 0 1 16: XLR8or killed =lvl1=Cheetah by UT_MOD_SPAS
222 # 2:56 Kill: 14 4 21: Qst killed Leftovercrack by UT_MOD_PSG1
223 re.compile(r'^(?P<action>[a-z]+):\s'
224 r'(?P<data>('
225 r'?P<acid>[0-9]+)\s'
226 r'(?P<cid>[0-9]+)\s'
227 r'(?P<aweap>[0-9]+):\s+'
228 r'(?P<text>.*))$', re.IGNORECASE),
229
230 # Processing chats and tell events...
231 # 5:39 saytell: 15 16 repelSteeltje: nno
232 # 5:39 saytell: 15 15 repelSteeltje: nno
233 re.compile(r'^(?P<action>[a-z]+):\s'
234 r'(?P<data>'
235 r'(?P<cid>[0-9]+)\s'
236 r'(?P<acid>[0-9]+)\s'
237 r'(?P<name>[^ ]+):\s+'
238 r'(?P<text>.*))$', re.IGNORECASE),
239
240 # 3:53 say: 8 denzel: lol
241 # 15:37 say: 9 .:MS-T:.BstPL: this name is quite a challenge
242 # 2:28 sayteam: 12 New_UrT_Player_v4.1: woekele
243 # 16:33 Flag: 2 0: team_CTF_redflag
244 # SGT: fix issue with on_say when something like this come and the match could'nt find the name group
245 # say: 7 -crespino-:
246 re.compile(r'^(?P<action>[a-z]+):\s'
247 r'(?P<data>'
248 r'(?P<cid>[0-9]+)\s'
249 r'(?P<name>[^ ]+):\s*'
250 r'(?P<text>.*))$', re.IGNORECASE),
251
252 # 15:42 Flag Return: RED
253 # 15:42 Flag Return: BLUE
254 re.compile(r'^(?P<action>Flag Return):\s(?P<data>(?P<color>.+))$', re.IGNORECASE),
255
256 # Bombmode actions:
257 # 3:06 Bombholder is 2
258 re.compile(r'^(?P<action>Bombholder)(?P<data>\sis\s(?P<cid>[0-9]+).*)$', re.IGNORECASE), #achsenknopf; seemed to only accept one cipher, so i changed it :3
259
260 # was planted, was defused, was tossed, has been collected (doh, how gramatically correct!)
261 # 2:13 Bomb was tossed by 2
262 # 2:32 Bomb was planted by 2
263 # 3:01 Bomb was defused by 3!
264 # 2:17 Bomb has been collected by 2
265 re.compile(r'^(?P<action>Bomb)\s'
266 r'(?P<data>(was|has been)\s'
267 r'(?P<subaction>[a-z]+)\sby\s'
268 r'(?P<cid>[0-9]+).*)$', re.IGNORECASE),
269
270 #17:24 Pop!
271 re.compile(r'^(?P<action>Pop)!$', re.IGNORECASE),
272
273 # Falling thru Item stuff and so forth
274 re.compile(r'^(?P<action>[a-z]+):\s(?P<data>.*)$', re.IGNORECASE),
275
276 # Shutdowngame and Warmup... the one word lines
277 re.compile(r'^(?P<action>[a-z]+):$', re.IGNORECASE)
278 )
279
280 # map: ut4_casa
281 # num score ping name lastmsg address qport rate
282 # --- ----- ---- --------------- ------- --------------------- ----- -----
283 # 2 0 19 ^1XLR^78^8^9or 0 145.99.135.227:27960 41893 8000 # player with a live ping
284 # 4 0 CNCT Dz!k^7 450 83.175.191.27:64459 50308 20000 # connecting player
285 # 9 0 ZMBI ^7 1900 81.178.80.68:27960 10801 8000 # zombies (need to be disconnected!)
286 _regPlayer = re.compile(r'^(?P<slot>[0-9]+)\s+'
287 r'(?P<score>[0-9-]+)\s+'
288 r'(?P<ping>[0-9]+|CNCT|ZMBI)\s+'
289 r'(?P<name>.*?)\s+'
290 r'(?P<last>[0-9]+)\s+'
291 r'(?P<ip>[0-9.]+):(?P<port>[0-9-]+)\s+'
292 r'(?P<qport>[0-9]+)\s+'
293 r'(?P<rate>[0-9]+)$', re.IGNORECASE)
294
295 _reColor = re.compile(r'(\^\d)')
296
297 # Map: ut4_algiers
298 # Players: 8
299 # Scores: R:97 B:98
300 # 0: FREE k:0 d:0 ping:0
301 # 4: yene RED k:16 d:8 ping:50 92.104.110.192:63496
302 _reTeamScores = re.compile(r'^Scores:\s+'
303 r'R:(?P<RedScore>.+)\s+'
304 r'B:(?P<BlueScore>.+)$', re.IGNORECASE)
305
306 _rePlayerScore = re.compile(r'^(?P<slot>[0-9]+): '
307 r'(?P<name>.*) '
308 r'(?P<team>RED|BLUE|SPECTATOR|FREE) '
309 r'k:(?P<kill>[0-9]+) d:(?P<death>[0-9]+) ping:(?P<ping>[0-9]+|CNCT|ZMBI)( '
310 r'(?P<ip>[0-9.]+):(?P<port>[0-9-]+))?$', re.IGNORECASE)
311
312 ## kill modes
313 MOD_WATER = '1'
314 MOD_LAVA = '3'
315 MOD_TELEFRAG = '5'
316 MOD_FALLING = '6'
317 MOD_SUICIDE = '7'
318 MOD_TRIGGER_HURT = '9'
319 MOD_CHANGE_TEAM = '10'
320 UT_MOD_KNIFE = '12'
321 UT_MOD_KNIFE_THROWN = '13'
322 UT_MOD_BERETTA = '14'
323 UT_MOD_DEAGLE = '15'
324 UT_MOD_SPAS = '16'
325 UT_MOD_UMP45 = '17'
326 UT_MOD_MP5K = '18'
327 UT_MOD_LR300 = '19'
328 UT_MOD_G36 = '20'
329 UT_MOD_PSG1 = '21'
330 UT_MOD_HK69 = '22'
331 UT_MOD_BLED = '23'
332 UT_MOD_KICKED = '24'
333 UT_MOD_HEGRENADE = '25'
334 UT_MOD_SR8 = '28'
335 UT_MOD_AK103 = '30'
336 UT_MOD_SPLODED = '31'
337 UT_MOD_SLAPPED = '32'
338 UT_MOD_BOMBED = '33'
339 UT_MOD_NUKED = '34'
340 UT_MOD_NEGEV = '35'
341 UT_MOD_HK69_HIT = '37'
342 UT_MOD_M4 = '38'
343 UT_MOD_FLAG = '39'
344 UT_MOD_GOOMBA = '40'
345
346 # HIT LOCATIONS
347 HL_HEAD = '0'
348 HL_HELMET = '1'
349 HL_TORSO = '2'
350
351 # WORLD CID (used for Mr. Sentry detection)
352 WORLD = '1022'
353
354 ## weapons id on Hit: lines are different than the one
355 ## on the Kill: lines. Here the translation table
356 hitweapon2killweapon = {
357 1: UT_MOD_KNIFE,
358 2: UT_MOD_BERETTA,
359 3: UT_MOD_DEAGLE,
360 4: UT_MOD_SPAS,
361 5: UT_MOD_MP5K,
362 6: UT_MOD_UMP45,
363 8: UT_MOD_LR300,
364 9: UT_MOD_G36,
365 10: UT_MOD_PSG1,
366 14: UT_MOD_SR8,
367 15: UT_MOD_AK103,
368 17: UT_MOD_NEGEV,
369 19: UT_MOD_M4,
370 21: UT_MOD_HEGRENADE,
371 22: UT_MOD_KNIFE_THROWN,
372 }
373
374 # From data provided by Garreth http://bit.ly/jf4QXc on http://bit.ly/krwBCv :
375 #
376 # Head(0) Helmet(1) Torso(2) Kevlar(3) Arms(4) Legs(5) Body(6) Killed
377 # MOD_TELEFRAG='5' 0 0 0 0 0 0 0 0
378 # UT_MOD_KNIFE='12' 100 60 44 35 20 20 44 100
379 # UT_MOD_KNIFE_THROWN='13' 100 60 44 35 20 20 44 100
380 # UT_MOD_BERETTA='14' 100 34 30 20 11 11 30 100
381 # UT_MOD_DEAGLE='15' 100 66 57 38 22 22 57 100
382 # UT_MOD_SPAS='16' 25 25 25 25 25 25 25 100
383 # UT_MOD_UMP45='17' 100 51 44 29 17 17 44 100
384 # UT_MOD_MP5K='18' 50 34 30 20 11 11 30 100
385 # UT_MOD_LR300='19' 100 51 44 29 17 17 44 100
386 # UT_MOD_G36='20' 100 51 44 29 17 17 44 100
387 # UT_MOD_PSG1='21' 100 63 97 63 36 36 97 100
388 # UT_MOD_HK69='22' 50 50 50 50 50 50 50 100
389 # UT_MOD_BLED='23' 15 15 15 15 15 15 15 15
390 # UT_MOD_KICKED='24' 20 20 20 20 20 20 20 100
391 # UT_MOD_HEGRENADE='25' 50 50 50 50 50 50 50 100
392 # UT_MOD_SR8='28' 100 100 100 100 50 50 100 100
393 # UT_MOD_AK103='30' 100 58 51 34 19 19 51 100
394 # UT_MOD_NEGEV='35' 50 34 30 20 11 11 30 100
395 # UT_MOD_HK69_HIT='37' 20 20 20 20 20 20 20 100
396 # UT_MOD_M4='38' 100 51 44 29 17 17 44 100
397 # UT_MOD_GOOMBA='40' 100 100 100 100 100 100 100 100
398
399 damage = {
400 MOD_TELEFRAG: [0, 0, 0, 0, 0, 0, 0, 0],
401 UT_MOD_KNIFE: [100, 60, 44, 35, 20, 20, 44, 100],
402 UT_MOD_KNIFE_THROWN: [100, 60, 44, 35, 20, 20, 44, 100],
403 UT_MOD_BERETTA: [100, 34, 30, 20, 11, 11, 30, 100],
404 UT_MOD_DEAGLE: [100, 66, 57, 38, 22, 22, 57, 100],
405 UT_MOD_SPAS: [25, 25, 25, 25, 25, 25, 25, 100],
406 UT_MOD_UMP45: [100, 51, 44, 29, 17, 17, 44, 100],
407 UT_MOD_MP5K: [50, 34, 30, 20, 11, 11, 30, 100],
408 UT_MOD_LR300: [100, 51, 44, 29, 17, 17, 44, 100],
409 UT_MOD_G36: [100, 51, 44, 29, 17, 17, 44, 100],
410 UT_MOD_PSG1: [100, 63, 97, 63, 36, 36, 97, 100],
411 UT_MOD_HK69: [50, 50, 50, 50, 50, 50, 50, 100],
412 UT_MOD_BLED: [15, 15, 15, 15, 15, 15, 15, 15],
413 UT_MOD_KICKED: [20, 20, 20, 20, 20, 20, 20, 100],
414 UT_MOD_HEGRENADE: [50, 50, 50, 50, 50, 50, 50, 100],
415 UT_MOD_SR8: [100, 100, 100, 100, 50, 50, 100, 100],
416 UT_MOD_AK103: [100, 58, 51, 34, 19, 19, 51, 100],
417 UT_MOD_NEGEV: [50, 34, 30, 20, 11, 11, 30, 100],
418 UT_MOD_HK69_HIT: [20, 20, 20, 20, 20, 20, 20, 100],
419 UT_MOD_M4: [100, 51, 44, 29, 17, 17, 44, 100],
420 UT_MOD_GOOMBA: [100, 100, 100, 100, 100, 100, 100, 100],
421 }
422
423 ####################################################################################################################
424 # #
425 # PARSER INITIALIZATION #
426 # #
427 ####################################################################################################################
428
429 def startup(self):
430 """
431 Called after the parser is created before run().
432 """
433 if not self.config.has_option('server', 'game_log'):
434 self.critical("Your main config file is missing the 'game_log' setting in section 'server'")
435 raise SystemExit(220)
436
437 # add UrT specific events
438 self.Events.createEvent('EVT_GAME_FLAG_RETURNED', 'Flag returned')
439 self.Events.createEvent('EVT_CLIENT_GEAR_CHANGE', 'Client gear change')
440 self.Events.createEvent('EVT_SURVIVOR_WIN', 'Survivor Winner')
441 self.Events.createEvent('EVT_BOMB_EXPLODED', 'Bomb exploded')
442 self.Events.createEvent('EVT_SENTRY_KILL', 'Mr Sentry kill')
443
444 # add event mappings
445 self._eventMap['warmup'] = self.getEventID('EVT_GAME_WARMUP')
446 self._eventMap['shutdowngame'] = self.getEventID('EVT_GAME_ROUND_END')
447
448 # add the world client
449 self.clients.newClient('-1', guid='WORLD', name='World', hide=True, pbid='WORLD')
450
451 # get map from the status rcon command
452 mapname = self.getMap()
453 if mapname:
454 self.game.mapName = mapname
455 self.info('map is: %s' % self.game.mapName)
456
457 # force g_logsync
458 self.debug('Forcing server cvar g_logsync to %s' % self._logSync)
459 self.setCvar('g_logsync', self._logSync)
460
461 # get gamepaths/vars
462 cvarlist = self.cvarList("fs_")
463
464 self.game.fs_game = cvarlist.get('fs_game')
465 if not self.game.fs_game:
466 self.warning("Could not query server for fs_game")
467 else:
468 self.debug("fs_game: %s" % self.game.fs_game)
469
470 self.game.fs_basepath = cvarlist.get('fs_basepath')
471 if not self.game.fs_basepath:
472 self.warning("Could not query server for fs_basepath")
473 else:
474 self.game.fs_basepath = self.game.fs_basepath.rstrip('/')
475 self.debug('fs_basepath: %s' % self.game.fs_basepath)
476
477 self.game.fs_homepath = cvarlist.get('fs_homepath')
478 if not self.game.fs_homepath:
479 self.warning("Could not query server for fs_homepath")
480 else:
481 self.game.fs_homepath = self.game.fs_homepath.rstrip('/')
482 self.debug('fs_homepath: %s' % self.game.fs_homepath)
483
484 self._maplist = self.getMaps()
485
486 def pluginsStarted(self):
487 """
488 Called after the parser loaded and started all plugins.
489 """
490 plist = self.getPlayerList()
491 for cid in plist.keys():
492 userinfostring = self.queryClientUserInfoByCid(cid)
493 if userinfostring:
494 self.OnClientuserinfo(None, userinfostring)
495
496 player_teams = dict()
497 tries = 0
498 while tries < 3:
499 try:
500 tries += 1
501 player_teams = self.getPlayerTeams()
502 break
503 except Exception as err:
504 if tries < 3:
505 self.warning(err)
506 else:
507 self.error("Cannot fix players teams: %s" % err)
508 return
509
510 for cid in plist.keys():
511 client = self.clients.getByCID(cid)
512 if client and client.cid in player_teams:
513 newteam = player_teams[client.cid]
514 if newteam != client.team:
515 self.debug('Fixing client team for %s : %s is now %s' % (client.name, client.team, newteam))
516 setattr(client, 'team', newteam)
517
518 def unpause(self):
519 """
520 Unpause B3 log parsing.
521 """
522 self.pluginsStarted() # so we get teams refreshed
523 self.clients.sync()
524 b3.parser.Parser.unpause(self)
525
526 ####################################################################################################################
527 # #
528 # PARSING #
529 # #
530 ####################################################################################################################
531
532 def getLineParts(self, line):
533 """
534 Parse a log line returning extracted tokens.
535 :param line: The line to be parsed
536 """
537 line = re.sub(self._lineClear, '', line, 1)
538 m = None
539 for f in self._lineFormats:
540 m = re.match(f, line)
541 if m:
542 break
543
544 if m is not None:
545 client = None
546 target = None
547 try:
548 data = m.group('data').strip()
549 except:
550 data = None
551 return m, m.group('action').lower(), data, client, target
552 elif '------' not in line:
553 self.verbose('Line did not match format: %s' % line)
554
555 def parseUserInfo(self, info):
556 """
557 Parse an infostring.
558 :param info: The infostring to be parsed.
559 """
560 # 2 \ip\145.99.135.227:27960\challenge\-232198920\qport\2781\protocol\68\battleye\1\name\[SNT]^1XLR^78or...
561 # 7 n\[SNT]^1XLR^78or\t\3\r\2\tl\0\f0\\f1\\f2\\a0\0\a1\0\a2\0
562 player_id, info = string.split(info, ' ', 1)
563
564 if info[:1] != '\\':
565 info = '\\' + info
566
567 self.verbose2('Parsing userinfo: %s', info)
568 options = re.findall(r'\\([^\\]+)\\([^\\]+)', info)
569
570 data = dict()
571 for o in options:
572 data[o[0]] = o[1]
573
574 data['cid'] = player_id
575 return data
576
577 ####################################################################################################################
578 # #
579 # EVENT HANDLERS #
580 # #
581 ####################################################################################################################
582
583 def OnClientconnect(self, action, data, match=None):
584 self.debug('Client connected: ready to parse userinfo line')
585 #client = self.clients.getByCID(data)
586 #return b3.events.Event(b3.events.EVT_CLIENT_JOIN, None, client)
587
588 def OnClientbegin(self, action, data, match=None):
589 # we get user info in two parts:
590 # 19:42.36 ClientBegin: 4
591 client = self.getByCidOrJoinPlayer(data)
592 if client:
593 return b3.events.Event(self.getEventID('EVT_CLIENT_JOIN'), data=data, client=client)
594
595 def OnClientuserinfo(self, action, data, match=None):
596 # 2 \ip\145.99.135.227:27960\challenge\-232198920\qport\2781\protocol\68\battleye\1\name\[SNT]^1XLR^78or...
597 # connecting bot:
598 # 0 \gear\GMIORAA\team\blue\skill\5.000000\characterfile\bots/ut_chicken_c.c\color\4\sex\male\race\2\snaps\20\..
599 bclient = self.parseUserInfo(data)
600
601 bot = False
602 if 'cl_guid' not in bclient and 'skill' in bclient:
603 # must be a bot connecting
604 self.bot('Bot connecting!')
605 bclient['ip'] = '0.0.0.0'
606 bclient['cl_guid'] = 'BOT' + str(bclient['cid'])
607 bot = True
608
609 if 'name' in bclient:
610 # remove spaces from name
611 bclient['name'] = bclient['name'].replace(' ', '')
612
613 # split port from ip field
614 if 'ip' in bclient:
615 ip_port_data = string.split(bclient['ip'], ':', 1)
616 bclient['ip'] = ip_port_data[0]
617 if len(ip_port_data) > 1:
618 bclient['port'] = ip_port_data[1]
619
620 if 'team' in bclient:
621 bclient['team'] = self.getTeam(bclient['team'])
622
623 if 'cl_guid' in bclient and not 'pbid' in bclient and self.PunkBuster:
624 bclient['pbid'] = bclient['cl_guid']
625
626 self.verbose('Parsed userinfo: %s' % bclient)
627
628 if bclient:
629 client = self.clients.getByCID(bclient['cid'])
630 if client:
631 # update existing client
632 for k, v in bclient.iteritems():
633 if hasattr(client, 'gear') and k == 'gear' and client.gear != v:
634 self.queueEvent(b3.events.Event(self.getEventID('EVT_CLIENT_GEAR_CHANGE'), v, client))
635 if not k.startswith('_') and k not in ('login', 'password', 'groupBits', 'maskLevel',
636 'autoLogin', 'greeting'):
637 setattr(client, k, v)
638 else:
639 # make a new client
640 if self.PunkBuster:
641 # we will use punkbuster's guid
642 guid = None
643 else:
644 # use io guid
645 if 'cl_guid' in bclient:
646 guid = bclient['cl_guid']
647 else:
648 guid = 'unknown'
649
650 # v1.0.17 - mindriot - 02-Nov-2008
651 if not 'name' in bclient:
652 bclient['name'] = self._empty_name_default
653
654 if not 'ip' in bclient:
655 if guid == 'unknown':
656 # happens when a client is (temp)banned and got kicked so client
657 # was destroyed, but infoline was still waiting to be parsed.
658 self.debug('Client disconnected: ignoring...')
659 return None
660 else:
661 try:
662 # see issue xlr8or/big-brother-bot#87 - ip can be missing
663 self.debug("Missing IP, trying to get ip with 'status'")
664 plist = self.getPlayerList()
665 client_data = plist[bclient['cid']]
666 bclient['ip'] = client_data['ip']
667 except Exception, err:
668 bclient['ip'] = ''
669 self.warning("Failed to get client %s ip address" % bclient['cid'], err)
670
671 nguid = ''
672 # override the guid... use ip's only if self.console.IpsOnly is set True.
673 if self.IpsOnly:
674 nguid = bclient['ip']
675
676 # replace last part of the guid with two segments of the ip
677 elif self.IpCombi:
678 i = bclient['ip'].split('.')
679 d = len(i[0]) + len(i[1])
680 nguid = guid[:-d] + i[0] + i[1]
681
682 # Quake clients don't have a cl_guid, we'll use ip instead
683 elif guid == 'unknown':
684 nguid = bclient['ip']
685
686 if nguid != '':
687 guid = nguid
688
689 self.clients.newClient(bclient['cid'], name=bclient['name'], ip=bclient['ip'],
690 state=b3.STATE_ALIVE, guid=guid, bot=bot, data={'guid': guid})
691
692 return None
693
694 def OnClientuserinfochanged(self, action, data, match=None):
695 # 7 n\[SNT]^1XLR^78or\t\3\r\2\tl\0\f0\\f1\\f2\\a0\0\a1\0\a2\0
696 parseddata = self.parseUserInfo(data)
697 if parseddata:
698 self.verbose('Parsed userinfo: %s' % parseddata)
699 client = self.clients.getByCID(parseddata['cid'])
700 if client:
701 # update existing client
702 if 'n' in parseddata:
703 setattr(client, 'name', parseddata['n'])
704
705 if 't' in parseddata:
706 team = self.getTeam(parseddata['t'])
707 setattr(client, 'team', team)
708
709 if 'r' in parseddata:
710 if team == b3.TEAM_BLUE:
711 setattr(client, 'raceblue', parseddata['r'])
712 elif team == b3.TEAM_RED:
713 setattr(client, 'racered', parseddata['r'])
714
715 if parseddata.get('f0') is not None \
716 and parseddata.get('f1') is not None \
717 and parseddata.get('f2') is not None:
718
719 data = "%s,%s,%s" % (parseddata['f0'], parseddata['f1'], parseddata['f2'])
720 if team == b3.TEAM_BLUE:
721 setattr(client, 'funblue', data)
722 elif team == b3.TEAM_RED:
723 setattr(client, 'funred', data)
724
725 if 'a0' in parseddata and 'a1' in parseddata and 'a2' in parseddata:
726 setattr(client, 'cg_rgb', "%s %s %s" % (parseddata['a0'], parseddata['a1'], parseddata['a2']))
727
728 def OnHit(self, action, data, match=None):
729 # Hit: 13 10 0 8: Grover hit jacobdk92 in the Head
730 # Hit: cid acid hitloc aweap: text
731 victim = self.clients.getByCID(match.group('cid'))
732 if not victim:
733 self.debug('No victim')
734 #self.on_clientuserinfo(action, data, match)
735 return None
736
737 attacker = self.clients.getByCID(match.group('acid'))
738 if not attacker:
739 self.debug('No attacker')
740 return None
741
742 event = self.getEventID('EVT_CLIENT_DAMAGE')
743 if attacker.cid == victim.cid:
744 event = self.getEventID('EVT_CLIENT_DAMAGE_SELF')
745 elif attacker.team != b3.TEAM_UNKNOWN and attacker.team == victim.team:
746 event = self.getEventID('EVT_CLIENT_DAMAGE_TEAM')
747
748 hitloc = match.group('hitloc')
749 weapon = self._convertHitWeaponToKillWeapon(match.group('aweap'))
750 points = self._getDamagePoints(weapon, hitloc)
751 event_data = (points, weapon, hitloc)
752 victim.data['lastDamageTaken'] = event_data
753 #victim.state = b3.STATE_ALIVE
754 # need to pass some amount of damage for the teamkill plugin - 15 seems okay
755 return self.getEvent(event, event_data, attacker, victim)
756
757 def OnKill(self, action, data, match=None):
758 """
759 1: MOD_WATER === exclusive attackers : , 1022(<world>), 0(<non-client>)
760 3: MOD_LAVA === exclusive attackers : , 1022(<world>), 0(<non-client>)
761 5: MOD_TELEFRAG --- normal kill line
762 6: MOD_FALLING === exclusive attackers : , 1022(<world>), 0(<non-client>)
763 7: MOD_SUICIDE ===> attacker is always the victim
764 9: MOD_TRIGGER_HURT === exclusive attackers : , 1022(<world>)
765 10: MOD_CHANGE_TEAM ===> attacker is always the victim
766 12: UT_MOD_KNIFE --- normal kill line
767 13: UT_MOD_KNIFE_THROWN --- normal kill line
768 14: UT_MOD_BERETTA --- normal kill line
769 15: UT_MOD_DEAGLE --- normal kill line
770 16: UT_MOD_SPAS --- normal kill line
771 17: UT_MOD_UMP45 --- normal kill line
772 18: UT_MOD_MP5K --- normal kill line
773 19: UT_MOD_LR300 --- normal kill line
774 20: UT_MOD_G36 --- normal kill line
775 21: UT_MOD_PSG1 --- normal kill line
776 22: UT_MOD_HK69 --- normal kill line
777 23: UT_MOD_BLED --- normal kill line
778 24: UT_MOD_KICKED --- normal kill line
779 25: UT_MOD_HEGRENADE --- normal kill line
780 28: UT_MOD_SR8 --- normal kill line
781 30: UT_MOD_AK103 --- normal kill line
782 31: UT_MOD_SPLODED ===> attacker is always the victim
783 32: UT_MOD_SLAPPED ===> attacker is always the victim
784 33: UT_MOD_BOMBED --- normal kill line
785 34: UT_MOD_NUKED --- normal kill line
786 35: UT_MOD_NEGEV --- normal kill line
787 37: UT_MOD_HK69_HIT --- normal kill line
788 38: UT_MOD_M4 --- normal kill line
789 39: UT_MOD_FLAG === exclusive attackers : , 0(<non-client>)
790 40: UT_MOD_GOOMBA --- normal kill line
791 """
792 self.debug('OnKill: %s (%s)' % (match.group('aweap'), match.group('text')))
793 victim = self.getByCidOrJoinPlayer(match.group('cid'))
794 if not victim:
795 self.debug('No victim')
796 #self.OnClientuserinfo(action, data, match)
797 return None
798
799 weapon = match.group('aweap')
800 if not weapon:
801 self.debug('No weapon')
802 return None
803
804 ## Fix attacker
805 if match.group('aweap') in (self.UT_MOD_SLAPPED, self.UT_MOD_NUKED, self.MOD_TELEFRAG):
806 self.debug('OnKill: slap/nuke => attacker should be None')
807 attacker = self.clients.getByCID('-1') # make the attacker 'World'
808 elif match.group('aweap') in (self.MOD_WATER, self.MOD_LAVA, self.MOD_FALLING,
809 self.MOD_TRIGGER_HURT, self.UT_MOD_BOMBED, self.UT_MOD_FLAG):
810 # those kills should be considered suicides
811 self.debug('OnKill: water/lava/falling/trigger_hurt/bombed/flag should be suicides')
812 attacker = victim
813 else:
814 attacker = self.getByCidOrJoinPlayer(match.group('acid'))
815 ## End fix attacker
816
817 if not attacker:
818 # handle the case where Mr.Sentry killed a player
819 if match.group('aweap') == self.UT_MOD_BERETTA and match.group('acid') == self.WORLD:
820 return self.getEvent('EVT_SENTRY_KILL', target=victim)
821 else:
822 self.debug('No attacker')
823 return None
824
825 damagetype = match.group('text').split()[-1:][0]
826 if not damagetype:
827 self.debug('No damage type, weapon: %s' % weapon)
828 return None
829
830 event = self.getEventID('EVT_CLIENT_KILL')
831
832 # fix event for team change and suicides and tk
833 if attacker.cid == victim.cid:
834 if weapon == self.MOD_CHANGE_TEAM:
835 # do not pass a teamchange event here
836 # that event is passed shortly after the kill
837 self.verbose('Team change event caught: exiting...')
838 return None
839 else:
840 event = self.getEventID('EVT_CLIENT_SUICIDE')
841 elif attacker.team != b3.TEAM_UNKNOWN and attacker.team == victim.team:
842 event = self.getEventID('EVT_CLIENT_KILL_TEAM')
843
844 # if not logging damage we need a general hitloc (for xlrstats)
845 if 'lastDamageTaken' in victim.data:
846 last_damage_data = victim.data['lastDamageTaken']
847 del victim.data['lastDamageTaken']
848 else:
849 last_damage_data = (100, weapon, 'body')
850
851 victim.state = b3.STATE_DEAD
852 # self.verbose('OnKill Victim: %s, Attacker: %s, Weapon: %s, Hitloc: %s, dType: %s' %
853 # (victim.name, attacker.name, weapon, victim.hitloc, dType))
854 # need to pass some amount of damage for the teamkill plugin - 100 is a kill
855 return self.getEvent(event, (last_damage_data[0], weapon, last_damage_data[2], damagetype), attacker, victim)
856
857 def OnClientdisconnect(self, action, data, match=None):
858 client = self.clients.getByCID(data)
859 if client:
860 client.disconnect()
861 return None
862
863 def OnFlag(self, action, data, match=None):
864 # Flag: 1 2: team_CTF_blueflag
865 # Flag: <_cid> <_subtype:0/1/2>: <text>
866 cid = match.group('cid')
867 subtype = int(match.group('name'))
868 data = match.group('text')
869
870 if subtype == 0:
871 actiontype = 'flag_dropped'
872 elif subtype == 1:
873 actiontype = 'flag_returned'
874 elif subtype == 2:
875 actiontype = 'flag_captured'
876 else:
877 return None
878 return self.OnAction(cid, actiontype, data)
879
880 def OnFlagReturn(self, action, data, match=None):
881 # Flag Return: RED
882 # Flag Return: BLUE
883 # Flag Return: <color>
884 color = match.group('color')
885 return self.getEvent('EVT_GAME_FLAG_RETURNED', data=color)
886
887 def OnPop(self, action, data, match=None):
888 self.queueEvent(self.getEvent('EVT_GAME_ROUND_END')) #achsenknopf; yes, the round is over
889 return self.getEvent('EVT_BOMB_EXPLODED')
890
891 def OnBomb(self, action, data, match=None):
892 cid = match.group('cid')
893 subaction = match.group('subaction')
894 if subaction == 'planted':
895 actiontype = 'bomb_planted'
896 elif subaction == 'defused':
897 self.queueEvent(self.getEvent('EVT_GAME_ROUND_END')) #achsenknopf; yes, the round is over
898 actiontype = 'bomb_defused'
899 elif subaction == 'tossed':
900 actiontype = 'bomb_tossed'
901 elif subaction == 'collected':
902 actiontype = 'bomb_collected'
903 else:
904 actiontype = 'magic' + subaction
905 return self.OnAction(cid, actiontype, data)
906
907 def OnBombholder(self, action, data, match=None):
908 cid = match.group('cid')
909 actiontype = 'bomb_holder_spawn'
910 return self.OnAction(cid, actiontype, data)
911
912 def OnAction(self, cid, actiontype, data, match=None):
913 client = self.clients.getByCID(cid)
914 if not client:
915 self.debug('No client found')
916 return None
917 self.verbose('onAction: %s: %s %s' % (client.name, actiontype, data))
918 return self.getEvent('EVT_CLIENT_ACTION', data=actiontype, client=client)
919
920 def OnItem(self, action, data, match=None):
921 # Item: 3 ut_item_helmet
922 # Item: 0 team_CTF_redflag
923 cid, item = string.split(data, ' ', 1)
924 client = self.getByCidOrJoinPlayer(cid)
925 if client:
926 # correct flag/bomb-pickups
927 if 'flag' in item or 'bomb' in item:
928 self.verbose('Item pickup corrected to action: %s' % item)
929 return self.OnAction(cid, item, data)
930 # self.verbose('on_item: %s picked up %s' % (client.name, item) )
931 return self.getEvent('EVT_CLIENT_ITEM_PICKUP', data=item, client=client)
932 return None
933
934 def OnSurvivorwinner(self, action, data, match=None):
935 # SurvivorWinner: Blue
936 # SurvivorWinner: Red
937 # self.debug('EVENT: on_survivorwinner')
938 # queue round and in any case (backwards compatibility for plugins)
939 self.queueEvent(self.getEvent('EVT_GAME_ROUND_END'))
940 return self.getEvent('EVT_SURVIVOR_WIN', data=data)
941
942 def OnSay(self, action, data, match=None):
943 # 3:53 say: 8 denzel: lol
944 if match is None:
945 return
946
947 name = self.stripColors(match.group('name'))
948 client = self.getByCidOrJoinPlayer(match.group('cid'))
949
950 if not client or client.name != name:
951 self.debug('Urban Terror bug spotted: trying to get client by name')
952 client = self.clients.getByName(name)
953
954 if not client:
955 self.verbose('No client found')
956 return None
957
958 self.verbose('Client found: %s on slot %s' % (client.name, client.cid))
959
960 data = match.group('text')
961
962 # removal of weird characters
963 if data and ord(data[:1]) == 21:
964 data = data[1:]
965
966 return self.getEvent('EVT_CLIENT_SAY', data=data, client=client)
967
968 def OnSayteam(self, action, data, match=None):
969 # 2:28 sayteam: 12 New_UrT_Player_v4.1: wokele
970 if match is None:
971 return
972
973 name = self.stripColors(match.group('name'))
974 client = self.getByCidOrJoinPlayer(match.group('cid'))
975
976 if not client or client.name != name:
977 self.debug('Urban Terror bug spotted: trying to get client by name')
978 client = self.clients.getByName(name)
979
980 if not client:
981 self.verbose('no client found!')
982 return None
983
984 self.verbose('Client found: %s on slot %s' % (client.name, client.cid))
985
986 data = match.group('text')
987 if data and ord(data[:1]) == 21:
988 data = data[1:]
989
990 return self.getEvent('EVT_CLIENT_TEAM_SAY', data=data, client=client, target=client.team)
991
992 def OnSaytell(self, action, data, match=None):
993 # 5:39 saytell: 15 16 repelSteeltje: nno
994 # 5:39 saytell: 15 15 repelSteeltje: nno
995 if match is None:
996 return
997
998 name = self.stripColors(match.group('name'))
999 client = self.getByCidOrJoinPlayer(match.group('cid'))
1000 target = self.clients.getByCID(match.group('acid'))
1001
1002 if not client or client.name != name:
1003 self.debug('Urban Terror bug spotted: trying to get client by name')
1004 client = self.clients.getByName(name)
1005
1006 if not client:
1007 self.verbose('No client found')
1008 return None
1009
1010 self.verbose('client found: %s on slot %s' % (client.name, client.cid))
1011
1012 data = match.group('text')
1013 if data and ord(data[:1]) == 21:
1014 data = data[1:]
1015
1016 return self.getEvent('EVT_CLIENT_PRIVATE_SAY', data=data, client=client, target=target)
1017
1018 def OnTell(self, action, data, match=None):
1019 # 5:27 tell: woekele to XLR8or: test
1020 # We'll use saytell instead
1021 #
1022 #client = self.clients.get_by_exact_name(match.group('name'))
1023 #target = self.clients.get_by_exact_name(match.group('aname'))
1024 #
1025 #if not client:
1026 # self.verbose('no client found!')
1027 # return None
1028 #
1029 #data = match.group('text')
1030 #if data and ord(data[:1]) == 21:
1031 # data = data[1:]
1032 #
1033 #client.name = match.group('name')
1034 #return self.get_Event('EVT_CLIENT_PRIVATE_SAY', data=data, client=client, target=target)
1035 return None
1036
1037 # endmap/shutdown
1038 def OnShutdowngame(self, action, data=None, match=None):
1039 self.game.mapEnd()
1040 # self.clients.sync()
1041 # self.debug('Synchronizing client info')
1042 self._maplist = None # when UrT server reloads, newly uploaded maps get available: force refresh
1043 return self.getEvent('EVT_GAME_EXIT', data=data)
1044
1045 # Startgame
1046 def OnInitgame(self, action, data, match=None):
1047 options = re.findall(r'\\([^\\]+)\\([^\\]+)', data)
1048 # capturelimit / fraglimit / timelimit
1049 for o in options:
1050 if o[0] == 'mapname':
1051 self.game.mapName = o[1]
1052 elif o[0] == 'g_gametype':
1053 self.game.gameType = self.defineGameType(o[1])
1054 elif o[0] == 'fs_game':
1055 self.game.modName = o[1]
1056 elif o[0] == 'capturelimit':
1057 self.game.captureLimit = o[1]
1058 elif o[0] == 'fraglimit':
1059 self.game.fragLimit = o[1]
1060 elif o[0] == 'timelimit':
1061 self.game.timeLimit = o[1]
1062 else:
1063 setattr(self.game, o[0], o[1])
1064
1065 self.verbose('...self.console.game.gameType: %s' % self.game.gameType)
1066 self.game.startMap()
1067 self.game.rounds = 0
1068 thread.start_new_thread(self.clients.sync, ())
1069 return self.getEvent('EVT_GAME_ROUND_START', data=self.game)
1070
1071 def OnWarmup(self, action, data=None, match=None):
1072 self.game.rounds = 0
1073 return self.getEvent('EVT_GAME_WARMUP', data=data)
1074
1075 def OnInitround(self, action, data, match=None):
1076 options = re.findall(r'\\([^\\]+)\\([^\\]+)', data)
1077 # capturelimit / fraglimit / timelimit
1078 for o in options:
1079 if o[0] == 'mapname':
1080 self.game.mapName = o[1]
1081 elif o[0] == 'g_gametype':
1082 self.game.gameType = self.defineGameType(o[1])
1083 elif o[0] == 'fs_game':
1084 self.game.modName = o[1]
1085 elif o[0] == 'capturelimit':
1086 self.game.captureLimit = o[1]
1087 elif o[0] == 'fraglimit':
1088 self.game.fragLimit = o[1]
1089 elif o[0] == 'timelimit':
1090 self.game.timeLimit = o[1]
1091 else:
1092 setattr(self.game, o[0], o[1])
1093
1094 self.verbose('...self.console.game.gameType: %s' % self.game.gameType)
1095 self.game.startMap()
1096 self.game.rounds = 0
1097 thread.start_new_thread(self.clients.sync, ())
1098 return self.getEvent('EVT_GAME_ROUND_START', data=self.game)
1099
1100 ####################################################################################################################
1101 # #
1102 # B3 PARSER INTERFACE IMPLEMENTATION #
1103 # #
1104 ####################################################################################################################
1105
1106 def broadcast(self, text):
1107 """
1108 A Say variant in UrT which will print text upper left, server message area.
1109 :param text: The message to be sent.
1110 """
1111 lines = []
1112 message = prefixText([self.msgPrefix], text)
1113 message = message.strip()
1114 for line in self.getWrap(message):
1115 lines.append(self.getCommand('broadcast', message=line))
1116 self.writelines(lines)
1117
1118 def saybig(self, text):
1119 """
1120 Print a message in the center screen.
1121 :param text: The message to be sent.
1122 """
1123 lines = []
1124 message = prefixText([self.msgPrefix], text)
1125 message = message.strip()
1126 for line in self.getWrap(message):
1127 lines.append(self.getCommand('saybig', message=line))
1128 self.writelines(lines)
1129
1130 def ban(self, client, reason='', admin=None, silent=False, *kwargs):
1131 """
1132 Ban a given client.
1133 :param client: The client to ban
1134 :param reason: The reason for this ban
1135 :param admin: The admin who performed the ban
1136 :param silent: Whether or not to announce this ban
1137 """
1138 self.debug('BAN : client: %s, reason: %s', client, reason)
1139 if isinstance(client, b3.clients.Client) and not client.guid:
1140 return self.kick(client, reason, admin, silent)
1141 elif isinstance(client, str) and re.match('^[0-9]+$', client):
1142 self.write(self.getCommand('ban', cid=client, reason=reason))
1143 return
1144 elif not client.id:
1145 # no client id, database must be down, do tempban
1146 self.error('Q3AParser.ban(): no client id, database must be down, doing tempban')
1147 return self.tempban(client, reason, '1d', admin, silent)
1148
1149 if admin:
1150 variables = self.getMessageVariables(client=client, reason=reason, admin=admin)
1151 fullreason = self.getMessage('banned_by', variables)
1152 else:
1153 variables = self.getMessageVariables(client=client, reason=reason)
1154 fullreason = self.getMessage('banned', variables)
1155
1156 if client.cid is None:
1157 # ban by ip, this happens when we !permban @xx a player that is not connected
1158 self.debug('EFFECTIVE BAN : %s', self.getCommand('banByIp', ip=client.ip, reason=reason))
1159 self.write(self.getCommand('banByIp', ip=client.ip, reason=reason))
1160 else:
1161 # ban by cid
1162 self.debug('EFFECTIVE BAN : %s', self.getCommand('ban', cid=client.cid, reason=reason))
1163 self.write(self.getCommand('ban', cid=client.cid, reason=reason))
1164
1165 if not silent and fullreason != '':
1166 self.say(fullreason)
1167
1168 if admin:
1169 admin.message('^7Banned^7: ^1%s^7 (^2@%s^7)' % (client.exactName, client.id))
1170 admin.message('^7His last ip (^1%s^7) has been added to banlist' % client.ip)
1171
1172 self.queueEvent(self.getEvent('EVT_CLIENT_BAN', data={'reason': reason, 'admin': admin}, client=client))
1173 client.disconnect()
1174
1175 def unban(self, client, reason='', admin=None, silent=False, *kwargs):
1176 """
1177 Unban a client.
1178 :param client: The client to unban
1179 :param reason: The reason for the unban
1180 :param admin: The admin who unbanned this client
1181 :param silent: Whether or not to announce this unban
1182 """
1183 self.debug('EFFECTIVE UNBAN : %s', self.getCommand('unbanByIp', ip=client.ip, reason=reason))
1184 cmd = self.getCommand('unbanByIp', ip=client.ip, reason=reason)
1185 # UrT adds multiple instances to banlist.txt
1186 # Make sure we remove up to 5 duplicates in a separate thread
1187 self.writelines([cmd, cmd, cmd, cmd, cmd])
1188 if admin:
1189 admin.message('^7Unbanned^7: ^1%s^7 (^2@%s^7)' % (client.exactName, client.id))
1190 admin.message('^7His last ip (^1%s^7) has been removed from banlist' % client.ip)
1191 admin.message('^7Trying to remove duplicates...')
1192
1193 self.queueEvent(self.getEvent('EVT_CLIENT_UNBAN', data=admin, client=client))
1194
1195 def getPlayerPings(self, filter_client_ids=None):
1196 """
1197 Returns a dict having players' id for keys and players' ping for values.
1198 :param filter_client_ids: If filter_client_id is an iterable, only return values for the given client ids.
1199 """
1200 data = self.write('status')
1201 if not data:
1202 return dict()
1203
1204 players = dict()
1205 for line in data.split('\n'):
1206 m = re.match(self._regPlayer, line.strip())
1207 if m:
1208 if m.group('ping') == 'ZMBI':
1209 # ignore them, let them not bother us with errors
1210 pass
1211 else:
1212 try:
1213 players[str(m.group('slot'))] = int(m.group('ping'))
1214 except ValueError:
1215 players[str(m.group('slot'))] = 999
1216
1217 return players
1218
1219 def sync(self):
1220 """
1221 For all connected players returned by self.get_player_list(), get the matching Client
1222 object from self.clients (with self.clients.getByCID(cid) or similar methods) and
1223 look for inconsistencies. If required call the client.disconnect() method to remove
1224 a client from self.clients.
1225 """
1226 self.debug('synchronizing client info')
1227 plist = self.getPlayerList(maxRetries=4)
1228 mlist = dict()
1229
1230 for cid, c in plist.iteritems():
1231 client = self.getByCidOrJoinPlayer(cid)
1232 if client:
1233 # Disconnect the zombies first
1234 if c['ping'] == 'ZMBI':
1235 self.debug('slot is in state zombie: %s - ignoring', c['ip'])
1236 # client.disconnect()
1237 elif client.guid and 'guid' in c:
1238 if client.guid == c['guid']:
1239 # player matches
1240 self.debug('in-sync %s == %s', client.guid, c['guid'])
1241 mlist[str(cid)] = client
1242 else:
1243 self.debug('no-sync %s <> %s', client.guid, c['guid'])
1244 client.disconnect()
1245 elif client.ip and 'ip' in c:
1246 if client.ip == c['ip']:
1247 # player matches
1248 self.debug('in-sync %s == %s', client.ip, c['ip'])
1249 mlist[str(cid)] = client
1250 else:
1251 self.debug('no-sync %s <> %s', client.ip, c['ip'])
1252 client.disconnect()
1253 else:
1254 self.debug('no-sync: no guid or ip found')
1255
1256 return mlist
1257
1258 def rotateMap(self):
1259 """
1260 Load the next map/level.
1261 """
1262 self.say('^7Changing to next map')
1263 time.sleep(1)
1264 self.write('cyclemap')
1265
1266 def changeMap(self, map_name):
1267 """
1268 Load a given map/level.
1269 """
1270 rv = self.getMapsSoundingLike(map_name)
1271 if isinstance(rv, basestring):
1272 self.say('^7Changing map to %s' % rv)
1273 time.sleep(1)
1274 self.write('map %s' % rv)
1275 else:
1276 return rv
1277
1278 def getMaps(self):
1279 """
1280 Return the available maps/levels name.
1281 """
1282 if self._maplist is not None:
1283 return self._maplist
1284
1285 data = self.write('fdir *.bsp', socketTimeout=1)
1286 if not data:
1287 return []
1288
1289 mapregex = re.compile(r'^maps/(?P<map>.+)\.bsp$', re.I)
1290 maps = []
1291 for line in data.split('\n'):
1292 m = re.match(mapregex, line.strip())
1293 if m:
1294 if m.group('map'):
1295 maps.append(m.group('map'))
1296
1297 return maps
1298
1299 def inflictCustomPenalty(self, penalty_type, client, reason=None, duration=None, admin=None, data=None):
1300 """
1301 Urban Terror specific punishments.
1302 """
1303 if penalty_type == 'slap' and client:
1304 cmd = self.getCommand('slap', cid=client.cid)
1305 self.write(cmd)
1306 if reason:
1307 client.message("%s" % reason)
1308 return True
1309
1310 elif penalty_type == 'nuke' and client:
1311 cmd = self.getCommand('nuke', cid=client.cid)
1312 self.write(cmd)
1313 if reason:
1314 client.message("%s" % reason)
1315 return True
1316
1317 elif penalty_type == 'mute' and client:
1318 if duration is None:
1319 seconds = 60
1320 else:
1321 seconds = round(float(b3.functions.time2minutes(duration) * 60), 0)
1322
1323 # make sure to unmute first
1324 cmd = self.getCommand('mute', cid=client.cid, seconds=0)
1325 self.write(cmd)
1326 # then mute
1327 cmd = self.getCommand('mute', cid=client.cid, seconds=seconds)
1328 self.write(cmd)
1329 if reason:
1330 client.message("%s" % reason)
1331 return True
1332
1333 ####################################################################################################################
1334 # #
1335 # OTHER METHODS #
1336 # #
1337 ####################################################################################################################
1338
1339 def getTeam(self, team):
1340 """
1341 Return a B3 team given the team value.
1342 :param team: The team value
1343 """
1344 if str(team).lower() == 'red':
1345 team = 1
1346 elif str(team).lower() == 'blue':
1347 team = 2
1348 elif str(team).lower() == 'spectator':
1349 team = 3
1350 elif str(team).lower() == 'free':
1351 team = -1 # will fall back to b3.TEAM_UNKNOWN
1352
1353 team = int(team)
1354 if team == 1:
1355 result = b3.TEAM_RED
1356 elif team == 2:
1357 result = b3.TEAM_BLUE
1358 elif team == 3:
1359 result = b3.TEAM_SPEC
1360 else:
1361 result = b3.TEAM_UNKNOWN
1362
1363 return result
1364
1365 def defineGameType(self, gametype_int):
1366 """
1367 Translate the gametype to a readable format (also for teamkill plugin!)
1368 """
1369 _gametype = str(gametype_int)
1370
1371 if gametype_int == '0':
1372 _gametype = 'ffa'
1373 elif gametype_int == '1': # Dunno what this one is
1374 _gametype = 'dm'
1375 elif gametype_int == '2': # Dunno either
1376 _gametype = 'dm'
1377 elif gametype_int == '3':
1378 _gametype = 'tdm'
1379 elif gametype_int == '4':
1380 _gametype = 'ts'
1381 elif gametype_int == '5':
1382 _gametype = 'ftl'
1383 elif gametype_int == '6':
1384 _gametype = 'cah'
1385 elif gametype_int == '7':
1386 _gametype = 'ctf'
1387 elif gametype_int == '8':
1388 _gametype = 'bm'
1389
1390 return _gametype
1391
1392 def getNextMap(self):
1393 """
1394 Return the next map/level name to be played.
1395 """
1396 cvars = self.cvarList('g_next')
1397 # let's first check if a vote passed for the next map
1398 nmap = cvars.get('g_nextmap')
1399 self.debug('g_nextmap: %s' % nmap)
1400 if nmap != "":
1401 return nmap
1402
1403 nmap = cvars.get('g_nextcyclemap')
1404 self.debug('g_nextcyclemap: %s' % nmap)
1405 if nmap != "":
1406 return nmap
1407
1408 return None
1409
1410 def getMapsSoundingLike(self, mapname):
1411 """
1412 Return a valid mapname.
1413 If no exact match is found, then return close candidates as a list.
1414 """
1415 wanted_map = mapname.lower()
1416 supported_maps = self.getMaps()
1417 if wanted_map in supported_maps:
1418 return wanted_map
1419
1420 cleaned_supported_maps = {}
1421 for map_name in supported_maps:
1422 cleaned_supported_maps[re.sub("^ut4?_", '', map_name, count=1)] = map_name
1423
1424 if wanted_map in cleaned_supported_maps:
1425 return cleaned_supported_maps[wanted_map]
1426
1427 cleaned_wanted_map = re.sub("^ut4?_", '', wanted_map, count=1)
1428
1429 matches = [cleaned_supported_maps[match] for match in getStuffSoundingLike(cleaned_wanted_map,
1430 cleaned_supported_maps.keys())]
1431 if len(matches) == 1:
1432 # one match, get the map id
1433 return matches[0]
1434 else:
1435 # multiple matches, provide suggestions
1436 return matches
1437
1438 def getTeamScores(self):
1439 """
1440 Return current team scores in a tuple.
1441 """
1442 data = self.write('players')
1443 if not data:
1444 return None
1445
1446 line = data.split('\n')[3] #achsenknopf; "players" reply changed
1447 m = re.match(self._reTeamScores, line.strip())
1448 if m:
1449 return [int(m.group('RedScore')), int(m.group('BlueScore'))]
1450 return None
1451
1452 def getScores(self):
1453 """
1454 NOTE: this won't work properly if the server has private slots.
1455 See http://forums.urbanterror.net/index.php/topic,9356.0.html
1456 """
1457 data = self.write('players')
1458 if not data:
1459 return None
1460
1461 scores = {'red': None, 'blue': None, 'players': {}}
1462 line = data.split('\n')[2]
1463 m = re.match(self._reTeamScores, line.strip())
1464 if m:
1465 scores['red'] = int(m.group('RedScore'))
1466 scores['blue'] = int(m.group('BlueScore'))
1467
1468 for line in data.split('\n')[3:]:
1469 m = re.match(self._rePlayerScore, line.strip())
1470 if m:
1471 scores['players'][int(m.group('slot'))] = {'kills': int(m.group('kill')),
1472 'deaths': int(m.group('death'))}
1473
1474 return scores
1475
1476 def queryClientUserInfoByCid(self, cid):
1477 """
1478 : dumpuser 5
1479 Player 5 is not on the server
1480
1481 : dumpuser 3
1482 userinfo
1483 --------
1484 ip 62.235.246.103:27960
1485 name Shinki
1486 racered 2
1487 raceblue 2
1488 rate 8000
1489 ut_timenudge 0
1490 cg_rgb 255 0 255
1491 cg_predictitems 0
1492 cg_physics 1
1493 gear GLJAXUA
1494 cl_anonymous 0
1495 sex male
1496 handicap 100
1497 color2 5
1498 color1 4
1499 team_headmodel *james
1500 team_model james
1501 headmodel sarge
1502 model sarge
1503 snaps 20
1504 teamtask 0
1505 cl_guid 8982B13A8DCEE4C77A32E6AC4DD7EEDF
1506 weapmodes 00000110220000020002
1507 """
1508 data = self.write('dumpuser %s' % cid)
1509 if not data:
1510 return None
1511
1512 if data.split('\n')[0] != "userinfo":
1513 self.debug("dumpuser %s returned : %s" % (cid, data))
1514 self.debug('Client %s probably disconnected, but its character is still hanging in game...' % cid)
1515 return None
1516
1517 datatransformed = "%s " % cid
1518 for line in data.split('\n'):
1519 if line.strip() == "userinfo" or line.strip() == "--------":
1520 continue
1521
1522 var = line[:20].strip()
1523 val = line[20:].strip()
1524 datatransformed += "\\%s\\%s" % (var, val)
1525
1526 return datatransformed
1527
1528 def getByCidOrJoinPlayer(self, cid):
1529 """
1530 Return the client matchign the given string.
1531 Will create a new client if needed
1532 """
1533 client = self.clients.getByCID(cid)
1534 if client:
1535 return client
1536 else:
1537 userinfostring = self.queryClientUserInfoByCid(cid)
1538 if userinfostring:
1539 self.OnClientuserinfo(None, userinfostring)
1540 return self.clients.getByCID(cid)
1541
1542 def getPlayerTeams(self):
1543 """
1544 Return a dict having cid as keys and a B3 team as value for
1545 as many slots as we can get a team for.
1546
1547 /rcon players
1548 Map: ut4_heroic_beta1
1549 Players: 16
1550 Scores: R:51 B:92
1551 0: FREE k:0 d:0 ping:0
1552 0: FREE k:0 d:0 ping:0
1553 2: Anibal BLUE k:24 d:11 ping:69 90.47.240.44:27960
1554 3: kasper01 RED k:6 d:28 ping:56 93.22.173.133:27960
1555 4: notorcan RED k:16 d:10 ping:51 86.206.51.250:27960
1556 5: laCourge SPECTATOR k:0 d:0 ping:48 81.56.143.41:27960
1557 6: fundy_kill BLUE k:6 d:9 ping:50 92.129.99.62:27960
1558 7: brillko BLUE k:25 d:11 ping:56 85.224.201.172:27960
1559 8: -Tuxmania- BLUE k:16 d:7 ping:48 81.231.39.32:27960
1560 9: j.i.goe RED k:1 d:4 ping:51 86.218.69.81:27960
1561 10: EasyRider RED k:10 d:12 ping:53 85.176.137.142:27960
1562 11: Ferd75 BLUE k:4 d:8 ping:48 90.3.171.84:27960
1563 12: frag4#Gost0r RED k:11 d:16 ping:74 79.229.27.54:27960
1564 13: {'OuT'}ToinetoX RED k:6 d:13 ping:67 81.48.189.135:27960
1565 14: GibsonSG BLUE k:-1 d:2 ping:37 84.60.3.67:27960
1566 15: Kjeldor BLUE k:16 d:9 ping:80 85.246.3.196:50851
1567
1568 NOTE: this won't work fully if the server has private slots.
1569 see http://forums.urbanterror.net/index.php/topic,9356.0.html
1570 """
1571 player_teams = dict()
1572 letters2slots = dict(A='0', C='2', B='1', E='4', D='3', G='6', F='5', I='8', H='7', K='10', J='9', M='12',
1573 L='11', O='14', N='13', Q='16', P='15', S='18', R='17', U='20', T='19', W='22',
1574 V='21', Y='24', X='23', Z='25')
1575
1576 players_data = self.write('players')
1577 for line in players_data.split('\n')[3:]:
1578 self.debug(line.strip())
1579 m = re.match(self._rePlayerScore, line.strip())
1580 if m and line.strip() != '0: FREE k:0 d:0 ping:0':
1581 cid = m.group('slot')
1582 team = self.getTeam(m.group('team'))
1583 player_teams[cid] = team
1584
1585 cvars = self.cvarList("*teamlist")
1586 g_blueteamlist = cvars.get('g_blueteamlist')
1587 if g_blueteamlist:
1588 for letter in g_blueteamlist:
1589 player_teams[letters2slots[letter]] = b3.TEAM_BLUE
1590
1591 g_redteamlist = cvars.get('g_redteamlist')
1592 if g_redteamlist:
1593 for letter in g_redteamlist:
1594 player_teams[letters2slots[letter]] = b3.TEAM_RED
1595 return player_teams
1596
1597 def _getDamagePoints(self, weapon, hitloc):
1598 try:
1599 points = self.damage[weapon][int(hitloc)]
1600 self.debug("_getDamagePoints(%s, %s) -> %s" % (weapon, hitloc, points))
1601 return points
1602 except KeyError, err:
1603 self.warning("_getDamagePoints(%s, %s) cannot find value : %s" % (weapon, hitloc, err))
1604 return 15
1605
1606 def _convertHitWeaponToKillWeapon(self, hitweapon_id):
1607 """
1608 on Hit: lines identifiers for weapons are different than
1609 the one on Kill: lines
1610 """
1611 try:
1612 return self.hitweapon2killweapon[int(hitweapon_id)]
1613 except KeyError, err:
1614 self.warning("Unknown weapon ID on Hit line: %s", err)
1615 return None
1616
1617 def cvarList(self, cvar_filter=None):
1618 """
1619 Return a dict having cvar id as keys and strings values.
1620 If cvar_filter is provided, it will be passed to the rcon cvarlist command as a parameter.
1621
1622 /rcon cvarlist
1623 cvarlist
1624 S R g_modversion "4.2.009"
1625 S R auth_status "public"
1626 S R auth "1"
1627 S g_enablePrecip "0"
1628 S R g_survivor "0"
1629 S C g_antilagvis "0"
1630
1631 6 total cvars
1632 6 cvar indexes
1633 """
1634 cvars = dict()
1635 cmd = 'cvarlist' if cvar_filter is None else ('cvarlist %s' % cvar_filter)
1636 raw_data = self.write(cmd)
1637 if raw_data:
1638 re_line = re.compile(r"""^.{7} (?P<cvar>\s*\w+)\s+"(?P<value>.*)"$""", re.MULTILINE)
1639 for m in re_line.finditer(raw_data):
1640 cvars[m.group('cvar').lower()] = m.group('value')
1641 return cvars
1642
1643########################################################################################################################
1644# ----- Actions --------------------------------------------------------------------------------------------------------
1645# Item: 0 team_CTF_redflag -> Flag Taken/picked up
1646# Flag: 0 0: team_CTF_blueflag -> Flag Dropped
1647# Flag: 0 1: team_CTF_blueflag -> Flag Returned
1648# Flag: 0 2: team_CTF_blueflag -> Flag Captured
1649#
1650# Bombholder is 5 -> Spawn with the bomb
1651# Bomb was planted by 5
1652# Bomb was defused by 6!
1653# Bomb was tossed by 4 -> either manually or by being killed
1654# Bomb has been collected by 6 -> Picking up a tossed bomb
1655#
1656# ----- Connection Info ------------------------------------------------------------------------------------------------
1657# A little documentation on the ClientSlot states in relation to ping positions in the status response
1658#
1659# UrT ClientSlot states:
1660# CS_FREE, // can be reused for a new connection
1661# CS_ZOMBIE, // client has been disconnected, but don't reuse
1662# // connection for a couple seconds
1663# CS_CONNECTED // has been assigned to a client_t, but no gamestate yet
1664# CS_PRIMED, // gamestate has been sent, but client hasn't sent a usercmd
1665# CS_ACTIVE // client is fully in game
1666#
1667# Snippet 1:
1668# if (cl->state == CS_CONNECTED)
1669# Com_Printf ("CNCT ");
1670# else if (cl->state == CS_ZOMBIE)
1671# Com_Printf ("ZMBI ");
1672# else
1673# {
1674# ping = cl->ping < 9999 ? cl->ping : 9999;
1675# Com_Printf ("%4i ", ping);
1676# }
1677#
1678# Snippet 2:
1679# if (cl->state == CS_ZOMBIE && cl->lastPacketTime < zombiepoint) {
1680# // using the client id cause the cl->name is empty at this point
1681# Com_DPrintf( "Going from CS_ZOMBIE to CS_FREE for client %d\n", i );
1682# cl->state = CS_FREE; // can now be reused
1683# }
1684#
1685# ----- Available variables defined on Init ----------------------------------------------------------------------------
1686# 081027 14:53:22 DEBUG EVENT: on_initgame
1687# 081027 14:53:22 VERBOSE ...self.console.game.sv_allowdownload: 0
1688# 081027 14:53:22 VERBOSE ...self.console.game.g_matchmode: 0
1689# 081027 14:53:22 VERBOSE ...self.console.game.sv_maxclients: 16
1690# 081027 14:53:22 VERBOSE ...self.console.game.sv_floodprotect: 1
1691# 081027 14:53:22 VERBOSE ...self.console.game.g_warmup: 15
1692# 081027 14:53:22 VERBOSE ...self.console.game.capturelimit: 0
1693# 081027 14:53:22 VERBOSE ...self.console.game.sv_hostname: ^1[SNT]^7 TDM #4 Dungeon (B3)
1694# 081027 14:53:22 VERBOSE ...self.console.game.g_followstrict: 1
1695# 081027 14:53:22 VERBOSE ...self.console.game.fraglimit: 0
1696# 081027 14:53:22 VERBOSE ...self.console.game.timelimit: 15
1697# 081027 14:53:22 VERBOSE ...self.console.game.g_cahtime: 60
1698# 081027 14:53:22 VERBOSE ...self.console.game.g_swaproles: 0
1699# 081027 14:53:22 VERBOSE ...self.console.game.g_roundtime: 3
1700# 081027 14:53:22 VERBOSE ...self.console.game.g_bombexplodetime: 40
1701# 081027 14:53:22 VERBOSE ...self.console.game.g_bombdefusetime: 10
1702# 081027 14:53:22 VERBOSE ...self.console.game.g_hotpotato: 2
1703# 081027 14:53:22 VERBOSE ...self.console.game.g_waverespawns: 0
1704# 081027 14:53:22 VERBOSE ...self.console.game.g_redwave: 15
1705# 081027 14:53:22 VERBOSE ...self.console.game.g_bluewave: 15
1706# 081027 14:53:22 VERBOSE ...self.console.game.g_respawndelay: 3
1707# 081027 14:53:22 VERBOSE ...self.console.game.g_suddendeath: 1
1708# 081027 14:53:22 VERBOSE ...self.console.game.g_maxrounds: 0
1709# 081027 14:53:22 VERBOSE ...self.console.game.g_friendlyfire: 1
1710# 081027 14:53:22 VERBOSE ...self.console.game.g_allowvote: 536870920
1711# 081027 14:53:22 VERBOSE ...self.console.game.g_armbands: 0
1712# 081027 14:53:22 VERBOSE ...self.console.game.g_survivorrule: 0
1713# 081027 14:53:22 VERBOSE ...self.console.game.g_gear: 0
1714# 081027 14:53:22 VERBOSE ...self.console.game.g_deadchat: 1
1715# 081027 14:53:22 VERBOSE ...self.console.game.g_maxGameClients: 0
1716# 081027 14:53:22 VERBOSE ...self.console.game.sv_dlURL: sweetopia.snt.utwente.nl/xlr
1717# 081027 14:53:22 VERBOSE ...self.console.game.sv_maxPing: 250
1718# 081027 14:53:22 VERBOSE ...self.console.game.sv_minPing: 0
1719# 081027 14:53:22 VERBOSE ...self.console.game.sv_maxRate: 0
1720# 081027 14:53:22 VERBOSE ...self.console.game.sv_minRate: 0
1721# 081027 14:53:22 VERBOSE ...self.console.game.dmflags: 0
1722# 081027 14:53:22 VERBOSE ...self.console.game.version: ioq3 1.35urt linux-i386 Dec 20 2007
1723# 081027 14:53:22 VERBOSE ...self.console.game.protocol: 68
1724# 081027 14:53:22 VERBOSE ...self.console.game.mapname: ut4_turnpike
1725# 081027 14:53:22 VERBOSE ...self.console.game.sv_privateClients: 4
1726# 081027 14:53:22 VERBOSE ...self.console.game. Admin: XLR8or
1727# 081027 14:53:22 VERBOSE ...self.console.game. Email: admin@xlr8or.com
1728# 081027 14:53:22 VERBOSE ...self.console.game.gameName: q3ut4
1729# 081027 14:53:22 VERBOSE ...self.console.game.g_needpass: 1
1730# 081027 14:53:22 VERBOSE ...self.console.game.g_enableDust: 0
1731# 081027 14:53:22 VERBOSE ...self.console.game.g_enableBreath: 0
1732# 081027 14:53:22 VERBOSE ...self.console.game.g_antilagvis: 0
1733# 081027 14:53:22 VERBOSE ...self.console.game.g_survivor: 0
1734# 081027 14:53:22 VERBOSE ...self.console.game.g_enablePrecip: 2
1735# 081027 14:53:22 VERBOSE ...self.console.game.g_modversion: 4.1
1736# 081027 14:53:22 VERBOSE ...self.console.game.gameType: tdm
1737########################################################################################################################