mirror of
https://github.com/abakh/nbsdgames.git
synced 2025-02-02 15:07:27 -05:00
863 lines
32 KiB
Python
863 lines
32 KiB
Python
#! /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
|
|
from . import modes
|
|
from .modes import KeyPressed, KeyReleased
|
|
from . 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
|
|
|
|
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 = ""
|
|
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 '\n' in tail:
|
|
break
|
|
n = tail.index('\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, '*'))
|
|
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()
|
|
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] == '\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:
|
|
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('.', 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(''.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 '\x80' <= udpdata[0] < '\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)
|
|
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)
|