#! /usr/bin/env python "Mass game server" # Only files ending with '.mas' will be shared datapath = 'data/' # TCP and UDP port to listen (both must be reachable) port = 3223 from twisted.internet.protocol import Protocol, Factory, DatagramProtocol from twisted.internet import reactor from twisted.protocols.basic import LineReceiver import string, sys, os, time # simple logging def LogLine(tag, msg): outline = time.strftime('%c', time.localtime(time.time())) + ' ' + tag + ': ' + msg print outline sys.stdout.flush() def Bulk(msg): pass #LogLine('BULK', msg); def Info(msg): LogLine('INFO', msg); # useful for players to find out their udp address # not useful for players behind a NAT of this very server ;) class UDPEcho(DatagramProtocol): def datagramReceived(self, data, hostport): Info('UDP echo for %s:%d' % hostport) self.transport.write('%s %d' % hostport, hostport) def MakeNameSecure(name): allowed = string.ascii_letters + string.digits + '-_' s = '' for c in name: if c in allowed: s += c return s def Checksum(data): sum = 0 for c in data: if c == '\r': continue sum += ord(c) return sum % 1024 gameno = 0 class MassGame: def __init__(self, line): self.players = [] self.creationLine = line self.running = False self.finished = False self.maxPlayers = 1 global gameno gameno += 1 self.gameno = gameno Info('Game %d created' % self.gameno) def acceptPlayer(self, player): if self.running: return False if self.players: if player.version != self.players[0].version: return False if len(self.players) >= self.maxPlayers: return False player.sendLine(self.creationLine) player.id = len(self.players) player.sendLine('ID %d' % player.id) for p in self.players: p.sendLine('+ %d' % player.id) player.sendLine('+ %d' % p.id) self.players.append(player) player.ready = False return True def finish(self): if not self.finished: self.finished = True # the hard way (for now) for p in self.players: p.transport.loseConnection() Info('Game %d finished' % self.gameno) def lineReceived(self, player, line): #Bulk('GAME %d %s' % (self.gameno, line)) cmd = line.split()[0] if cmd == 'MaxPlayers': self.maxPlayers = int(line.split()[1]) elif cmd == 'UDP': id = int(line.split(' ', 2)[1]) what = line.split(' ', 2)[-1] self.players[id].sendLine('= %d %s' % (player.id, what)) elif cmd == 'START': assert not self.running player.ready = True for p in self.players: if not p.ready: return self.running = True Info('Game %d starts' % self.gameno) for p in self.players: p.sendLine('START') def lostPlayer(self, player): # TODO: at least remove game when all players disconnected pass class MassPlayer(LineReceiver): delimiter = '\n' def connectionMade(self): self.sendLine('MASS gameserver version 0.4') self.lineReceivedAction = self.greetingLineReceived self.files = {} self.game = None peer = self.transport.getPeer() self.peer = '%s:%d' % (peer.host, peer.port) Info(self.peer + ' has connected') def lineReceived(self, line): Bulk(self.peer + ' ' + line) self.lineReceivedAction(line) def greetingLineReceived(self, line): if line.startswith('INFO'): self.sendLine('INFO %d games running.' % len(self.factory.games)) self.transport.loseConnection() elif line.startswith('MASS '): self.version = line[5:] self.lineReceivedAction = self.pregameLineReceived #self.sendLine('MASS version accepted') else: self.sendLine('QUIT invalid greeting') self.transport.loseConnection() def pregameLineReceived(self, line): if line == 'JOIN': if not self.factory.games: self.sendLine('NACK no game running') return for game in self.factory.games: if game.acceptPlayer(self): self.game = game self.lineReceivedAction = self.gameLineReceived return self.sendLine('NACK only full games or games with other version running') elif line.startswith('CREATE'): self.game = MassGame(line) assert self.game.acceptPlayer(self) self.factory.games.append(self.game) self.lineReceivedAction = self.gameLineReceived else: self.sendLine('QUIT Invalid command') self.transport.loseConnection() def gameLineReceived(self, line): if line.startswith('GET ') or line.startswith('CHECKSUM '): name = MakeNameSecure(line.split()[1]) # make sure all clients use the same version data = self.files.get(name) if data is None: filename = datapath + name + '.mas' if os.path.isfile(filename): data = open(filename).read() self.files[name] = data data = data or '' if line.startswith('CHECKSUM ') and data: data = str(Checksum(data)) elif not data: Info('Requested file %s.mas not found' % name) else: Info('Sending file %s.mas (%d bytes)' % (name, len(data))) self.sendLine('DATA %d' % len(data)) self.transport.write(data) else: self.game.lineReceived(self, line) def connectionLost(self, reason=None): Info(self.peer + ' disconnected') if self.game: # for nicer client exit messages reactor.callLater(5.0, self.cleanup) def cleanup(self): self.game.finish() if self.game in self.factory.games: self.factory.games.remove(self.game) self.game = None def main(): factory = Factory() factory.protocol = MassPlayer factory.games = [] factory.playersWaiting = [] reactor.listenTCP(port, factory) reactor.listenUDP(port, UDPEcho()) Info("Listening on UDP and TCP port %d" % port) reactor.run() if __name__ == '__main__': main()