| 1 | """ |
|---|
| 2 | Python Quake 3 Library |
|---|
| 3 | http://misc.slowchop.com/misc/wiki/pyquake3 |
|---|
| 4 | Copyright (C) 2006-2007 Gerald Kaszuba |
|---|
| 5 | |
|---|
| 6 | This program is free software; you can redistribute it and/or |
|---|
| 7 | modify it under the terms of the GNU General Public License |
|---|
| 8 | as published by the Free Software Foundation; either version 2 |
|---|
| 9 | of the License, or (at your option) any later version. |
|---|
| 10 | |
|---|
| 11 | This program is distributed in the hope that it will be useful, |
|---|
| 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 14 | GNU General Public License for more details. |
|---|
| 15 | |
|---|
| 16 | You should have received a copy of the GNU General Public License |
|---|
| 17 | along with this program; if not, write to the Free Software |
|---|
| 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|---|
| 19 | """ |
|---|
| 20 | |
|---|
| 21 | import socket |
|---|
| 22 | import re |
|---|
| 23 | |
|---|
| 24 | class Player: |
|---|
| 25 | def __init__(self, name, frags, ping, address=None, bot=-1): |
|---|
| 26 | self.name = name |
|---|
| 27 | self.frags = frags |
|---|
| 28 | self.ping = ping |
|---|
| 29 | self.address = address |
|---|
| 30 | self.bot = bot |
|---|
| 31 | def __str__(self): |
|---|
| 32 | return self.name |
|---|
| 33 | def __repr__(self): |
|---|
| 34 | return str(self) |
|---|
| 35 | |
|---|
| 36 | class PyQuake3: |
|---|
| 37 | packet_prefix = '\xff' * 4 |
|---|
| 38 | player_reo = re.compile(r'^(\d+) (\d+) "(.*)"') |
|---|
| 39 | def __init__(self, server, rcon_password=''): |
|---|
| 40 | self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
|---|
| 41 | self.set_server(server) |
|---|
| 42 | self.set_rcon_password(rcon_password) |
|---|
| 43 | def set_server(self, server): |
|---|
| 44 | try: |
|---|
| 45 | self.address, self.port = server.split(':') |
|---|
| 46 | except: |
|---|
| 47 | raise Exception('Server address must be in the format of \ |
|---|
| 48 | "address:port"') |
|---|
| 49 | self.port = int(self.port) |
|---|
| 50 | self.s.connect((self.address, self.port)) |
|---|
| 51 | def get_address(self): |
|---|
| 52 | return '%s:%s' % (self.address, self.port) |
|---|
| 53 | def set_rcon_password(self, rcon_password): |
|---|
| 54 | self.rcon_password = rcon_password |
|---|
| 55 | def send_packet(self, data): |
|---|
| 56 | self.s.send('%s%s\n' % (self.packet_prefix, data)) |
|---|
| 57 | def recv(self, timeout=1): |
|---|
| 58 | self.s.settimeout(timeout) |
|---|
| 59 | try: |
|---|
| 60 | return self.s.recv(4096) |
|---|
| 61 | except socket.error, e: |
|---|
| 62 | raise Exception('Error receiving the packet: %s' % \ |
|---|
| 63 | e[1]) |
|---|
| 64 | def command(self, cmd, timeout=1, retries=3): |
|---|
| 65 | while retries: |
|---|
| 66 | self.send_packet(cmd) |
|---|
| 67 | try: |
|---|
| 68 | data = self.recv(timeout) |
|---|
| 69 | except: |
|---|
| 70 | data = None |
|---|
| 71 | if data: |
|---|
| 72 | return self.parse_packet(data) |
|---|
| 73 | retries -= 1 |
|---|
| 74 | raise Exception('Server response timed out') |
|---|
| 75 | def rcon(self, cmd): |
|---|
| 76 | r = self.command('rcon "%s" %s' % (self.rcon_password, cmd)) |
|---|
| 77 | if r[1] == 'No rconpassword set on the server.\n' or r[1] == \ |
|---|
| 78 | 'Bad rconpassword.\n': |
|---|
| 79 | raise Exception(r[1][:-1]) |
|---|
| 80 | return r |
|---|
| 81 | def parse_packet(self, data): |
|---|
| 82 | if data.find(self.packet_prefix) != 0: |
|---|
| 83 | raise Exception('Malformed packet') |
|---|
| 84 | first_line_length = data.find('\n') |
|---|
| 85 | if first_line_length == -1: |
|---|
| 86 | raise Exception('Malformed packet') |
|---|
| 87 | response_type = data[len(self.packet_prefix):first_line_length] |
|---|
| 88 | response_data = data[first_line_length+1:] |
|---|
| 89 | return response_type, response_data |
|---|
| 90 | def parse_status(self, data): |
|---|
| 91 | split = data[1:].split('\\') |
|---|
| 92 | values = dict(zip(split[::2], split[1::2])) |
|---|
| 93 | # if there are \n's in one of the values, it's the list of players |
|---|
| 94 | for var, val in values.items(): |
|---|
| 95 | pos = val.find('\n') |
|---|
| 96 | if pos == -1: |
|---|
| 97 | continue |
|---|
| 98 | split = val.split('\n', 1) |
|---|
| 99 | values[var] = split[0] |
|---|
| 100 | self.parse_players(split[1]) |
|---|
| 101 | return values |
|---|
| 102 | def parse_players(self, data): |
|---|
| 103 | self.players = [] |
|---|
| 104 | for player in data.split('\n'): |
|---|
| 105 | if not player: |
|---|
| 106 | continue |
|---|
| 107 | match = self.player_reo.match(player) |
|---|
| 108 | if not match: |
|---|
| 109 | print 'couldnt match', player |
|---|
| 110 | continue |
|---|
| 111 | frags, ping, name = match.groups() |
|---|
| 112 | self.players.append(Player(name, frags, ping)) |
|---|
| 113 | def update(self): |
|---|
| 114 | cmd, data = self.command('getstatus') |
|---|
| 115 | self.vars = self.parse_status(data) |
|---|
| 116 | def rcon_update(self): |
|---|
| 117 | cmd, data = self.rcon('status') |
|---|
| 118 | lines = data.split('\n') |
|---|
| 119 | players = lines[3:] |
|---|
| 120 | self.players = [] |
|---|
| 121 | for p in players: |
|---|
| 122 | while p.find(' ') != -1: |
|---|
| 123 | p = p.replace(' ', ' ') |
|---|
| 124 | while p.find(' ') == 0: |
|---|
| 125 | p = p[1:] |
|---|
| 126 | if p == '': |
|---|
| 127 | continue |
|---|
| 128 | p = p.split(' ') |
|---|
| 129 | self.players.append(Player(p[3][:-2], p[0], p[1], p[5], p[6])) |
|---|
| 130 | |
|---|
| 131 | if __name__ == '__main__': |
|---|
| 132 | q = PyQuake3('localhost:27960','hello') |
|---|
| 133 | q.update() |
|---|
| 134 | print 'The name of %s is %s, running map %s with %s player(s).' % \ |
|---|
| 135 | (q.get_address(), q.vars['sv_hostname'], \ |
|---|
| 136 | q.vars['mapname'], len(q.players)) |
|---|
| 137 | for player in q.players: |
|---|
| 138 | print '%s with %s frags and a %sms ping' % (player.name, \ |
|---|
| 139 | player.frags, player.ping) |
|---|
| 140 | q.rcon_update() |
|---|
| 141 | for player in q.players: |
|---|
| 142 | print '%s has an address of %s' % (player.name, player.address) |
|---|
| 143 | |
|---|