#! /usr/bin/env python import sys, os from socket import * from select import select import struct, zlib import time from common.msgstruct import * from common.pixmap import decodepixmap from common import hostchooser import modes from modes import KeyPressed, KeyReleased import caching #import psyco; psyco.full() # switch to udp_over_tcp if the udp socket didn't receive at least 60% of # the packets sent by the server UDP_EXPECTED_RATIO = 0.60 def loadpixmap(dpy, data, colorkey=None): w, h, data = decodepixmap(data) if colorkey is None: colorkey = -1 elif colorkey < 0: r, g, b = struct.unpack("BBB", self.data[:3]) colorkey = b | (g<<8) | (r<<16) return dpy.pixmap(w, h, data, colorkey) class Icon: alpha = 255 def __init__(self, playfield): self.playfield = playfield self.size = 0, 0 def __getattr__(self, attr): if attr == 'pixmap': self.pixmap = self.playfield.getpixmap(self.bmpcode) if hasattr(self.playfield.dpy, 'getopticon'): ico = self.playfield.dpy.getopticon( self.pixmap, self.originalrect, self.alpha) if ico is not None: self.pixmap = ico self.rect = None return self.pixmap elif attr in ('bmpcode', 'rect'): raise KeyError(attr) elif attr == 'originalrect': self.originalrect = self.rect return self.originalrect raise AttributeError(attr) def clear(self): if 'pixmap' in self.__dict__: del self.pixmap def __str__(self): return f"Icon( bmpcode={self.bmpcode}, alpha={self.alpha} )" class DataChunk(caching.Data): SOURCEDIR = os.path.abspath(os.path.join(os.path.dirname(caching.__file__), os.pardir)) CACHEDIR = os.path.join(SOURCEDIR, 'cache') TOTAL = 0 def __init__(self, fileid): caching.Data.__init__(self) self.fileid = fileid self.pending = [] self.progresshook = None def server_md5(self, playfield, filename, position, length, checksum): if not self.loadfrom(filename, position, length, checksum): self.pending.append((0, position)) playfield.s.sendall(message(CMSG_DATA_REQUEST, self.fileid, position, length)) def server_patch(self, position, data, lendata): #print 'server_patch', self.fileid, position, len(data) prev = DataChunk.TOTAL >> 10 DataChunk.TOTAL += lendata total = DataChunk.TOTAL >> 10 if total != prev: print("downloaded %dkb of data from server" % total) self.store(position, data) try: self.pending.remove((0, position)) except ValueError: pass else: while self.pending and self.pending[0][0]: callback = self.pending[0][1] del self.pending[0] callback(self) def when_ready(self, callback): if self.pending: self.pending.append((1, callback)) else: callback(self) class Playfield: TASKBAR_HEIGHT = 48 def __init__(self, s, sockaddr): self.s = s self.sockaddr = sockaddr try: self.s.setsockopt(SOL_IP, IP_TOS, 0x10) # IPTOS_LOWDELAY except error as e: print("Cannot set IPTOS_LOWDELAY:", str(e), file=sys.stderr) try: self.s.setsockopt(SOL_TCP, TCP_NODELAY, 1) except error as e: print("Cannot set TCP_NODELAY:", str(e), file=sys.stderr) initialbuf = b"" while 1: t = self.s.recv(200) if not t and not hasattr(self.s, 'RECV_CAN_RETURN_EMPTY'): raise error("connexion closed") initialbuf += t if len(initialbuf) >= len(MSG_WELCOME): head = initialbuf[:len(MSG_WELCOME)] tail = initialbuf[len(MSG_WELCOME):] if head != MSG_WELCOME: raise error("connected to something not a game server") if b'\n' in tail: break n = tail.index(b'\n') line2 = tail[:n] self.initialbuf = tail[n+1:] self.gameident = line2.strip() ## self.datapath = None ## if self.gameident.endswith(']'): ## i = self.gameident.rfind('[') ## if i >= 0: ## self.gameident, self.datapath = (self.gameident[:i].strip(), ## self.gameident[i+1:-1]) print("connected to %r." % self.gameident) self.s.sendall(message(CMSG_PROTO_VERSION, 3)) def setup(self, mode, udp_over_tcp): self.playing = {} # 0, 1, or 'l' for local self.keys = {} self.keycodes = {} self.last_key_event = (None, None) self.dpy = None self.snd = None self.pixmaps = {} # {bmpcode: dpy_pixmap} self.bitmaps = {} # {bmpcode: (fileid_or_data, colorkey)} self.icons = {} self.sounds = {} self.currentmusic = None self.fileids = {} self.sprites = [] self.playingsounds = {} self.playericons = {} self.screenmode = mode self.initlevel = 0 if 'udp_over_tcp' in mode[-1]: udp_over_tcp = mode[-1]['udp_over_tcp'] self.trackcfgmtime = None if 'cfgfile' in mode[-1]: self.trackcfgfile = mode[-1]['cfgfile'] else: self.trackcfgfile = os.path.join(DataChunk.SOURCEDIR, 'http2', 'config.txt') self.udpsock = None self.udpsock_low = None self.udpsock2 = None self.accepted_broadcast = 0 self.tcpbytecounter = 0 self.udpbytecounter = 0 if udp_over_tcp == 1: self.start_udp_over_tcp() else: self.pending_udp_data = None if udp_over_tcp == 'auto': self.udpsock_low = 0 self.dyndecompress = [[None, None, None, None] for i in range(8)] self.dynnorepeat = None def run(self, mode, udp_over_tcp='auto'): self.setup(mode, udp_over_tcp) try: self.mainloop() finally: if self.dpy: self.dpy.close() try: self.s.close() except: pass def mainloop(self): pss = hostchooser.serverside_ping() self.initial_iwtd = [self.s, pss] self.iwtd = self.initial_iwtd[:] self.animdelay = 0.0 inbuf = self.process_inbuf(self.initialbuf) self.initialbuf = "" errors = 0 while 1: if self.dpy: self.processkeys() iwtd, owtd, ewtd = select(self.iwtd, [], [], self.animdelay) self.animdelay = 0.5 if self.dpy: self.processkeys() if self.s in iwtd: inputdata = self.s.recv(0x6000) self.tcpbytecounter += len(inputdata) inbuf += inputdata inbuf = self.process_inbuf(inbuf) if self.dpy: if self.udpsock in iwtd: udpdata1 = None while self.udpsock in iwtd: try: udpdata = self.udpsock.recv(65535) except error as e: print(e, file=sys.stderr) errors += 1 if errors > 10: raise break self.udpbytecounter += len(udpdata) if len(udpdata) > 3 and '\x80' <= udpdata[0] < '\x90': udpdata = self.dynamic_decompress(udpdata) if udpdata is not None: udpdata1 = udpdata iwtd, owtd, ewtd = select(self.iwtd, [], [], 0) if udpdata1 is not None: self.update_sprites(udpdata1) if self.udpsock2 in iwtd: while self.udpsock2 in iwtd: udpdata = self.udpsock2.recv(65535) self.udpbytecounter += len(udpdata) if udpdata == BROADCAST_MESSAGE: if not self.accepted_broadcast: self.s.sendall(message(CMSG_UDP_PORT, b'*')) self.accepted_broadcast = 1 #self.udpsock_low = None udpdata = '' iwtd, owtd, ewtd = select(self.iwtd, [], [], 0) if udpdata and self.accepted_broadcast: self.update_sprites(udpdata) if self.pending_udp_data: self.update_sprites(self.pending_udp_data) self.pending_udp_data = '' erasetb = self.taskbarmode and self.draw_taskbar() d = self.dpy.flip() if d: self.animdelay = min(self.animdelay, d) if self.snd: d = self.snd.flop() if d: self.animdelay = min(self.animdelay, d) if erasetb: self.erase_taskbar(erasetb) if pss in iwtd: hostchooser.answer_ping(pss, self.gameident, self.sockaddr) def process_inbuf(self, inbuf): while inbuf: values, inbuf = decodemessage(inbuf) if not values: break # incomplete message fn = Playfield.MESSAGES.get(values[0], self.msg_unknown) fn(self, *values[1:]) return inbuf def dynamic_decompress(self, udpdata): # Format of a UDP version 3 packet: # header byte: 0x80 - 0x87 packet from thread 0 - 7 # or 0x88 - 0x8F reset packet from thread 0 - 7 # previous frame in same thread (1 byte) # frame number (1 byte) thread = self.dyndecompress[ord(udpdata[0]) & 7] # thread==[decompress, lastframenumber, recompressed, lastframedata] prevframe = udpdata[1] thisframe = udpdata[2] #print '---' #for t in self.dyndecompress: # print repr(t)[:120] #print #print `udpdata[:3]` if udpdata[0] >= '\x88': # reset d = zlib.decompressobj().decompress if prevframe != thisframe: # if not global sync point # sync point from a previous frame # find all threads with the same prevframe threads = [t for t in self.dyndecompress if prevframe == t[1]] if not threads: return None # lost # find a thread with already-recompressed data for t in threads: if t[2]: data = t[3] break else: # recompress and cache the prevframe data t = threads[0] data = t[3] co = zlib.compressobj(6) data = co.compress(data) + co.flush(zlib.Z_SYNC_FLUSH) t[2] = 1 t[3] = data d(data) # use it to initialize the state of the decompressobj #print d thread[0] = d elif prevframe != thread[1]: #print 'lost' return None # lost else: d = thread[0] # go forward in thread try: framedata = d(udpdata[3:]) #print d thread[1] = thisframe thread[2] = 0 thread[3] = framedata if thisframe == self.dynnorepeat: return None self.dynnorepeat = thisframe return framedata except zlib.error: #print 'crash' return None def geticon(self, icocode): try: return self.icons[icocode] except KeyError: ico = self.icons[icocode] = Icon(self) return ico def getpixmap(self, bmpcode): try: return self.pixmaps[bmpcode] except KeyError: data, colorkey = self.bitmaps[bmpcode] if type(data) is type(''): data = zlib.decompress(data) else: if data.pending: raise KeyError data = data.read() print(f"getpixmap(bmpcode = {bmpcode}) data={data[:64]} colorkey={colorkey}",file=sys.stderr) pixmap = loadpixmap(self.dpy, data, colorkey) self.pixmaps[bmpcode] = pixmap return pixmap def update_sprites(self, udpdata): sprites = self.sprites unpack = struct.unpack currentsounds = {} base = 0 while udpdata[base+4:base+6] == b'\xFF\xFF': key, lvol, rvol = struct.unpack("!hBB", udpdata[base:base+4]) try: snd = self.sounds[key] except KeyError: pass # ignore sounds with bad code (probably not defined yet) else: n = self.playingsounds.get(key) if n: currentsounds[key] = n-1 elif self.snd: self.snd.play(snd, lvol / 255.0, rvol / 255.0) currentsounds[key] = 4 base += 6 self.playingsounds = currentsounds for j in range(len(sprites)): if sprites[j][0] != udpdata[base:base+6]: removes = sprites[j:] del sprites[j:] removes.reverse() eraser = self.dpy.putppm for reserved, eraseargs in removes: eraser(*eraseargs) break base += 6 #print "%d sprites redrawn" % (len(udpdata)/6-j) try: overlayer = self.dpy.overlayppm except AttributeError: getter = self.dpy.getppm setter = self.dpy.putppm for j in range(base, len(udpdata)-5, 6): info = udpdata[j:j+6] x, y, icocode = unpack("!hhh", info[:6]) try: ico = self.icons[icocode] sprites.append((info, (x, y, getter((x, y) + ico.size)))) setter(x, y, ico.pixmap, ico.rect) except KeyError: #print "bad ico code", icocode pass # ignore sprites with bad ico (probably not defined yet) else: for j in range(base, len(udpdata)-5, 6): info = udpdata[j:j+6] x, y, icocode = unpack("!hhh", info[:6]) try: ico = self.icons[icocode] overlay = overlayer(x, y, ico.pixmap, ico.rect, ico.alpha) sprites.append((info, overlay)) except KeyError: #print "bad ico code", icocode pass # ignore sprites with bad ico (probably not defined yet) t0, n = self.painttimes n = n + 1 if n == 50: t = time.time() t, t0 = t-t0, t if t: print("%.2f images per second, %.1f kbytes per second" % ( float(n)/t, float(self.tcpbytecounter+self.udpbytecounter)/1024/t)) self.tcpbytecounter = -self.udpbytecounter n = 0 self.painttimes = t0, n def get_taskbar(self): y0 = self.height - self.TASKBAR_HEIGHT iconlist = [] f = 1.5 * time.time() f = f-int(f) pi = list(self.playericons.items()) pi.sort() xpos = 0 for id, ico in pi: if self.playing.get(id) != 'l': w, h = ico.size xpos += int(w * 5 / 3) if not self.playing.get(id): y = self.height - h if self.keydefinition and id == self.keydefinition[0]: num, icons = self.keys[self.nextkeyname()] ico = icons[int(f*len(icons))-1] y = y0 + int((self.TASKBAR_HEIGHT-ico.size[1])/2) self.animdelay = 0.04 iconlist.append((xpos-w, y, ico, id)) pi.reverse() f = f * (1.0-f) * 4.0 xpos = self.width for id, ico in pi: if self.playing.get(id) == 'l': w, h = ico.size xpos -= int(w * 5 / 3) dy = self.TASKBAR_HEIGHT - h - 1 y = self.height - h - int(dy*f) iconlist.append((xpos, y, ico, id)) self.animdelay = 0.04 return y0, iconlist def clic_taskbar(self, xxx_todo_changeme): (cx,cy) = xxx_todo_changeme y0, icons = self.get_taskbar() if cy >= y0: for x, y, ico, id in icons: if x <= cx < x+ico.size[0]: return id return None def draw_taskbar(self): y0, icons = self.get_taskbar() rect = (0, y0, self.width, self.TASKBAR_HEIGHT) bkgnd = self.dpy.getppm(rect) self.dpy.taskbar(rect) for x, y, ico, id in icons: try: print(f"x={x} y={y} ico={ico} id={id}",file=sys.stderr) self.dpy.putppm(x, y, ico.pixmap, ico.rect) except KeyError: pass return y0, bkgnd def erase_taskbar(self, xxx_todo_changeme1): (y0, bkgnd) = xxx_todo_changeme1 self.dpy.putppm(0, y0, bkgnd) def nextkeyname(self): pid, df = self.keydefinition undef = [(num, keyname) for keyname, (num, icons) in list(self.keys.items()) if keyname not in df and icons] if undef: num, keyname = min(undef) return keyname else: return None def startplaying(self): args = () if hasattr(self.s, 'udp_over_udp_mixer'): # for SocketOverUdp: reuse the UDP address port = self.s.getsockname()[1] self.udpsock_low = None self.s.udp_over_udp_decoder = self.udp_over_udp_decoder self.start_udp_over_tcp() elif self.pending_udp_data is not None: port = MSG_INLINE_FRAME else: if '*udpsock*' in PORTS: self.udpsock, (host, port) = PORTS['*udpsock*'] args = (host,) else: self.udpsock = socket(AF_INET, SOCK_DGRAM) self.udpsock.bind(('', PORTS.get('CLIENT', INADDR_ANY))) host, port = self.udpsock.getsockname() # Send a dummy UDP message to the server. Some NATs will # then let through the UDP messages from the server. self.udpsock.sendto(b'.', self.s.getpeername()) self.iwtd.append(self.udpsock) self.initial_iwtd.append(self.udpsock) if 'sendudpto' in PORTS: args = (PORTS['sendudpto'],) outbound = [] outbound.append(message(CMSG_UDP_PORT, port, *args)) if self.snd and self.snd.has_music: outbound.append(message(CMSG_ENABLE_MUSIC, 1)) outbound.append(message(CMSG_PING)) self.s.sendall(b''.join(outbound)) def start_udp_over_tcp(self): self.pending_udp_data = '' self.udp_over_tcp_decompress = zlib.decompressobj().decompress self.udpsock_low = None for name in ('udpsock', 'udpsock2'): sock = getattr(self, name) if sock is not None: try: self.iwtd.remove(sock) except ValueError: pass try: self.initial_iwtd.remove(sock) except ValueError: pass sock.close() setattr(self, name, None) def udp_over_udp_decoder(self, udpdata): if len(udpdata) > 3 and b'\x80' <= udpdata[0] < b'\x90': data = self.dynamic_decompress(udpdata) if data: self.pending_udp_data = data def processkeys(self): keyevents = self.dpy.keyevents() if keyevents: now = time.time() pending = {} for keysym, event in keyevents: pending[keysym] = event for keysym, event in list(pending.items()): code = self.keycodes.get((keysym, event)) if code and self.playing.get(code[0]) == 'l': if (code == self.last_key_event[0] and now - self.last_key_event[1] < 0.77): continue # don't send too much events for auto-repeat self.last_key_event = code, now self.s.sendall(code[1]) elif self.keydefinition: self.define_key(keysym) pointermotion = self.dpy.pointermotion() if pointermotion: x, y = pointermotion self.settaskbar(y >= self.height - 2*self.TASKBAR_HEIGHT) mouseevents = self.dpy.mouseevents() if mouseevents: self.settaskbar(1) self.keydefinition = None for clic in mouseevents: clic_id = self.clic_taskbar(clic) if clic_id is not None: if self.playing.get(clic_id) == 'l': self.s.sendall(message(CMSG_REMOVE_PLAYER, clic_id)) else: self.keydefinition = clic_id, {} if self.taskbartimeout is not None and time.time() > self.taskbartimeout: self.settaskbar(0) def settaskbar(self, nmode): self.taskbartimeout = None if self.taskbarfree: self.taskbarmode = (nmode or 'l' not in list(self.playing.values()) or (self.keydefinition is not None)) if nmode: self.taskbartimeout = time.time() + 5.0 if hasattr(self.dpy, 'settaskbar'): self.dpy.settaskbar(self.taskbarmode) def define_key(self, keysym): clic_id, df = self.keydefinition if keysym in list(df.values()): return df[self.nextkeyname()] = keysym if self.nextkeyname() is not None: return self.keydefinition = None self.s.sendall(message(CMSG_ADD_PLAYER, clic_id)) for keyname, (num, icons) in list(self.keys.items()): if keyname[:1] == '-': event = KeyReleased keyname = keyname[1:] else: event = KeyPressed if keyname in df: keysym = df[keyname] self.keycodes[keysym, event] = \ clic_id, message(CMSG_KEY, clic_id, num) def msg_unknown(self, *rest): print("?", file=sys.stderr) def msg_player_join(self, id, local, *rest): if local: self.playing[id] = 'l' self.settaskbar(0) self.checkcfgfile(1) else: self.playing[id] = 1 def msg_player_kill(self, id, *rest): self.playing[id] = 0 for key, (pid, msg) in list(self.keycodes.items()): if pid == id: del self.keycodes[key] def msg_broadcast_port(self, port): if self.pending_udp_data is not None: return if self.udpsock2 is not None: try: self.iwtd.remove(self.udpsock2) except ValueError: pass try: self.initial_iwtd.remove(self.udpsock2) except ValueError: pass self.udpsock2.close() self.udpsock2 = None self.accepted_broadcast = 0 try: self.udpsock2 = socket(AF_INET, SOCK_DGRAM) self.udpsock2.bind(('', port)) self.udpsock2.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) except error as e: print("Cannot listen on the broadcast port %d" % port, str(e)) self.udpsock2 = None else: self.iwtd.append(self.udpsock2) self.initial_iwtd.append(self.udpsock2) def msg_def_playfield(self, width, height, backcolor=None, gameident=None, *rest): #if self.snd is not None: # self.snd.close() if self.dpy is not None: # clear all pixmaps for ico in list(self.icons.values()): ico.clear() self.pixmaps.clear() self.dpy.close() del self.sprites[:] self.width = width self.height = height if gameident: self.gameident = gameident self.dpy = modes.open_dpy(self.screenmode, width, height, self.gameident.decode()) self.snd = self.snd or modes.open_snd(self.screenmode) if self.snd: self.s.sendall(message(CMSG_ENABLE_SOUND)) self.iwtd = self.dpy.selectlist() + self.initial_iwtd self.dpy.clear() # backcolor is ignored self.painttimes = (time.time(), 0) self.s.sendall(message(CMSG_PING)) self.taskbarmode = 0 self.taskbarfree = 0 self.taskbartimeout = None self.keydefinition = None def msg_def_key(self, name, num, *icons): self.keys[name] = num, [self.geticon(ico) for ico in icons] def msg_def_icon(self, bmpcode, icocode, x, y, w, h, alpha=255, *rest): ## if h<0: ## try: ## bitmap, height = self.flippedbitmaps[bmpcode] ## except KeyError: ## bitmap, height = self.dpy.vflipppm(self.bitmaps[bmpcode]) ## self.flippedbitmaps[bmpcode] = bitmap, height ## y = height - y ## h = - h ## else: ico = self.geticon(icocode) ico.bmpcode = bmpcode ico.rect = x, y, w, h ico.size = w, h if alpha < 255: ico.alpha = alpha def msg_def_bitmap(self, bmpcode, data, colorkey=None, *rest): if type(data) is not type(''): data = self.fileids[data] self.bitmaps[bmpcode] = data, colorkey def msg_def_sample(self, smpcode, data, *rest): def ready(f, self=self, smpcode=smpcode): if self.snd: self.sounds[smpcode] = self.snd.sound(f) f.clear() if type(data) is type(''): data = zlib.decompress(data) f = DataChunk(None) f.store(0, data) ready(f) else: f = self.fileids[data] f.when_ready(ready) def msg_patch_file(self, fileid, position, data, lendata=None, *rest): if fileid in self.fileids: f = self.fileids[fileid] else: f = self.fileids[fileid] = DataChunk(fileid) f.server_patch(position, data, lendata or len(data)) def msg_zpatch_file(self, fileid, position, data, *rest): data1 = zlib.decompress(data) self.msg_patch_file(fileid, position, data1, len(data), *rest) def msg_md5_file(self, fileid, filename, position, length, checksum, *rest): if fileid in self.fileids: f = self.fileids[fileid] else: f = self.fileids[fileid] = DataChunk(fileid) f.server_md5(self, filename, position, length, checksum) def msg_play_music(self, loop_from, *codes): codes = [self.fileids[c] for c in codes] self.currentmusic = loop_from, codes, list(codes) self.activate_music() def activate_music(self, f=None): loop_from, codes, checkcodes = self.currentmusic if checkcodes: checkcodes.pop().when_ready(self.activate_music) elif self.snd: self.snd.play_musics(codes, loop_from) def msg_fadeout(self, time, *rest): if self.snd: self.snd.fadeout(time) def msg_player_icon(self, pid, icocode, *rest): self.playericons[pid] = self.geticon(icocode) def checkcfgfile(self, force=0): if self.trackcfgfile: try: st = os.stat(self.trackcfgfile) except OSError: pass else: if force or (st.st_mtime != self.trackcfgmtime): self.trackcfgmtime = st.st_mtime try: f = open(self.trackcfgfile, 'r') data = f.read().strip() f.close() d = eval(data or '{}', {}, {}) except: print('Invalid config file format', file=sys.stderr) else: d = d.get(gethostname(), {}) namemsg = '' for id, local in list(self.playing.items()): keyid = 'player%d' % id if local == 'l' and keyid in d: namemsg = namemsg + message( CMSG_PLAYER_NAME, id, d[keyid]) if namemsg: self.s.sendall(namemsg) def msg_ping(self, *rest): self.s.sendall(message(CMSG_PONG, *rest)) self.checkcfgfile() if rest and self.udpsock_low is not None: udpkbytes = rest[0] if not udpkbytes: return #inp = self.udpbytecounter / (udpkbytes*1024.0) #print "(%d%% packet loss)" % int(100*(1.0-inp)) if (udpkbytes<<10) * UDP_EXPECTED_RATIO > self.udpbytecounter: # too many packets were dropped (including, maybe, all of them) self.udpsock_low += 1 if self.udpsock_low >= 3 and self.initlevel >= 1: # third time now -- that's too much print("Note: routing UDP traffic over TCP", end=' ') inp = self.udpbytecounter / (udpkbytes*1024.0) print("(%d%% packet loss)" % int(100*(1.0-inp))) self.start_udp_over_tcp() self.s.sendall(message(CMSG_UDP_PORT, MSG_INLINE_FRAME)) else: # enough packets received self.udpsock_low = 0 def msg_pong(self, *rest): if self.initlevel == 0: self.startplaying() self.initlevel = 1 elif self.initlevel == 1: if self.snd and self.snd.has_music: self.s.sendall(message(CMSG_ENABLE_MUSIC, 2)) self.initlevel = 2 if not self.taskbarfree and not self.taskbarmode: self.taskbarfree = 1 self.settaskbar(1) def msg_inline_frame(self, data, *rest): if self.pending_udp_data is not None: self.pending_udp_data = self.udp_over_tcp_decompress(data) MESSAGES = { MSG_BROADCAST_PORT:msg_broadcast_port, MSG_DEF_PLAYFIELD: msg_def_playfield, MSG_DEF_KEY : msg_def_key, MSG_DEF_ICON : msg_def_icon, MSG_DEF_BITMAP : msg_def_bitmap, MSG_DEF_SAMPLE : msg_def_sample, MSG_PLAY_MUSIC : msg_play_music, MSG_FADEOUT : msg_fadeout, MSG_PLAYER_JOIN : msg_player_join, MSG_PLAYER_KILL : msg_player_kill, MSG_PLAYER_ICON : msg_player_icon, MSG_PING : msg_ping, MSG_PONG : msg_pong, MSG_INLINE_FRAME : msg_inline_frame, MSG_PATCH_FILE : msg_patch_file, MSG_ZPATCH_FILE : msg_zpatch_file, MSG_MD5_FILE : msg_md5_file, ## MSG_LOAD_PREFIX : msg_load_prefix, } def run(s, sockaddr, *args, **kw): try: import psyco except ImportError: pass else: psyco.bind(Playfield.update_sprites) Playfield(s, sockaddr).run(*args, **kw)