#!/usr/bin/env python """ Wrapper for savage2.bin dedicated server. * The wrapper launches the savage2 binary as dedicated server, establishes a connection between the binary and the wrapper. - Wrapper monitors if the server is up, may it have died the wrapper relaunches it. * The wrapper has a telnet interface as proof of concept. (default listening on port 4242) - The telnet interface will display the server console - has a input functionality to perform commands and see output. * May the wrapper be exitted the savage2 dedicated server will run as normal. * Library offers at this moment (until plugins are truely made): - (Base)ConsoleParser onRecievedAccountId (client_id , account_id) onConnect (idx , client_id, ip, port) onSetName (idx, nickname) onTeamChange (nickname, team) onMessage (channel[All,Team 1, Team 2], message) onServerStatus (map, timestamp, client_num, disconnect_num, entries, snapshots) onDisconnect (idx) onConnected (name) onPlayerReady (idx) onNewGame () * For example purposes: - TestParser(ConsoleParser) - onRecievedAccountId (client_id , account_id) * Retrieves the SF of a player from the account_id from the savage2 website. (useful for auto balance) ;D """ READ_TIMEOUT = 15 APPLICATION = "/home/david/Savage2/savage2.bin" HOST, PORT = "localhost", 4242 from sys import stdout import signal import readline, subprocess, re import threading, SocketServer from collections import deque class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler): def handle(self): print "\nConnection established by: %s\n" % self.client_address[0] Savage2ConsoleHandler.addChannel (self.onConsoleMessage) # while until user is gone. while (True): line = self.request.recv (1024); if (line == ''): break Savage2SocketHandler.put (line); Savage2SocketHandler.broadcast (); # clean up print "\nLost connection: %s" % self.client_address[0] Savage2ConsoleHandler.delChannel (self.onConsoleMessage) def onConsoleMessage (self, line): self.request.send (line + "\n") class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): pass class Savage2Thread(threading.Thread): process = None def run(self): self.launchDaemon () def launchDaemon (self): Savage2SocketHandler.addChannel (self.onSocketMessage) Savage2DaemonHandler.addChannel (self.onDaemonMessage) self.process = subprocess.Popen ([APPLICATION, "Set host_dedicatedServer true"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, universal_newlines=True) print "Launched process(%s)" % self.process.pid self.read () def read(self): # annoying colors and stuff ansisequence = re.compile(r'\x1B\[[^A-Za-z]*[A-Za-z]|\x1b\([^A-Za-z]*[A-Za-z]|\x08') while (True): # read lines line = self.process.stdout.readline (); line = ansisequence.sub ('' , line).replace ("\t", "").rstrip ("\r\n").strip ("\r") line = line.replace ('^[[?1049h^[[1;52r^[[m^[(B^[[4l^[[?7h^[[?25l^[[H^[[2J^[[41d', '') # process is dead if line == "" and self.process.poll () is not None: break if (line == ">"): continue line = line.lstrip ('>') # broadcast Savage2ConsoleHandler.put (line) Savage2ConsoleHandler.broadcast () self.clean () print "Process dead?" def clean (self): print "IOError: %s(pid: %s) stdin is closed." % (APPLICATION, self.process.pid) Savage2SocketHandler.delChannel (self.onSocketMessage) Savage2DaemonHandler.delChannel (self.onDaemonMessage) self.launchDaemon () def write(self, line): try: self.process.stdin.write (line + "\n") except IOError: self.clean () def onSocketMessage (self, line): self.write (line) def onDaemonMessage (self, line): self.write (line) class Savage2ConsoleHandler(): def __init__(self): Savage2ConsoleHandler.queue = deque() Savage2ConsoleHandler.channel = [] @staticmethod def addChannel (cb): Savage2ConsoleHandler.channel.append (cb) @staticmethod def delChannel (cb): Savage2ConsoleHandler.channel.remove (cb) @staticmethod def put (line): Savage2ConsoleHandler.queue.append (line) @staticmethod def broadcast (): line = Savage2ConsoleHandler.get () if (line is None): return; for cb in Savage2ConsoleHandler.channel: cb (line) @staticmethod def get(): try: return Savage2ConsoleHandler.queue.popleft () except: return None class Savage2SocketHandler(): def __init__(self): Savage2SocketHandler.queue = deque() Savage2SocketHandler.channel = [] @staticmethod def addChannel (cb): Savage2SocketHandler.channel.append (cb) @staticmethod def delChannel (cb): Savage2SocketHandler.channel.remove (cb) @staticmethod def put (line): Savage2SocketHandler.queue.append (line) @staticmethod def broadcast (): line = Savage2SocketHandler.get () for cb in Savage2SocketHandler.channel: cb (line) @staticmethod def get(): try: return Savage2SocketHandler.queue.popleft () except: return None class Savage2DaemonHandler(): def __init__(self): Savage2DaemonHandler.queue = deque() Savage2DaemonHandler.channel = [] @staticmethod def addChannel (cb): Savage2DaemonHandler.channel.append (cb) @staticmethod def delChannel (cb): Savage2DaemonHandler.channel.remove (cb) @staticmethod def put (line): Savage2DaemonHandler.queue.append (line) @staticmethod def broadcast (): line = Savage2DaemonHandler.get () for cb in Savage2DaemonHandler.channel: cb (line) @staticmethod def get(): try: return Savage2DaemonHandler.queue.popleft () except: return None class ConsoleParser(): filters = [] def __init__(self): self.filters = dict({ self.onRecievedAccountId : re.compile ('SGame: Recieved persistant stats for client (\d+) \(Account ID: (\d+)\)\.'), self.onConnect : re.compile ('Sv: New client connection: #(\d+), ID: (\d+), (\d+.\d+.\d+.\d+):(\d+)'), self.onSetName : re.compile ('Sv: Client #(\d+) set name to (\S+)'), self.onTeamChange : re.compile ('Sv: Client #(\d+) requested to join team: (\d)'), self.onMessage : re.compile ('Sv: \[(.*)\] (.*): (.*)'), self.onServerStatus : re.compile ('SGame: Server Status: Map\((.*)\) Timestamp\((\d+)\) Active Clients\((\d+)\) Disconnects\((\d+)\) Entities\((\d+)\) Snapshots\((\d+)\)'), self.onDisconnect : re.compile ('SGame: Removed client #(\d+)'), self.onConnected : re.compile ('Sv: (\S+) has connected.'), self.onPlayerReady : re.compile ('Sv: Client #(\d+) is ready to enter the game'), self.onNewGame : re.compile ('NewGameStarted') }) def onLineRecieved(self, line): print "Debug: %s\n" % line for handler in self.filters: filter = self.filters [handler] match = filter.match (line) if (match is not None): handler(match.groups ()) #X Sv: New client connection: #203, ID: 8625, 83.226.95.135:51427 def onConnect(self, *args, **kwargs): print "ON_CONNECT\n" print args print "\n" pass def onRecievedAccountId(self, *args, **kwargs): print "ON_RECIEVED_ACCOUNT_ID\n" print args print "\n" pass def onNewGame(self, *args, **kwargs): print "ON_NEW_GAME\n" print args print "\n" pass def onConnected(self, *args, **kwargs): print "ON_CONNECTED\n" print args print "\n" pass def onPlayerReady(self, *args, **kwargs): print "ON_PLAYER_READY\n" print args print "\n" pass #X Sv: Client #88 set name to Cicero23 #def onSetName(self , clientId, name): def onSetName(self, *args, **kwargs): print "ON_SET_NAME\n" print args print "\n" pass #X SGame: Client #180 requested to join team: IDX #def onTeamChange (self, clientId, teamIdx): def onTeamChange (self, *args, **kwargs): print "ON_TEAM_CHANGE\n" print args print "\n" pass #X Sv: [TEAM 1] BeastSlayer`: need ammo #X Sv: [TEAM 2] BeastSlayer`: need ammo #X Sv: [ALL] bLu3_eYeS: is any 1 here ? #def onMessage (self, channel, name): def onMessage (self, *args, **kwargs): print "ON_MESSAGE\n" print args print "\n" pass # SGame: Server Status: Map(ss2010_6) Timestamp(69180000) Active Clients(9) Disconnects(160) Entities(1700) Snapshots(34671) #def onServerStatus(self, map, timestamp, activeClients, disconnects, entities, snapshots): def onServerStatus(self, *args, **kwargs): print "ON_SERVER_STATUS\n" print args print "\n" pass # SGame: Removed client #195 #def onDisconnect(self, clientId, message): def onDisconnect(self, *args, **kwargs): print "ON_DISCONNECT\n" print args print "\n" pass import httplib, urllib class TestParser(ConsoleParser): def onRecievedAccountId (self, *args, **kwargs): print "ON_RECIEVED_ACCOUNT_ID\n" print args print "\n" c = httplib.HTTPConnection ("www.savage2.com") c.request ("GET", "/en/player_stats.php?id=" + args[0][1]) r = c.getresponse () html = r.read () ladder = re.search ('Ladder: (\d+)', html); lifetime = re.search ('Lifetime: (\d+)' , html); print "Connected ID: %s has playerId %s with SF(ladder): %s and SF(lifetime): %s\n" % (args[0][0], args[0][1] , ladder.group(1) , lifetime.group(1)) # Launches various threads, actual savage2 daemon and the inet server class Savage2Daemon(): parser = None server = None server_thread = None thread = None debug = "" def __init__(self): # Setup our broadcasters Savage2ConsoleHandler () Savage2SocketHandler () Savage2DaemonHandler () # Add callback's for messages Savage2ConsoleHandler.addChannel (self.onConsoleMessage) Savage2SocketHandler.addChannel (self.onSocketMessage) # Launch savage2 thread # this thread will run and handle savage2 dedicated server # relaunching it on savage2.bin exit. self.thread = Savage2Thread () self.thread.daemon = True self.thread.start () #self.parser = ConsoleParser () self.parser = TestParser () #print "\x1BE" #print "(Debug)onConsoleMessage> %s\n" % line def onConsoleMessage (self, line): self.parser.onLineRecieved (line) pass #print "(Debug)onSocketMessage> %s\n" % repr(line) def onSocketMessage (self, line): pass def disableServer(self): print "Shutting socket server down.\n" Savage2ConsoleHandler.delChannel (self.onConsoleMessage) Savage2SocketHandler.delChannel (self.onSocketMessage) self.server.shutdown () def enableServer(self): self.server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler) self.server_thread = threading.Thread(target=self.server.serve_forever) self.server_thread.setDaemon(True) self.server_thread.start () ip, port = self.server.server_address print "Started daemon: %d\n" % port # Main thread if __name__ == "__main__": # Launch and enable our daemon/wrapper. savage2Daemon = Savage2Daemon () savage2Daemon.enableServer () # reset the mouse # NEED FIX. output is bugged :/ dunno why print "\x1BE" # Catch keyboard interrupts, while we run our main while. try: while True: # block till user input line = raw_input("") # get first word words = line.split (" ") if (words [0] == "exit"): break # pass rest through the broadcaster else: Savage2DaemonHandler.put (line); Savage2DaemonHandler.broadcast (); pass except KeyboardInterrupt: pass # Clean. savage2Daemon.disableServer () print "Exit\n"