268 lines
8.1 KiB
Python
Executable file
268 lines
8.1 KiB
Python
Executable file
#coding: utf8
|
|
|
|
__version__ = '0.6'
|
|
|
|
import time
|
|
from gevent import spawn, sleep as gsleep
|
|
from gevent.socket import socket
|
|
from gevent.coros import Semaphore, RLock
|
|
from gevent.queue import Queue
|
|
from collections import deque
|
|
import pygame
|
|
import cairo
|
|
import math
|
|
import random
|
|
import array
|
|
import os
|
|
import os.path
|
|
|
|
import logging
|
|
log = logging.getLogger('pixelflut')
|
|
|
|
async = spawn
|
|
|
|
class Client(object):
|
|
px_per_tick = 100
|
|
|
|
def __init__(self, canvas):
|
|
self.canvas = canvas
|
|
self.socket = None
|
|
self.connect_ts = time.time()
|
|
# This buffer discards all but the newest 1024 messages
|
|
self.sendbuffer = deque([], 1024)
|
|
# And this is used to limit clients to X messages per tick
|
|
# We start at 0 (instead of x) to add a reconnect-penalty.
|
|
self.limit = Semaphore(0)
|
|
self.lock = RLock()
|
|
|
|
def send(self, line):
|
|
self.sendbuffer.append(line.strip() + '\n')
|
|
|
|
def nospam(self, line):
|
|
if not self.sendbuffer:
|
|
self.sendbuffer.append(line.strip() + '\n')
|
|
|
|
def disconnect(self):
|
|
with self.lock:
|
|
if self.socket:
|
|
socket = self.socket
|
|
self.socket = None
|
|
socket.close()
|
|
log.info('Disconnect')
|
|
|
|
def serve(self, socket):
|
|
with self.lock:
|
|
self.socket = socket
|
|
sendall = self.socket.sendall
|
|
readline = self.socket.makefile().readline
|
|
|
|
try:
|
|
while self.socket:
|
|
self.limit.acquire()
|
|
# Idea: Send first, receive later. If the client is to
|
|
# slow to get the send-buffer empty, he cannot send.
|
|
while self.sendbuffer:
|
|
sendall(self.sendbuffer.popleft())
|
|
line = readline().strip()
|
|
if not line:
|
|
break
|
|
arguments = line.split()
|
|
command = arguments.pop(0)
|
|
try:
|
|
self.canvas.fire('COMMAND-%s' % command.upper(), self, *arguments)
|
|
except Exception, e:
|
|
socket.send('ERROR %r :(' % e)
|
|
break
|
|
finally:
|
|
self.disconnect()
|
|
|
|
def tick(self):
|
|
while self.limit.counter <= self.px_per_tick:
|
|
self.limit.release()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Canvas(object):
|
|
size = 640, 480
|
|
flags = pygame.RESIZABLE#|pygame.FULLSCREEN
|
|
|
|
def __init__(self):
|
|
pygame.init()
|
|
pygame.mixer.quit()
|
|
self.set_title()
|
|
self.screen = pygame.display.set_mode(self.size, self.flags)
|
|
self.ticks = 0
|
|
self.width = self.screen.get_width()
|
|
self.height = self.screen.get_height()
|
|
self.clients = {}
|
|
self.events = {}
|
|
self.font = pygame.font.Font(None, 17)
|
|
|
|
def serve(self, host, port):
|
|
self.host = host
|
|
self.port = port
|
|
|
|
spawn(self._loop)
|
|
|
|
self.socket = socket()
|
|
self.socket.bind((host, port))
|
|
self.socket.listen(500)
|
|
while True:
|
|
sock, addr = self.socket.accept()
|
|
ip, port = addr
|
|
|
|
log.info('Connect %s:%d', ip, port)
|
|
|
|
client = self.clients.get(ip)
|
|
if not client:
|
|
client = self.clients[ip] = Client(self)
|
|
|
|
client.disconnect()
|
|
spawn(self.handle_client, client, sock)
|
|
|
|
def handle_client(self, client, sock):
|
|
try:
|
|
self.fire('CONNECT', client)
|
|
client.serve(sock) # This blocks until ready
|
|
self.fire('DISCONNECT', client)
|
|
finally:
|
|
client.disconnect()
|
|
|
|
def _loop(self):
|
|
while True:
|
|
gsleep(0.01) # Required to allow other tasks to run
|
|
for e in pygame.event.get():
|
|
if e.type == pygame.VIDEORESIZE:
|
|
old = self.screen.copy()
|
|
self.screen = pygame.display.set_mode(e.size, self.flags)
|
|
self.screen.blit(old, (0,0))
|
|
self.width, self.height = e.size
|
|
self.fire('RESIZE')
|
|
elif e.type == pygame.QUIT:
|
|
self.fire('QUIT')
|
|
return
|
|
elif e.type == pygame.KEYDOWN:
|
|
self.fire('KEYDOWN-' + e.unicode)
|
|
self.ticks += 1
|
|
self.fire('TICK', self.ticks)
|
|
for c in self.clients.values():
|
|
c.tick()
|
|
pygame.display.flip()
|
|
|
|
def on(self, name):
|
|
''' If used as a decorator, binds a function to an event. '''
|
|
def decorator(func):
|
|
self.events[name] = func
|
|
return func
|
|
return decorator
|
|
|
|
def fire(self, name, *a, **ka):
|
|
''' Fire an event. '''
|
|
if name in self.events:
|
|
try:
|
|
self.events[name](self, *a, **ka)
|
|
except:
|
|
log.exception('Error in callback for %r', name)
|
|
|
|
def get_size(self):
|
|
''' Get the current screen dimension as a (width, height) tuple.'''
|
|
return self.width, self.height
|
|
|
|
def get_pixel(self, x, y):
|
|
''' Get colour of a pixel as an (r,g,b) tuple. '''
|
|
return self.screen.get_at((x,y))
|
|
|
|
def set_pixel(self, x, y, r, g, b, a=255):
|
|
''' Change the colour of a pixel. If an alpha value is given, the new
|
|
colour is mixed with the old colour accordingly. '''
|
|
if a == 0: return
|
|
screen = self.screen
|
|
if a == 0xff:
|
|
screen.set_at((x, y), (r,g,b))
|
|
elif 0 <= x < self.width and 0 <= y < self.height:
|
|
r2, g2, b2, a2 = screen.get_at((x, y))
|
|
r = (r2*(0xff-a)+(r*a)) / 0xff
|
|
g = (g2*(0xff-a)+(g*a)) / 0xff
|
|
b = (b2*(0xff-a)+(b*a)) / 0xff
|
|
screen.set_at((x, y), (r,g,b))
|
|
|
|
def clear(self, r=0, g=0, b=0, a=255):
|
|
''' Fill the entire screen with a solid colour (default: black)'''
|
|
self.screen.fill((r, g, b))
|
|
|
|
def save_as(self, filename):
|
|
''' Save screen to disk. '''
|
|
pygame.image.save(self.screen, filename)
|
|
|
|
def load_from(self, filename):
|
|
img = pygame.image.load(filename).convert()
|
|
self.screen.blit(img, (0,0))
|
|
|
|
def load_font(self, fname):
|
|
''' Load a font image with 16x16 sprites. '''
|
|
self.font_img = pygame.image.load(fname).convert()
|
|
self.font_res = int(self.font_img.get_width())/16
|
|
|
|
def set_title(self, text=None):
|
|
title = 'P1XELFLUT'
|
|
if text:
|
|
title += ' ' + text
|
|
pygame.display.set_caption(title)
|
|
|
|
def putc(self, x, y, c):
|
|
if not self.font_img:
|
|
self.load_font('font.png')
|
|
fx = (c%16) * self.font_res
|
|
fy = (c/16) * self.font_res
|
|
self.screen.blit(self.font_img, (x,y),
|
|
(fx,fy,self.font_res,self.font_res))
|
|
|
|
def text(self, x, y, text, delay=0, linespace=1):
|
|
for i, line in enumerate(text.splitlines()):
|
|
line += ' '
|
|
for j, c in enumerate(line):
|
|
self.putc(x+j*self.font_res, y+i*self.font_res, ord(c))
|
|
gsleep(delay)
|
|
y += linespace
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
|
|
import optparse
|
|
parser = optparse.OptionParser("usage: %prog [options] brain_script")
|
|
parser.add_option("-H", "--host", dest="hostname",
|
|
default="0.0.0.0", type="string",
|
|
help="specify hostname to run on")
|
|
parser.add_option("-p", "--port", dest="portnum", default=1234,
|
|
type="int", help="port number to run on")
|
|
|
|
(options, args) = parser.parse_args()
|
|
if len(args) != 1:
|
|
parser.error("incorrect number of arguments")
|
|
|
|
canvas = Canvas()
|
|
task = spawn(canvas.serve, options.hostname, options.portnum)
|
|
|
|
brainfile = args[0]
|
|
mtime = 0
|
|
|
|
while True:
|
|
gsleep(1)
|
|
if mtime < os.stat(brainfile).st_mtime:
|
|
canvas.fire('UNLOAD')
|
|
canvas.events.clear()
|
|
try:
|
|
execfile(brainfile, {'on':canvas.on, '__file__': brainfile})
|
|
except:
|
|
log.exception('Brain failed')
|
|
continue
|
|
canvas.fire('LOAD')
|
|
mtime = os.stat(brainfile).st_mtime
|
|
|
|
task.join()
|
|
|