diff options
author | Diego Roversi <diegor@tiscali.it> | 2019-09-08 18:12:27 +0200 |
---|---|---|
committer | Diego Roversi <diegor@tiscali.it> | 2019-09-08 18:12:27 +0200 |
commit | 1d9925c287b318ec21343e2682b51ab6a36ae8db (patch) | |
tree | 17d1c0ac21eea6f291146520afa8381db4586fb4 /common |
initial commit from cvs 1.6.2
Diffstat (limited to 'common')
-rw-r--r-- | common/.cvsignore | 1 | ||||
-rw-r--r-- | common/__init__.py | 1 | ||||
-rw-r--r-- | common/gamesrv.py | 1378 | ||||
-rw-r--r-- | common/hostchooser.py | 192 | ||||
-rw-r--r-- | common/httpserver.py | 192 | ||||
-rw-r--r-- | common/javaserver.py | 157 | ||||
-rw-r--r-- | common/msgstruct.py | 75 | ||||
-rw-r--r-- | common/pixmap.py | 163 | ||||
-rw-r--r-- | common/stdlog.py | 106 | ||||
-rw-r--r-- | common/udpovertcp.py | 46 |
10 files changed, 2311 insertions, 0 deletions
diff --git a/common/.cvsignore b/common/.cvsignore new file mode 100644 index 0000000..539da74 --- /dev/null +++ b/common/.cvsignore @@ -0,0 +1 @@ +*.py[co] diff --git a/common/__init__.py b/common/__init__.py new file mode 100644 index 0000000..ea30561 --- /dev/null +++ b/common/__init__.py @@ -0,0 +1 @@ +#empty diff --git a/common/gamesrv.py b/common/gamesrv.py new file mode 100644 index 0000000..bb27d66 --- /dev/null +++ b/common/gamesrv.py @@ -0,0 +1,1378 @@ +from __future__ import generators +from socket import * +from select import select +from struct import pack, unpack +import zlib, os, random, struct, md5, sys +from time import time, ctime +from msgstruct import * +from errno import EWOULDBLOCK + + +SERVER_TIMEOUT = 7200 # 2 hours without any connection or port activity + + +def protofilepath(filename): + dirpath = filename + path = [] + while dirpath: + dirpath, component = os.path.split(dirpath) + assert component, "invalid file path %r" % (filename,) + path.insert(0, component) + path.insert(0, game.FnBasePath) + return '/'.join(path) + + +class Icon: + count = 0 + + def __init__(self, bitmap, code, x,y,w,h, alpha=255): + self.w = w + self.h = h + self.origin = (bitmap, x, y) + self.code = code + if alpha == 255: + self.msgdef = message(MSG_DEF_ICON, bitmap.code, code, x,y,w,h) + else: + self.msgdef = message(MSG_DEF_ICON, bitmap.code, code, x,y,w,h, alpha) + framemsgappend(self.msgdef) + + def getimage(self): + import pixmap + bitmap, x, y = self.origin + image = pixmap.decodepixmap(bitmap.read()) + return pixmap.cropimage(image, (x, y, self.w, self.h)) + + def getorigin(self): + bitmap, x, y = self.origin + return bitmap, (x, y, self.w, self.h) + + +class DataChunk: + + def __init__(self): + for c in clients: + if c.initialized == 2: + self.defall(c) + if recording and game: + self.defall(recording) + + def read(self, slice=None): + f = open(self.filename, "rb") + data = f.read() + f.close() + if slice: + start, length = slice + data = data[start:start+length] + return data + + def defall(self, client): + if client.proto == 1 or not self.filename: + # protocol 1 + try: + msgdef = self.msgdef + except AttributeError: + data = zlib.compress(self.read()) + msgdef = self.msgdef = self.getmsgdef(data) + else: + # protocol >= 2 + try: + msgdef = self.sendmsgdef + except AttributeError: + fileid = len(filereaders) + filereaders[fileid] = self.read + data = self.read() + msgdef = self.sendmsgdef = (self.getmd5def(fileid, data) + + self.getmsgdef(fileid)) + client.msgl.append(msgdef) + + def getmd5def(self, fileid, data, offset=0): + checksum = md5.new(data).digest() + return message(MSG_MD5_FILE, fileid, protofilepath(self.filename), + offset, len(data), checksum) + + +class Bitmap(DataChunk): + + def __init__(self, code, filename, colorkey=None): + self.code = code + self.filename = filename + self.icons = {} + self.colorkey = colorkey + DataChunk.__init__(self) + + def geticon(self, x,y,w,h, alpha=255): + rect = (x,y,w,h) + try: + return self.icons[rect] + except: + ico = Icon(self, Icon.count, x,y,w,h, alpha) + Icon.count += 1 + self.icons[rect] = ico + return ico + + def geticonlist(self, w, h, count): + return map(lambda i, fn=self.geticon, w=w, h=h: fn(i*w, 0, w, h), range(count)) + + def getmsgdef(self, data): + if self.colorkey is not None: + return message(MSG_DEF_BITMAP, self.code, data, self.colorkey) + else: + return message(MSG_DEF_BITMAP, self.code, data) + + def defall(self, client): + DataChunk.defall(self, client) + for i in self.icons.values(): + client.msgl.append(i.msgdef) + + +class MemoryBitmap(Bitmap): + + def __init__(self, code, data, colorkey=None): + self.data = data + Bitmap.__init__(self, code, None, colorkey) + + def read(self, slice=None): + data = self.data + if slice: + start, length = slice + data = data[start:start+length] + return data + + +class Sample(DataChunk): + + def __init__(self, code, filename, freqfactor=1): + self.code = code + self.filename = filename + self.freqfactor = freqfactor + DataChunk.__init__(self) + + def defall(self, client): + if client.has_sound > 0: + DataChunk.defall(self, client) + + def getmsgdef(self, data): + return message(MSG_DEF_SAMPLE, self.code, data) + + def read(self, slice=None): + f = open(self.filename, "rb") + data = f.read() + f.close() + if self.freqfactor != 1: + freq, = unpack("<i", data[24:28]) + freq = int(freq * self.freqfactor) + data = data[:24] + pack("<i", freq) + data[28:] + if slice: + start, length = slice + data = data[start:start+length] + return data + + def getmd5def(self, fileid, data): + if self.freqfactor == 1: + return DataChunk.getmd5def(self, fileid, data) + else: + datahead = data[:28] + datatail = data[28:] + return (message(MSG_PATCH_FILE, fileid, 0, datahead) + + DataChunk.getmd5def(self, fileid, datatail, offset=28)) + + def play(self, lvolume=1.0, rvolume=None, pad=0.5, singleclient=None): + if rvolume is None: + rvolume = lvolume + lvolume *= 2.0*(1.0-pad) + rvolume *= 2.0*pad + if lvolume < 0.0: + lvolume = 0.0 + elif lvolume > 1.0: + lvolume = 1.0 + if rvolume < 0.0: + rvolume = 0.0 + elif rvolume > 1.0: + rvolume = 1.0 + message = pack("!hBBh", self.code, int(lvolume*255.0), + int(rvolume*255.0), -1) + if singleclient is None: + clist = clients[:] + else: + clist = [singleclient] + for c in clist: + if c.has_sound: + c.sounds.setdefault(message, 4) + + +class Music(DataChunk): + + def __init__(self, filename, filerate=44100): + self.filename = filename + self.filerate = filerate + self.f = open(filename, 'rb') + self.f.seek(0, 2) + filesize = self.f.tell() + self.endpos = max(self.filerate, filesize - self.filerate) + self.fileid = len(filereaders) + filereaders[self.fileid] = self.read + self.md5msgs = {} + DataChunk.__init__(self) + + def read(self, (start, length)): + self.f.seek(start) + return self.f.read(length) + + def msgblock(self, position, limited=1): + blocksize = self.filerate + if limited and position+blocksize > self.endpos: + blocksize = self.endpos-position + if blocksize <= 0: + return '' + #self.f.seek(position) + #return message(MSG_DEF_MUSIC, self.code, position, self.f.read(blocksize)) + try: + msg = self.md5msgs[position] + except KeyError: + data = self.read((position, blocksize)) + checksum = md5.new(data).digest() + msg = message(MSG_MD5_FILE, self.fileid, protofilepath(self.filename), + position, blocksize, checksum) + self.md5msgs[position] = msg + return msg + + def clientsend(self, clientpos): + msg = self.msgblock(clientpos) + #print 'clientsend:', self.code, len(msg), clientpos + if msg: + return [msg], clientpos + self.filerate + else: + return [], None + + def initialsend(self, c): + return [self.msgblock(0), self.msgblock(self.endpos, 0)], self.filerate + + def defall(self, client): + pass + + +def clearsprites(): + sprites_by_n.clear() + sprites[:] = [''] + +def compactsprites(insert_new=None, insert_before=None): + global sprites, sprites_by_n + if insert_before is not None: + if insert_new.alive: + insert_before = insert_before.alive + else: + insert_before = None + newsprites = [''] + newd = {} + l = sprites_by_n.items() + l.sort() + for n, s in l: + if n == insert_before: + prevn = insert_new.alive + newn = insert_new.alive = len(newsprites) + newsprites.append(sprites[prevn]) + newd[newn] = insert_new + l.remove((prevn, insert_new)) + newn = s.alive = len(newsprites) + newsprites.append(sprites[n]) + newd[newn] = s + sprites = newsprites + sprites_by_n = newd + + +class Sprite: + +## try: +## import psyco.classes +## except ImportError: +## pass +## else: +## __slots__ = ['x', 'y', 'ico', 'alive'] +## __metaclass__ = psyco.classes.psymetaclass + + def __init__(self, ico, x,y): + self.x = x + self.y = y + self.ico = ico + self.alive = len(sprites) + if (-ico.w < x < game.width and + -ico.h < y < game.height): + sprites.append(pack("!hhh", x, y, ico.code)) + else: + sprites.append('') # starts off-screen + sprites_by_n[self.alive] = self + + def move(self, x,y, ico=None): + self.x = x + self.y = y + if ico is not None: + self.ico = ico + sprites[self.alive] = pack("!hhh", x, y, self.ico.code) + + def setdisplaypos(self, x, y): + # special use only (self.x,y are not updated) + s = sprites[self.alive] + if len(s) == 6: + sprites[self.alive] = pack("!hh", x, y) + s[4:] + + def setdisplayicon(self, ico): + # special use only (self.ico is not updated) + s = sprites[self.alive] + if len(s) == 6: + sprites[self.alive] = s[:4] + pack("!h", ico.code) + + #sizeof_displaypos = struct.calcsize("!hh") + def getdisplaypos(self): + # special use only (normally, read self.x,y,ico directly) + s = sprites[self.alive] + if self.alive and len(s) == 6: + return unpack("!hh", s[:4]) + else: + return None, None + + def step(self, dx,dy): + x = self.x = self.x + dx + y = self.y = self.y + dy + sprites[self.alive] = pack("!hhh", x, y, self.ico.code) + + def seticon(self, ico): + self.ico = ico + sprites[self.alive] = pack("!hhh", self.x, self.y, ico.code) + + def hide(self): + sprites[self.alive] = '' + + def kill(self): + if self.alive: + del sprites_by_n[self.alive] + sprites[self.alive] = '' + self.alive = 0 + + def prefix(self, n, m=0): + pass #sprites[self.alive] = pack("!hhh", n, m, 32767) + sprites[self.alive] + + def to_front(self): + if self.alive and self.alive < len(sprites)-1: + self._force_to_front() + + def _force_to_front(self): + info = sprites[self.alive] + sprites[self.alive] = '' + del sprites_by_n[self.alive] + self.alive = len(sprites) + sprites_by_n[self.alive] = self + sprites.append(info) + + def to_back(self, limit=None): + assert self is not limit + if limit: + n1 = limit.alive + 1 + else: + n1 = 1 + if self.alive > n1: + if n1 in sprites_by_n: + keys = sprites_by_n.keys() + keys.remove(self.alive) + keys.sort() + keys = keys[keys.index(n1):] + reinsert = [sprites_by_n[n] for n in keys] + for s1 in reinsert: + s1._force_to_front() + assert n1 not in sprites_by_n + info = sprites[self.alive] + sprites[self.alive] = '' + del sprites_by_n[self.alive] + self.alive = n1 + sprites_by_n[n1] = self + sprites[n1] = info + + def __repr__(self): + if self.alive: + return "<sprite %d at %d,%d>" % (self.alive, self.x, self.y) + else: + return "<killed sprite>" + + +class Player: + standardplayericon = None + + def playerjoin(self): + pass + + def playerleaves(self): + pass + + def _playerleaves(self): + if self.isplaying(): + self._client.killplayer(self) + del self._client + self.playerleaves() + + def isplaying(self): + return hasattr(self, "_client") + + +class Client: + SEND_BOUND_PER_FRAME = 0x6000 # bytes + KEEP_ALIVE = 2.2 # seconds + + def __init__(self, socket, addr): + socket.setblocking(0) + self.socket = socket + self.addr = addr + self.udpsocket = None + self.udpsockcounter = 0 + self.initialdata = MSG_WELCOME + self.initialized = 0 + self.msgl = [message(MSG_PING)] + self.buf = "" + self.players = { } + self.sounds = None + self.has_sound = 0 + self.has_music = 0 + self.musicpos = { } + self.proto = 1 + self.dyncompress = None + addsocket('CLIENT', self.socket, self.input_handler) + clients.append(self) + self.log('connected') + self.send_buffer(self.initialdata) + + def opengame(self, game): + if self.initialized == 0: + self.initialdata += game.FnDesc + '\n' + self.initialized = 1 + if self.initialized == 1: + if game.broadcast_port: + self.initialdata += message(MSG_BROADCAST_PORT, game.broadcast_port) + game.trigger_broadcast() + self.initialdata += game.deffieldmsg() + else: + self.msgl.append(game.deffieldmsg()) + self.activity = self.last_ping = time() + self.force_ping_delay = 0.6 + for c in clients: + for id in c.players.keys(): + self.msgl.append(message(MSG_PLAYER_JOIN, id, c is self)) + + def emit(self, udpdata, broadcast_extras): + if self.initialdata: + self.send_buffer(self.initialdata) + elif self.initialized == 2: + buffer = ''.join(self.msgl) + if buffer: + self.send_buffer(buffer) + if self.udpsocket is not None: + if self.sounds: + if broadcast_extras is None or self not in broadcast_clients: + udpdata = ''.join(self.sounds.keys() + [udpdata]) + else: + broadcast_extras.update(self.sounds) + for key, value in self.sounds.items(): + if value: + self.sounds[key] = value-1 + else: + del self.sounds[key] + if broadcast_extras is None or self not in broadcast_clients: + if self.dyncompress is not None: + udpdatas = self.dynamic_compress(udpdata) + else: + udpdatas = [udpdata] + for udpdata in udpdatas: + try: + self.udpsockcounter += self.udpsocket.send(udpdata) + except error, e: + print >> sys.stderr, 'ignored:', str(e) + pass # ignore UDP send errors (buffer full, etc.) + if self.has_music > 1 and NOW >= self.musicstreamer: + self.musicstreamer += 0.99 + self.sendmusicdata() + if not self.msgl: + if abs(NOW - self.activity) <= self.KEEP_ALIVE: + if abs(NOW - self.last_ping) <= self.force_ping_delay: + return + if self.udpsockcounter < 1024: + return + self.force_ping_delay += 0.2 + self.msgl.append(message(MSG_PING, self.udpsockcounter>>10)) + self.last_ping = NOW + + def setup_dyncompress(self): + def dyncompress(): + # See comments in pclient.Playfield.dynamic_decompress(). + threads = [] + for t in range(3): + co = zlib.compressobj(6) + threads.append((chr(0x88 + t) + chr(t), co)) + frame = 0 + globalsync = 0 + + while 1: + # write three normal packets, one on each thread + for t in range(3): + head, co = threads.pop(0) + yield head + chr(frame), co + threads.append((chr(ord(head[0]) & 0x87) + chr(frame), co)) + yield None, None + frame = (frame + 1) & 0xFF + + # sync frame, write two packets (on two threads) + # and restart compression at the current frame for these threads + head, co = threads.pop(0) + yield head + chr(frame), co + co1 = zlib.compressobj(6) + co2 = zlib.compressobj(6) + globalsync += 1 + if globalsync == 4: + # next on this thread will be a global sync packet + nextframe = (frame + 2) & 0xFF + globalsync = 0 + else: + # next of this thread will be a local sync packet + yield None, co1 + nextframe = frame + threads.append((chr(ord(head[0]) | 8) + chr(nextframe), co1)) + + # 2nd packet of the current frame + head, co = threads.pop(0) + yield head + chr(frame), co + yield None, co2 + threads.append((chr(ord(head[0]) | 8) + chr(frame), co2)) + + yield None, None + frame = (frame + 1) & 0xFF + + self.dyncompress = dyncompress() + + def dynamic_compress(self, framedata): + result = [] + for head, co in self.dyncompress: + if not co: + return result + data = [head, co.compress(framedata), co.flush(zlib.Z_SYNC_FLUSH)] + if head: + result.append(''.join(data)) + + def send_can_mix(self): + return not self.msgl and self.socket is not None + + def send_buffer(self, buffer): + try: + count = self.socket.send(buffer[:self.SEND_BOUND_PER_FRAME]) + except error, e: + if e.args[0] != EWOULDBLOCK: + self.msgl = [] + self.initialdata = "" + self.disconnect(e, 'emit') + return + else: + #g = open('log', 'ab'); g.write(buffer[:count]); g.close() + buffer = buffer[count:] + self.activity = NOW + if self.initialdata: + self.initialdata = buffer + elif buffer: + self.msgl = [buffer] + else: + self.msgl = [] + + def receive(self, data): + #print "receive:", `data` + try: + data = self.buf + data + while data: + values, data = decodemessage(data) + if not values: + break # incomplete message + fn = self.MESSAGES.get(values[0]) + if fn: + fn(self, *values[1:]) + else: + print "unknown message from", self.addr, ":", values + self.buf = data + except struct.error: + import traceback + traceback.print_exc() + self.socket.send('\n\n<h1>Protocol Error</h1>\n') + hs = findsocket('HTTP') + if hs is not None: + url = 'http://%s:%s' % (HOSTNAME, displaysockport(hs)) + self.socket.send(''' +If you meant to point your web browser to this server, +then use the following address: + +<a href="%s">%s</a> +''' % (url, url)) + self.disconnect('protocol error', 'receive') + + def input_handler(self): + try: + data = self.socket.recv(2048) + except error, e: + self.disconnect(e, "socket.recv") + else: + if data: + self.activity = NOW + self.receive(data) + elif not hasattr(self.socket, 'RECV_CAN_RETURN_EMPTY'): + # safecheck that this means disconnected + iwtd, owtd, ewtd = select([self.socket], [], [], 0.0) + if self.socket in iwtd: + self.disconnect('end of data', 'socket.recv') + + def disconnect(self, err=None, infn=None): + removesocket('CLIENT', self.socket) + if err: + extra = ": " + str(err) + else: + extra = "" + if infn: + extra += " in " + infn + print 'Disconnected by', self.addr, extra + self.log('disconnected' + extra) + for p in self.players.values(): + p._playerleaves() + try: + del broadcast_clients[self] + except KeyError: + pass + clients.remove(self) + try: + self.socket.close() + except: + pass + self.socket = None + if not clients and game is not None: + game.FnDisconnected() + + def killplayer(self, player): + for id, p in self.players.items(): + if p is player: + framemsgappend(message(MSG_PLAYER_KILL, id)) + del self.players[id] + if game: + game.updateplayers() + + def joinplayer(self, id, *rest): + if self.players.has_key(id): + print "Note: player %s is already playing" % (self.addr+(id,),) + return + if game is None: + return # refusing new player before the game starts + p = game.FnPlayers()[id] + if p is None: + print "Too many players. New player %s refused." % (self.addr+(id,),) + self.msgl.append(message(MSG_PLAYER_KILL, id)) + elif p.isplaying(): + print "Note: player %s is already played by another client" % (self.addr+(id,),) + else: + print "New player %s" % (self.addr+(id,),) + p._client = self + p.playerjoin() + p.setplayername('') + self.players[id] = p + game.updateplayers() + for c in clients: + c.msgl.append(message(MSG_PLAYER_JOIN, id, c is self)) + + def remove_player(self, id, *rest): + try: + p = self.players[id] + except KeyError: + print "Note: player %s is not playing" % (self.addr+(id,),) + else: + p._playerleaves() + + def set_player_name(self, id, name, *rest): + p = game.FnPlayers()[id] + p.setplayername(name) + + def set_udp_port(self, port, addr=None, *rest): + if port == MSG_BROADCAST_PORT: + self.log('set_udp_port: broadcast') + broadcast_clients[self] = 1 + #print "++++ Broadcasting ++++ to", self.addr + else: + try: + del broadcast_clients[self] + except KeyError: + pass + if port == MSG_INLINE_FRAME or port == 0: + # client requests data in-line on the TCP stream + self.dyncompress = None + import udpovertcp + self.udpsocket = udpovertcp.SocketMarshaller(self.socket, self) + s = self.udpsocket.tcpsock + self.log('set_udp_port: udp-over-tcp') + else: + try: + if hasattr(self.socket, 'udp_over_udp_mixer'): + # for SocketOverUdp + self.udpsocket = self.socket.udp_over_udp_mixer() + else: + self.udpsocket = socket(AF_INET, SOCK_DGRAM) + self.udpsocket.setblocking(0) + addr = addr or self.addr[0] + self.udpsocket.connect((addr, port)) + except error, e: + print >> sys.stderr, "Cannot set UDP socket to", addr, str(e) + self.udpsocket = None + self.udpsockcounter = sys.maxint + else: + if self.proto >= 3: + self.setup_dyncompress() + s = self.udpsocket + self.log('set_udp_port: %s:%d' % (addr, port)) + if s: + try: + s.setsockopt(SOL_IP, IP_TOS, 0x10) # IPTOS_LOWDELAY + except error, e: + print >> sys.stderr, "Cannot set IPTOS_LOWDELAY:", str(e) + + def enable_sound(self, sound_mode=1, *rest): + if sound_mode != self.has_sound: + self.sounds = {} + self.has_sound = sound_mode + if self.has_sound > 0: + for snd in samples.values(): + snd.defall(self) + #self.log('enable_sound %s' % sound_mode) + + def enable_music(self, mode, *rest): + if mode != self.has_music: + self.has_music = mode + self.startmusic() + #self.log('enable_music') + + def startmusic(self): + if self.has_music: + self.musicstreamer = time() + for cde in currentmusics[1:]: + if cde not in self.musicpos: + msgl, self.musicpos[cde] = music_by_id[cde].initialsend(self) + self.msgl += msgl + if self.has_music > 1: + self.sendmusicdata() + self.msgl.append(message(MSG_PLAY_MUSIC, *currentmusics)) + + def sendmusicdata(self): + for cde in currentmusics[1:]: + if self.musicpos[cde] is not None: + msgl, self.musicpos[cde] = music_by_id[cde].clientsend(self.musicpos[cde]) + self.msgl += msgl + return + + def ping(self, *rest): + if self.initialized < 2: + # send all current bitmap data + self.initialized = 2 + for b in bitmaps.values(): + b.defall(self) + self.finishinit(game) + for id, p in game.FnPlayers().items(): + if p.standardplayericon is not None: + self.msgl.append(message(MSG_PLAYER_ICON, id, p.standardplayericon.code)) + self.msgl.append(message(MSG_PONG, *rest)) + + def finishinit(self, game): + pass + + def pong(self, *rest): + pass + + def log(self, message): + print self.addr, message + + def protocol_version(self, version, *rest): + self.proto = version + + def md5_data_request(self, fileid, position, size, *rest): + data = filereaders[fileid]((position, size)) + data = zlib.compress(data) + self.msgl.append(message(MSG_ZPATCH_FILE, fileid, position, data)) + +## def def_file(self, filename, md5sum): +## fnp = [] +## while filename: +## filename, tail = os.path.split(filename) +## fnp.insert(0, tail) +## if fnp[:len(FnBasePath)] == FnBasePath: +## filename = os.path.join(*fnp[len(FnBasePath):]) +## self.known_files[filename] = md5sum + + MESSAGES = { + CMSG_PROTO_VERSION: protocol_version, + CMSG_ADD_PLAYER : joinplayer, + CMSG_REMOVE_PLAYER: remove_player, + CMSG_UDP_PORT : set_udp_port, + CMSG_ENABLE_SOUND : enable_sound, + CMSG_ENABLE_MUSIC : enable_music, + CMSG_PING : ping, + CMSG_PONG : pong, + CMSG_DATA_REQUEST : md5_data_request, + CMSG_PLAYER_NAME : set_player_name, +## CMSG_DEF_FILE : def_file, + } + + +class SimpleClient(Client): + + def finishinit(self, game): + num = 0 + for keyname, icolist, fn in game.FnKeys: + self.msgl.append(message(MSG_DEF_KEY, keyname, num, + *[ico.code for ico in icolist])) + num += 1 + + def cmsg_key(self, pid, keynum): + if game is not None: + try: + player = self.players[pid] + fn = game.FnKeys[keynum][2] + except (KeyError, IndexError): + game.FnUnknown() + else: + getattr(player, fn) () + + MESSAGES = Client.MESSAGES.copy() + MESSAGES.update({ + CMSG_KEY: cmsg_key, + }) + + +MAX_CLIENTS = 32 + +clients = [] +FnClient = SimpleClient +broadcast_clients = {} +filereaders = {} +bitmaps = {} +samples = {} +music_by_id = {} +currentmusics = [0] +sprites = [''] +sprites_by_n = {} +recording = None +game = None +serversockets = {} +socketsbyrole = {} +socketports = {} + +def framemsgappend(msg): + for c in clients: + c.msgl.append(msg) + if recording: + recording.write(msg) + +##def sndframemsgappend(msg): +## for c in clients: +## if c.has_sound: +## c.msgl.append(msg) + +def set_udp_port(port): + hostchooser.UDP_PORT = port + +def has_loop_music(): + return currentmusics[0] < len(currentmusics)-1 + +def finalsegment(music1, music2): + intro1 = music1[1:1+music1[0]] + intro2 = music2[1:1+music2[0]] + loop1 = music1[1+music1[0]:] + loop2 = music2[1+music2[0]:] + return loop1 == loop2 and intro1 == intro2[len(intro2)-len(intro1):] + +def set_musics(musics_intro, musics_loop, reset=1): + mlist = [] + loop_from = len(musics_intro) + mlist.append(loop_from) + for m in musics_intro + musics_loop: + mlist.append(m.fileid) + reset = reset or not finalsegment(mlist, currentmusics) + currentmusics[:] = mlist + if reset: + for c in clients: + c.startmusic() + +def fadeout(time=1.0): + msg = message(MSG_FADEOUT, int(time*1000)) + for c in clients: + if c.has_music > 1: + c.msgl.append(msg) + currentmusics[:] = [0] + + +def getbitmap(filename, colorkey=None): + try: + return bitmaps[filename] + except: + bmp = Bitmap(len(bitmaps), filename, colorkey) + bitmaps[filename] = bmp + return bmp + +def getsample(filename, freqfactor=1): + try: + return samples[filename, freqfactor] + except: + snd = Sample(len(samples), filename, freqfactor) + samples[filename, freqfactor] = snd + return snd + +def getmusic(filename, filerate=44100): + try: + return samples[filename] + except: + mus = Music(filename, filerate) + samples[filename] = mus + music_by_id[mus.fileid] = mus + return mus + +def newbitmap(data, colorkey=None): + bmp = MemoryBitmap(len(bitmaps), data, colorkey) + bitmaps[bmp] = bmp + return bmp + + +def addsocket(role, socket, handler=None, port=None): + if port is None: + host, port = socket.getsockname() + if handler is not None: + serversockets[socket] = handler + socketsbyrole.setdefault(role, []).append(socket) + socketports[socket] = port + +def findsockets(role): + return socketsbyrole.get(role, []) + +def findsocket(role): + l = findsockets(role) + if l: + return l[-1] + else: + return None + +def removesocket(role, socket=None): + if socket is None: + for socket in socketsbyrole.get(role, [])[:]: + removesocket(role, socket) + return + try: + del serversockets[socket] + except KeyError: + pass + try: + socketsbyrole.get(role, []).remove(socket) + except ValueError: + pass + try: + del socketports[socket] + except KeyError: + pass + +def opentcpsocket(port=None): + port = port or PORTS.get('LISTEN', INADDR_ANY) + s = findsocket('LISTEN') + if s is None: + s = socket(AF_INET, SOCK_STREAM) + try: + s.bind(('', port)) + s.listen(1) + except error: + if port == INADDR_ANY: + for i in range(10): + port = random.choice(xrange(8000, 12000)) + try: + s.bind(('', port)) + s.listen(1) + except error: + pass + else: + break + else: + raise error, "server cannot find a free TCP socket port" + else: + raise + + def tcpsocket_handler(s=s): + conn, addr = s.accept() + game.newclient(conn, addr) + + addsocket('LISTEN', s, tcpsocket_handler) + return s + +def openpingsocket(only_port=None): + only_port = only_port or PORTS.get('PING', None) + s = findsocket('PING') + if s is None: + import hostchooser + s = hostchooser.serverside_ping(only_port) + if s is None: + return None + def pingsocket_handler(s=s): + global game + import hostchooser + if game is not None: + args = game.FnDesc, ('', game.address[1]), game.FnExtraDesc() + else: + ts = findsocket('LISTEN') + if ts: + address = '', displaysockport(ts) + else: + address = '', '' + args = 'Not playing', address, '' + hs = findsocket('HTTP') + args = args + (displaysockport(hs),) + hostchooser.answer_ping(s, *args) + addsocket('PING', s, pingsocket_handler) + return s + +def openhttpsocket(ServerClass=None, HandlerClass=None, + port=None): + port = port or PORTS.get('HTTP', None) + s = findsocket('HTTP') + if s is None: + if ServerClass is None: + from BaseHTTPServer import HTTPServer as ServerClass + if HandlerClass is None: + import javaserver + from httpserver import MiniHandler as HandlerClass + server_address = ('', port or 8000) + try: + httpd = ServerClass(server_address, HandlerClass) + except error: + if port is None: + server_address = ('', INADDR_ANY) + try: + httpd = ServerClass(server_address, HandlerClass) + except error, e: + print >> sys.stderr, "cannot start HTTP server", str(e) + return None + else: + raise + s = httpd.socket + addsocket('HTTP', s, httpd.handle_request) + return s + +BROADCAST_PORT_RANGE = xrange(18000, 19000) +#BROADCAST_MESSAGE comes from msgstruct +BROADCAST_DELAY = 0.6180 +BROADCAST_DELAY_INCR = 2.7183 + +def openbroadcastsocket(broadcastport=None): + s = findsocket('BROADCAST') + if s is None: + try: + s = socket(AF_INET, SOCK_DGRAM) + s.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + except error, e: + print >> sys.stderr, "Cannot broadcast", str(e) + return None + port = broadcastport or random.choice(BROADCAST_PORT_RANGE) + addsocket('BROADCAST', s, port=port) + return s + +def displaysockport(s): + return socketports.get(s, 'off') + + +class Game: + width = 640 + height = 480 + backcolor = 0x000000 + + FnDesc = "NoName" + FnFrame = lambda self: 1.0 + FnExcHandler=lambda self, k: 0 + FnServerInfo=lambda self, s: None + FnPlayers = lambda self: {} + FnKeys = [] + FnUnknown = lambda self: None + FnDisconnected = lambda self: None + + def __init__(self): + global game + s = opentcpsocket() + self.address = HOSTNAME, socketports[s] + bs = self.broadcast_s = openbroadcastsocket() + self.broadcast_port = socketports.get(bs) + self.broadcast_next = None + self.nextframe = time() + clearsprites() + game = self + if recording: + for b in bitmaps.values(): + b.defall(recording) + self.pendingclients = [] + + def openserver(self): + ps = openpingsocket() + print '%s server at %s:%d, Broadcast %d, UDP %d' % ( + self.FnDesc, self.address[0], self.address[1], + displaysockport(self.broadcast_s), displaysockport(ps)) + + hs = openhttpsocket() + if hs: + print 'HTTP server: http://%s:%d' % ( + self.address[0], displaysockport(hs)) + + try: + from localmsg import autonotify + except ImportError: + pass + else: + autonotify(self.FnDesc, *self.address) + + if clients: + for c in clients: + c.opengame(self) + if recording: + recording.start() + recording.write(self.deffieldmsg()) + + def trigger_broadcast(self): + assert self.broadcast_s is not None + game.broadcast_delay = BROADCAST_DELAY + game.broadcast_next = time() + self.broadcast_delay + + def deffieldmsg(self): + return message(MSG_DEF_PLAYFIELD, + self.width, self.height, self.backcolor, + self.FnDesc) + + def socketerrors(self, ewtd): + for c in clients[:]: + if c.socket in ewtd: + del ewtd[c.socket] + c.disconnect("error", "select") + + def mainstep(self): + global NOW + if self.pendingclients: + self.newclient(*self.pendingclients.pop()) + NOW = time() + delay = self.nextframe - NOW + if delay<=0.0: + self.nextframe += self.FnFrame() + self.sendudpdata() + NOW = time() + delay = self.nextframe - NOW + if delay<0.0: + self.nextframe = NOW + delay = 0.0 + if self.broadcast_next is not None and NOW >= self.broadcast_next: + if not clients: + self.broadcast_next = None + else: + try: + self.broadcast_s.sendto(BROADCAST_MESSAGE, + ('<broadcast>', self.broadcast_port)) + #print "Broadcast ping" + except error: + pass # ignore failed broadcasts + self.broadcast_next = time() + self.broadcast_delay + self.broadcast_delay *= BROADCAST_DELAY_INCR + return delay + + def sendudpdata(self): + sprites[0] = '' + udpdata = ''.join(sprites) + if len(broadcast_clients) >= 2: + broadcast_extras = {} + else: + broadcast_extras = None + for c in clients[:]: + c.emit(udpdata, broadcast_extras) + if recording: + recording.udpdata(udpdata) + if broadcast_extras is not None: + udpdata = ''.join(broadcast_extras.keys() + [udpdata]) + try: + self.broadcast_s.sendto(udpdata, + ('<broadcast>', self.broadcast_port)) + #print "Broadcast UDP data" + except error: + pass # ignore failed broadcasts + + def FnExtraDesc(self): + players = 0 + for c in clients: + players += len(c.players) + if players == 0: + return 'no player' + elif players == 1: + return 'one player' + else: + return '%d players' % players + + def updateplayers(self): + pass + + def updateboard(self): + pass + + def newclient(self, conn, addr): + if len(clients)==MAX_CLIENTS: + print "Too many connections; refusing new connection from", addr + conn.close() + else: + try: + addrname = (gethostbyaddr(addr[0])[0],) + addr[1:] + except: + addrname = addr + print 'Connected by', addrname + try: + c = FnClient(conn, addrname) + except error, e: + print 'Connexion already lost!', e + else: + if game is not None: + c.opengame(game) + + def newclient_threadsafe(self, conn, addr): + self.pendingclients.insert(0, (conn, addr)) + + +def recursiveloop(endtime, extra_sockets): + global game + timediff = 1 + while timediff: + if game is not None: + delay = game.mainstep() + else: + delay = 5.0 + iwtd = extra_sockets + serversockets.keys() + timediff = max(0.0, endtime - time()) + iwtd, owtd, ewtd = select(iwtd, [], iwtd, min(delay, timediff)) + if ewtd: + if game: + game.socketerrors(ewtd) + if ewtd: + print >> sys.stderr, "Unexpected socket error reported" + for s in iwtd: + if s in serversockets: + serversockets[s]() # call handler + elif s in extra_sockets: + return s + if not extra_sockets and timediff: + return 1 + return None + +SERVER_SHUTDOWN = 0.0 + +def mainloop(): + global game, SERVER_SHUTDOWN + servertimeout = None + try: + while serversockets: + try: + if game is not None: + delay = game.mainstep() + else: + delay = SERVER_SHUTDOWN or 5.0 + iwtd = serversockets.keys() + try: + iwtd, owtd, ewtd = select(iwtd, [], iwtd, delay) + except Exception, e: + from select import error as select_error + if not isinstance(e, select_error): + raise + iwtd, owtd, ewtd = [], [], [] + if ewtd: + if game: + game.socketerrors(ewtd) + if ewtd: + print >> sys.stderr, "Unexpected socket error reported" + servertimeout = None + if iwtd: + for s in iwtd: + if s in serversockets: + serversockets[s]() # call handler + servertimeout = None + elif SERVER_SHUTDOWN and not ewtd and not owtd: + SERVER_SHUTDOWN -= delay + if SERVER_SHUTDOWN <= 0.001: + raise SystemExit, "Server shutdown requested." + elif clients or getattr(game, 'autoreset', 0): + servertimeout = None + elif servertimeout is None: + servertimeout = time() + SERVER_TIMEOUT + elif time() > servertimeout: + raise SystemExit, "No more server activity, timing out." + except KeyboardInterrupt: + if game is None or not game.FnExcHandler(1): + raise + except SystemExit: + raise + except: + if game is None or not game.FnExcHandler(0): + raise + finally: + removesocket('LISTEN') + removesocket('PING') + if clients: + print "Server crash -- waiting for clients to terminate..." + while clients: + iwtd = [c.socket for c in clients] + try: + iwtd, owtd, ewtd = select(iwtd, [], iwtd, 120.0) + except KeyboardInterrupt: + break + if not (iwtd or owtd or ewtd): + break # timeout - give up + for c in clients[:]: + if c.socket in ewtd: + c.disconnect("select reported an error") + elif c.socket in iwtd: + try: + data = c.socket.recv(2048) + except error, e: + c.disconnect(e) + else: + if not data and not hasattr(c.socket, 'RECV_CAN_RETURN_EMPTY'): + c.disconnect("end of data") + print "Server closed." + +def closeeverything(): + global SERVER_SHUTDOWN + SERVER_SHUTDOWN = 2.5 + if game is not None: + game.FnServerInfo("Server is stopping!") + +# ____________________________________________________________ + +try: + from localmsg import recordfilename +except ImportError: + pass +else: + + class RecordFile: + proto = 2 + has_sound = 0 + + def __init__(self, filename, sampling=1/7.77): + self.filename = filename + self.f = None + self.sampling = sampling + self.msgl = [] + self.write = self.msgl.append + + def start(self): + if not self.f: + import gzip, atexit + self.f = gzip.open(self.filename, 'wb') + atexit.register(self.f.close) + self.recnext = time() + self.sampling + + def udpdata(self, udpdata): + if self.f: + now = time() + if now >= self.recnext: + while now >= self.recnext: + self.recnext += self.sampling + self.write(message(MSG_RECORDED, udpdata)) + self.f.write(''.join(self.msgl)) + del self.msgl[:] + + recording = RecordFile(recordfilename) + del recordfilename diff --git a/common/hostchooser.py b/common/hostchooser.py new file mode 100644 index 0000000..3243e06 --- /dev/null +++ b/common/hostchooser.py @@ -0,0 +1,192 @@ +from socket import * +import time, select, sys +from errno import ETIMEDOUT + +UDP_PORT = 8056 +PING_MESSAGE = "pclient-game-ping" +PONG_MESSAGE = "server-game-pong" + + +def serverside_ping(only_port=None): + port = only_port or UDP_PORT + s = socket(AF_INET, SOCK_DGRAM) + try: + s.bind(('', port)) + s.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + return s + except error, e: + if only_port is None: + s = socket(AF_INET, SOCK_DGRAM) + s.bind(('', INADDR_ANY)) + return s + else: + return None + +def answer_ping(s, descr, addr, extra='', httpport=''): + try: + data, source = s.recvfrom(100) + except error, e: + print >> sys.stderr, 'ping error:', str(e) + return + if data == PING_MESSAGE: + print >> sys.stderr, "ping by", source + answer = '%s:%s:%s:%s:%s:%s' % (PONG_MESSAGE, descr, + addr[0], addr[1], extra, httpport) + s.sendto(answer, source) + else: + print >> sys.stderr, \ + "unexpected data on UDP port %d by" % UDP_PORT, source + + +def pick(hostlist, delay=1): + s = socket(AF_INET, SOCK_DGRAM) + s.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + for host in hostlist: + print >> sys.stderr, "* Looking for a server on %s... " % host + try: + s.sendto(PING_MESSAGE, (host, UDP_PORT)) + except error, e: + print >> sys.stderr, 'send:', str(e) + continue + while 1: + iwtd, owtd, ewtd = select.select([s], [], [], delay) + if not iwtd: + break + try: + data, answer_from = s.recvfrom(200) + except error, e: + if e.args[0] != ETIMEDOUT: + print >> sys.stderr, 'recv:', str(e) + continue + break + data = data.split(':') + if len(data) >= 4 and data[0] == PONG_MESSAGE: + hostname = data[2] or answer_from[0] + try: + port = int(data[3]) + except ValueError: + pass + else: + result = (hostname, port) + print >> sys.stderr, "* Picking %r at" % data[1], result + return result + print >> sys.stderr, "got an unexpected answer", data, "from", answer_from + print >> sys.stderr, "no server found." + raise SystemExit + +def find_servers(hostlist=[('127.0.0.1', None), ('<broadcast>', None)], + tries=2, delay=0.5, verbose=1, port_needed=1): + import gamesrv + if verbose: + print >> sys.stderr, 'Looking for servers in the following list:' + for host, udpport in hostlist: + print >> sys.stderr, ' %s, UDP port %s' % ( + host, udpport or ("%s (default)" % UDP_PORT)) + events = {} + replies = [] + s = socket(AF_INET, SOCK_DGRAM) + s.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + for trynum in range(tries): + for host, udpport in hostlist: + try: + ipaddr = host + if host != '<broadcast>': + try: + ipaddr = gethostbyname(host) + except error, e: + print >> sys.stderr, 'gethostbyname:', str(e) + s.sendto(PING_MESSAGE, (ipaddr, udpport or UDP_PORT)) + hostsend, hostrecv = events.setdefault(ipaddr, ([], [])) + hostsend.append(time.time()) + except error, e: + print >> sys.stderr, 'send:', str(e) + continue + endtime = time.time() + delay + while gamesrv.recursiveloop(endtime, [s]): + try: + data, answer_from = s.recvfrom(200) + except error, e: + if e.args[0] != ETIMEDOUT: + print >> sys.stderr, 'recv:', str(e) + continue + break + try: + ipaddr = gethostbyname(answer_from[0]) + except error: + ipaddr = answer_from[0] + else: + hostsend, hostrecv = events.setdefault(ipaddr, ([], [])) + hostrecv.append(time.time()) + data = data.split(':') + if len(data) >= 4 and data[0] == PONG_MESSAGE: + try: + port = int(data[3]) + except ValueError: + if port_needed: + continue + port = '' + if data[2]: + hostname = data[2] + realhostname = [hostname] + else: + hostname = answer_from[0] + realhostname = lazy_gethostbyaddr(hostname) + server = ':'.join(data[1:2]+data[4:]) + replies.append((hostname, realhostname, port, server, ipaddr)) + else: + print >> sys.stderr, "got an unexpected answer from", answer_from + servers = {} + aliases = {} + timeout = time.time() + 2.0 # wait for gethostbyaddr() for 2 seconds + while replies: + i = 0 + now = time.time() + while i < len(replies): + hostname, realhostname, port, server, ipaddr = replies[i] + if realhostname: + hostname = realhostname[0] # got an answer + elif now < timeout: + i += 1 # must wait some more time + continue + result = (hostname, port) + servers[result] = server + aliases[hostname] = ipaddr + del replies[i] + if replies: + time.sleep(0.08) # time for gethostbyaddr() to finish + if verbose: + print >> sys.stderr, "%d answer(s):" % len(servers), servers.keys() + for host, port in servers.keys(): + ping = None + ipaddr = aliases[host] + if ipaddr in events: + hostsend, hostrecv = events[ipaddr] + if len(hostsend) == len(hostrecv) == tries: + ping = min([t2-t1 for t1, t2 in zip(hostsend, hostrecv)]) + servers[host, port] = (servers[host, port], ping) + sys.setcheckinterval(4096) + return servers + +# ____________________________________________________________ + +HOSTNAMECACHE = {} + +def _lazygetter(hostname, resultlst): + try: + try: + hostname = gethostbyaddr(hostname)[0] + if hostname == 'localhost': + from msgstruct import HOSTNAME as hostname + except error: + pass + finally: + resultlst.append(hostname) + +def lazy_gethostbyaddr(hostname): + try: + return HOSTNAMECACHE[hostname] + except KeyError: + resultlst = HOSTNAMECACHE[hostname] = [] + import thread + thread.start_new_thread(_lazygetter, (hostname, resultlst)) + return resultlst diff --git a/common/httpserver.py b/common/httpserver.py new file mode 100644 index 0000000..a39f854 --- /dev/null +++ b/common/httpserver.py @@ -0,0 +1,192 @@ +from __future__ import generators +from __future__ import nested_scopes +import BaseHTTPServer +from SimpleHTTPServer import SimpleHTTPRequestHandler +import urlparse, cgi, htmlentitydefs +import sys, os, time +from cStringIO import StringIO + + +class Translator: + """For use with format strings. + + 'formatstring % translator' will evaluate all %(xxx)s expressions + found in the format string in the given globals/locals. + + Multiline expressions are assumed to be one or several complete + statements; they are executed and whatever they print is inserted back + into the format string.""" + + def __init__(self, globals, locals): + self.globals = globals + self.locals = locals + + def __getitem__(self, expr): + if '\n' in expr: + if not expr.endswith('\n'): + expr += '\n' + prevstdout = sys.stdout + try: + sys.stdout = f = StringIO() + exec expr in self.globals, self.locals + finally: + sys.stdout = prevstdout + return f.getvalue() + else: + return eval(expr, self.globals, self.locals) + +class TranslatorIO: + "Lazy version of Translator." + + def __init__(self, fmt, d): + self.gen = self.generate(fmt, d) + + def read(self, ignored=None): + for text in self.gen: + if text: + return text + return '' + + def close(self): + self.gen = () + + def generate(self, fmt, d): + t = Translator(d, d) + for data in fmt.split('\x0c'): + yield data % t + + +# HTML quoting + +text_to_html = {} +for key, value in htmlentitydefs.entitydefs.items(): + text_to_html[value] = '&' + key + ';' + +def htmlquote(s): + return ''.join([text_to_html.get(c, c) for c in s]) + + +# HTTP Request Handler + +pathloaders = {} + +def canonicalpath(url): + if url.startswith('/'): + url = url[1:] + return url.lower() + +def register(url, loader): + pathloaders[canonicalpath(url)] = loader + +def is_registered(url): + return canonicalpath(url) in pathloaders + +def load(filename, mimetype=None, locals=None): + if mimetype and mimetype.startswith('text/'): + mode = 'r' + else: + mode = 'rb' + f = open(filename, mode) + if locals is not None: + data = f.read() + f.close() + #data = data.replace('%"', '%%"') + d = globals().copy() + d.update(locals) + f = TranslatorIO(data, d) + return f, mimetype + +def fileloader(filename, mimetype=None): + def loader(**options): + return load(filename, mimetype) + return loader + +class HTTPRequestError(Exception): + pass + +class MiniHandler(SimpleHTTPRequestHandler): + + def send_head(self, query=''): + addr, host, path, query1, fragment = urlparse.urlsplit(self.path) + path = canonicalpath(path) + if path not in pathloaders: + if path + '/' in pathloaders: + return self.redirect(path + '/') + self.send_error(404) + return None + kwds = {} + for q in [query1, query]: + if q: + kwds.update(cgi.parse_qs(q)) + loader = pathloaders[path] + try: + hdr = self.headers + hdr['remote host'] = self.client_address[0] + f, ctype = loader(headers=hdr, **kwds) + except IOError, e: + self.send_error(404, "I/O error: " + str(e)) + return None + except HTTPRequestError, e: + self.send_error(500, str(e)) + return None + except: + f = StringIO() + import traceback + traceback.print_exc(file=f) + data = htmlquote(f.getvalue()) + data = data.replace('\n', '<br>\n') + self.send_error(500) + return StringIO('<hr><p>'+data+'</p>') + if ctype is None: + ctype = self.guess_type(self.translate_path(self.path)) + elif f is None: + return self.redirect(ctype) + + self.send_response(200) + self.send_header("Content-type", ctype) + self.end_headers() + return f + + def redirect(self, url): + self.send_response(302) + self.send_header("Content-type", 'text/html') + self.send_header("Location", url) + self.end_headers() + return StringIO('''<html><head></head><body> +Please <a href="%s">click here</a> to continue. +</body></html> +''' % url) + + def do_POST(self): + try: + nbytes = int(self.headers.getheader('content-length')) + except: + nbytes = 0 + query = self.rfile.read(nbytes).strip() + f = self.send_head(query) + if f: + self.copyfile(f, self.wfile) + f.close() + + def parse_request(self): + if self.raw_requestline == '': + return False + else: + return SimpleHTTPRequestHandler.parse_request(self) + + def address_string(self): + """Override to avoid DNS lookups""" + return "%s:%d" % self.client_address + + def finish(self): + SimpleHTTPRequestHandler.finish(self) + self.connection.close() + while actions_when_finished: + actions_when_finished.pop(0)() + +actions_when_finished = [] + +def my_host(): + import gamesrv + port = gamesrv.socketports[gamesrv.openhttpsocket()] + return '127.0.0.1:%d' % port diff --git a/common/javaserver.py b/common/javaserver.py new file mode 100644 index 0000000..15a586b --- /dev/null +++ b/common/javaserver.py @@ -0,0 +1,157 @@ +import sys, os +from cStringIO import StringIO +import httpserver + +PLAYERNAMES = ['Bub', 'Bob', 'Boob', 'Beb', + 'Biob', 'Bab', 'Bib', + 'Baub', 'Beab', 'Biab'] +LOCALDIR = os.path.abspath(os.path.dirname(__file__)) +DATADIR = os.path.join(LOCALDIR, os.pardir, 'http2', 'data') + +EMPTY_PAGE = '''<html> +<head><title>No server is running</title></head> +<body><h1>No server is running at the moment.</h1> +</body> +</html> +''' + +INDEX_PAGE = '''<html> +<head><title>%(title)s</title></head> +<body><h1>%(title)s</h1> + <applet code=pclient.class width=%(width)s height=%(height)s> + <param name="gameport" value="%(gameport)d"> + %(names1)s + </applet> +<br> +<p align="center"><a href="name.html?%(names2)s">Player Names & Teams</a></p> +</body> +</html> +''' + +NAME_LINE1 = '<param name="%s" value="%s">' +NAME_SEP1 = '\n' +NAME_LINE2 = '%s=%s' +NAME_SEP2 = '&' + +def playernames(options): + NUM_PLAYERS = len(PLAYERNAMES) + result = {} + anyname = None + for id in range(NUM_PLAYERS): + keyid = 'player%d' % id + if keyid in options: + value = options[keyid][0] + anyname = anyname or value + teamid = 'team%d' % id + if teamid in options: + team = options[teamid][0] + if len(team) == 1: + value = '%s (%s)' % (value, team) + result[keyid] = value + if 'c' in options: + for id in range(NUM_PLAYERS): + keyid = 'player%d' % id + try: + del result[keyid] + except KeyError: + pass + if 'f' in options: + for id in range(NUM_PLAYERS): + keyid = 'player%d' % id + if not result.get(keyid): + result[keyid] = anyname or PLAYERNAMES[id] + else: + anyname = result[keyid] + return result + +def indexloader(**options): + if 'cheat' in options: + for opt in options.pop('cheat'): + __cheat(opt) + import gamesrv + if gamesrv.game is None: + indexdata = EMPTY_PAGE + else: + names = playernames(options).items() + indexdata = INDEX_PAGE % { + 'title': gamesrv.game.FnDesc, + 'width': gamesrv.game.width, + 'height': gamesrv.game.height, + 'gameport': gamesrv.game.address[1], + 'names1': NAME_SEP1.join([NAME_LINE1 % kv for kv in names]), + 'names2': NAME_SEP2.join([NAME_LINE2 % kv for kv in names]), + } + return StringIO(indexdata), 'text/html' + +def nameloader(**options): + if 's' in options: + return indexloader(**options) + locals = { + 'options': playernames(options), + } + return httpserver.load(os.path.join(DATADIR, 'name.html'), + 'text/html', locals=locals) + + +wave_cache = {} + +def wav2au(data): + # Very limited! Assumes a standard 8-bit mono .wav as input + import audioop, struct + freq, = struct.unpack("<i", data[24:28]) + data = data[44:] + data = audioop.bias(data, 1, -128) + data, ignored = audioop.ratecv(data, 1, 1, freq, 8000, None) + data = audioop.lin2ulaw(data, 1) + data = struct.pack('>4siiiii8s', + '.snd', # header + struct.calcsize('>4siiiii8s'), # header size + len(data), # data size + 1, # encoding + 8000, # sample rate + 1, # channels + 'magic.au') + data + return data + +def sampleloader(code=[], **options): + import gamesrv + try: + data = wave_cache[code[0]] + except KeyError: + for key, snd in gamesrv.samples.items(): + if str(getattr(snd, 'code', '')) == code[0]: + data = wave_cache[code[0]] = wav2au(snd.read()) + break + else: + raise KeyError, code[0] + return StringIO(data), 'audio/wav' + + +def setup(): + dir = os.path.abspath(os.path.join(os.path.dirname(__file__), + os.pardir, + 'java')) + if not os.path.isdir(dir): + return + + # register all '.class' files + for name in os.listdir(dir): + if name.endswith('.class'): + httpserver.register(name, httpserver.fileloader(os.path.join(dir, name))) + + # register a '', an 'index.html', and a 'name.html' file + httpserver.register('', indexloader) + httpserver.register('index.html', indexloader) + httpserver.register('name.html', nameloader) + + # 'name.html' has a few images, list the .png files in DATADIR + for fn in os.listdir(DATADIR): + fn = fn.lower() + if fn.endswith('.png'): + httpserver.register(fn, httpserver.fileloader( + os.path.join(DATADIR, fn))) + + # register the sample loader + httpserver.register('sample.wav', sampleloader) + +setup() diff --git a/common/msgstruct.py b/common/msgstruct.py new file mode 100644 index 0000000..26389f0 --- /dev/null +++ b/common/msgstruct.py @@ -0,0 +1,75 @@ +from struct import pack, unpack, calcsize + +try: + from localmsg import PORTS +except ImportError: + PORTS = {} +try: + from localmsg import HOSTNAME +except ImportError: + from socket import gethostname + HOSTNAME = gethostname() + + +MSG_WELCOME = "Welcome to gamesrv.py(3) !\n" +MSG_BROADCAST_PORT= "*" +MSG_DEF_PLAYFIELD = "p" +MSG_DEF_KEY = "k" +MSG_DEF_ICON = "r" +MSG_DEF_BITMAP = "m" +MSG_DEF_SAMPLE = "w" +MSG_DEF_MUSIC = "z" +MSG_PLAY_MUSIC = "Z" +MSG_FADEOUT = "f" +MSG_PLAYER_JOIN = "+" +MSG_PLAYER_KILL = "-" +MSG_PLAYER_ICON = "i" +MSG_PING = "g" +MSG_PONG = "G" +MSG_INLINE_FRAME = "\\" +MSG_PATCH_FILE = MSG_DEF_MUSIC +MSG_ZPATCH_FILE = "P" +MSG_MD5_FILE = "M" +MSG_RECORDED = "\x00" + +CMSG_PROTO_VERSION= "v" +CMSG_KEY = "k" +CMSG_ADD_PLAYER = "+" +CMSG_REMOVE_PLAYER= "-" +CMSG_UDP_PORT = "<" +CMSG_ENABLE_SOUND = "s" +CMSG_ENABLE_MUSIC = "m" +CMSG_PING = "g" +CMSG_PONG = "G" +CMSG_DATA_REQUEST = "M" +CMSG_PLAYER_NAME = "n" + +BROADCAST_MESSAGE = "game!" # less than 6 bytes + + +def message(tp, *values): + strtype = type('') + typecodes = [''] + for v in values: + if type(v) is strtype: + typecodes.append('%ds' % len(v)) + elif 0 <= v < 256: + typecodes.append('B') + else: + typecodes.append('l') + typecodes = ''.join(typecodes) + assert len(typecodes) < 256 + return pack(("!B%dsc" % len(typecodes)) + typecodes, + len(typecodes), typecodes, tp, *values) + +def decodemessage(data): + if data: + limit = ord(data[0]) + 1 + if len(data) >= limit: + typecodes = "!c" + data[1:limit] + end = limit + calcsize(typecodes) + if len(data) >= end: + return unpack(typecodes, data[limit:end]), data[end:] + elif end > 1000000: + raise OverflowError + return None, data diff --git a/common/pixmap.py b/common/pixmap.py new file mode 100644 index 0000000..13142d5 --- /dev/null +++ b/common/pixmap.py @@ -0,0 +1,163 @@ +from __future__ import generators +import cStringIO + +def decodepixmap(data): + f = cStringIO.StringIO(data) + sig = f.readline().strip() + assert sig == "P6" + while 1: + line = f.readline().strip() + if not line.startswith('#'): + break + wh = line.split() + w, h = map(int, wh) + sig = f.readline().strip() + assert sig == "255" + data = f.read() + f.close() + return w, h, data + +def encodepixmap(w, h, data): + return 'P6\n%d %d\n255\n%s' % (w, h, data) + +def cropimage((w, h, data), (x1, y1, w1, h1)): + assert 0 <= x1 <= x1+w1 <= w + assert 0 <= y1 <= y1+h1 <= h + scanline = w*3 + lines = [data[p:p+w1*3] + for p in range(y1*scanline + x1*3, + (y1+h1)*scanline + x1*3, + scanline)] + return w1, h1, ''.join(lines) + +def vflip(w, h, data): + scanline = w*3 + lines = [data[p:p+scanline] for p in range(0, len(data), scanline)] + lines.reverse() + return ''.join(lines) + +def hflip(w, h, data): + scanline = w*3 + lines = [''.join([data[p:p+3] for p in range(p1+scanline-3, p1-3, -3)]) + for p1 in range(0, len(data), scanline)] + return ''.join(lines) + +def rotate_cw(w, h, data): + scanline = w*3 + lastline = len(data) - scanline + lines = [''.join([data[p:p+3] for p in range(lastline + p1, -1, -scanline)]) + for p1 in range(0, scanline, 3)] + return ''.join(lines) + +def rotate_ccw(w, h, data): + scanline = w*3 + lines = [''.join([data[p:p+3] for p in range(p1, len(data), scanline)]) + for p1 in range(scanline-3, -3, -3)] + return ''.join(lines) + +def rotate_180(w, h, data): + scanline = w*3 + lines = [''.join([data[p:p+3] for p in range(p1+scanline-3, p1-3, -3)]) + for p1 in range(0, len(data), scanline)] + lines.reverse() + return ''.join(lines) + +def makebkgnd(w, h, data): + scanline = 3*w + result = [] + for position in range(0, scanline*h, scanline): + line = [] + for p in range(position, position+scanline, 3): + line.append(2 * (chr(ord(data[p ]) >> 3) + + chr(ord(data[p+1]) >> 3) + + chr(ord(data[p+2]) >> 3))) + line = ''.join(line) + result.append(line) + result.append(line) + return w*2, h*2, ''.join(result) + +translation_darker = ('\x00\x01' + '\x00'*126 + + ''.join([chr(n//4) for n in range(0,128)])) +translation_dragon = translation_darker[:255] + '\xC0' + +def make_dark((w, h, data), translation): + return w, h, data.translate(translation) + +def col((r, g, b)): + r = ord(r) + g = ord(g) + b = ord(b) + return ((g>>2 + r>>3) << 24) | (b << 16) | (g << 8) | r + +def imagezoomer(w, h, data): + "Zoom a cartoon image by a factor of three, progressively." + scale = 3 + scanline = 3*w + rw = (w-1)*scale+1 + rh = (h-1)*scale+1 + pixels = [] + colcache = {} + revcache = {} + for base in range(0, scanline*h, scanline): + line = [] + for x in range(w): + key = data[base + 3*x : base + 3*(x+1)] + try: + c = colcache[key] + except KeyError: + c = colcache[key] = col(key) + revcache[c] = key + line.append(c) + pixels.append(line) + yield None + + Pairs = { + (0, 0): [(0, 0, 0, 0), + (-1,0, 1, 0), + (0,-1, 0, 1)], + (1, 0): [(0, 0, 1, 0), + (0, 1, 1,-1), + (0,-1, 1, 1)], + (2, 0): [(0, 0, 1, 0), + (0, 1, 1,-1), + (0,-1, 1, 1)], + (0, 1): [(0, 0, 0, 1), + (-1,0, 1, 1), + (1, 0,-1, 1)], + (1, 1): [(0, 0, 1, 1), + (0, 1, 1, -1), + (1, 0,-1, 1)], + (2, 1): [(1, 0, 0, 1), + (0,-1, 1, 1), + (0, 0, 2, 1)], + (0, 2): [(0, 0, 0, 1), + (-1,0, 1, 1), + (1, 0,-1, 1)], + (1, 2): [(0, 1, 1, 0), + (-1,0, 1, 1), + (0, 0, 1, 2)], + (2, 2): [(0, 0, 1, 1), + (0, 1, 2, 0), + (1, 0, 0, 2)], + } + result = [] + for y in range(rh): + yield None + for x in range(rw): + # ______________________________ + + i = x//scale + j = y//scale + ps = [] + for dx1, dy1, dx2, dy2 in Pairs[x%scale, y%scale]: + if (0 <= i+dx1 < w and 0 <= i+dx2 < w and + 0 <= j+dy1 < h and 0 <= j+dy2 < h): + p1 = pixels[j+dy1][i+dx1] + p2 = pixels[j+dy2][i+dx2] + ps.append(max(p1, p2)) + p1 = min(ps) + + # ______________________________ + result.append(revcache[p1]) + data = ''.join(result) + yield (rw, rh, data) diff --git a/common/stdlog.py b/common/stdlog.py new file mode 100644 index 0000000..3d8063c --- /dev/null +++ b/common/stdlog.py @@ -0,0 +1,106 @@ +import sys, os +from time import localtime, ctime + + +class LogFile: + + def __init__(self, filename=None, limitsize=32768): + if filename is None: + filename = sys.argv[0] + if filename.endswith('.py'): + filename = filename[:-3] + filename += '.log' + self.limitsize = limitsize + if not self._open(filename): + import tempfile + filename = os.path.join(tempfile.gettempdir(), + os.path.basename(filename)) + if not self._open(filename): + self.f = self.filename = None + self.lasttime = None + + def close(self): + if self.f is not None: + self.f.close() + self.f = None + + def __nonzero__(self): + return self.f is not None + + def _open(self, filename): + try: + self.f = open(filename, 'r+', 1) + self.f.seek(0, 2) + except IOError: + # The open r+ might have failed simply because the file + # does not exist. Try to create it. + try: + self.f = open(filename, 'w+', 1) + except (OSError, IOError): + return 0 + except OSError: + return 0 + self.filename = filename + if self.f.tell() > 0: + print >> self.f + print >> self.f, '='*44 + return 1 + + def _check(self): + if self.f is None: + return 0 + lt = localtime() + if lt[:4] != self.lasttime: + self.lasttime = lt[:4] + if self.f.tell() >= self.limitsize: + self.f.seek(-(self.limitsize>>1), 1) + data = self.f.read() + self.f.seek(0) + self.f.write('(...)' + data) + self.f.truncate() + self.f.write('========= %s =========\n' % ctime()) + return 1 + + def write(self, data): + if self._check(): + self.f.write(data) + + def writelines(self, data): + if self._check(): + self.f.writelines(data) + + def flush(self): + if self._check(): + self.f.flush() + + +class Logger: + stdout_captured = 0 + stderr_captured = 0 + + def __init__(self, f): + self.targets = [f] + + def capture_stdout(self): + if not Logger.stdout_captured: + self.targets.append(sys.stdout) + sys.stdout = self + Logger.stdout_captured = 1 + + def capture_stderr(self): + if not Logger.stderr_captured: + self.targets.append(sys.stderr) + sys.stderr = self + Logger.stderr_captured = 1 + + def write(self, data): + for f in self.targets: + f.write(data) + + def writelines(self, data): + for f in self.targets: + f.writelines(data) + + def flush(self): + for f in self.targets: + f.flush() diff --git a/common/udpovertcp.py b/common/udpovertcp.py new file mode 100644 index 0000000..eae3d6b --- /dev/null +++ b/common/udpovertcp.py @@ -0,0 +1,46 @@ +from socket import * +from msgstruct import * +#from fcntl import ioctl +#from termios import TIOCOUTQ +from zlib import compressobj, Z_SYNC_FLUSH +import struct + +ZeroBuffer = struct.pack("i", 0) + + +class SocketMarshaller: + + def __init__(self, tcpsock, mixer): + self.tcpsock = tcpsock + self.mixer = mixer + self.mixer_can_mix = mixer.send_can_mix + self.mixer_send = mixer.send_buffer + self.tcpsock_fd = tcpsock.fileno() + # try to reduce TCP latency + try: + tcpsock.setsockopt(SOL_IP, IP_TOS, 0x10) # IPTOS_LOWDELAY + except error, e: + print "Cannot set IPTOS_LOWDELAY for client:", str(e) + try: + tcpsock.setsockopt(SOL_TCP, TCP_NODELAY, 1) + except error, e: + print "Cannot set TCP_NODELAY for client:", str(e) + compressor = compressobj(6) + self.compress = compressor.compress + self.compress_flush = compressor.flush + + def send(self, data): + if self.mixer_can_mix(): + # discard all packets if there is still data waiting in tcpsock + # --- mmmh, works much better without this check --- + #try: + # if ioctl(self.tcpsock_fd, TIOCOUTQ, ZeroBuffer) != ZeroBuffer: + # return + #except IOError, e: + # print "ioctl(TIOCOUTQ) failed, disconnecting client" + # self.mixer.disconnect(e) + #else: + data = self.compress(data) + self.compress_flush(Z_SYNC_FLUSH) + self.mixer_send(message(MSG_INLINE_FRAME, data)) + return len(data) + return 0 |