#!/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"