diff --git a/server.py b/server.py index df8e596d9..d5ea7f1ad 100644 --- a/server.py +++ b/server.py @@ -1,7 +1,7 @@ from math import floor from world import World -import Queue -import SocketServer +import queue +import socketserver import datetime import random import re @@ -56,18 +56,22 @@ except ImportError: pass + def log(*args): now = datetime.datetime.utcnow() line = ' '.join(map(str, (now,) + args)) - print line + print(line) with open(LOG_PATH, 'a') as fp: fp.write('%s\n' % line) + def chunked(x): return int(floor(round(x) / CHUNK_SIZE)) + def packet(*args): - return '%s\n' % ','.join(map(str, args)) + return ('%s\n' % ','.join(map(str, args))).encode("UTF-8") + class RateLimiter(object): def __init__(self, rate, per): @@ -75,6 +79,7 @@ def __init__(self, rate, per): self.per = float(per) self.allowance = self.rate self.last_check = time.time() + def tick(self): if not RATE_LIMIT: return False @@ -85,16 +90,18 @@ def tick(self): if self.allowance > self.rate: self.allowance = self.rate if self.allowance < 1: - return True # too fast + return True # too fast else: self.allowance -= 1 - return False # okay + return False # okay + -class Server(SocketServer.ThreadingMixIn, SocketServer.TCPServer): +class Server(socketserver.ThreadingMixIn, socketserver.TCPServer): allow_reuse_address = True daemon_threads = True -class Handler(SocketServer.BaseRequestHandler): + +class Handler(socketserver.BaseRequestHandler): def setup(self): self.position_limiter = RateLimiter(100, 5) self.limiter = RateLimiter(1000, 10) @@ -102,26 +109,29 @@ def setup(self): self.client_id = None self.user_id = None self.nick = None - self.queue = Queue.Queue() + self.queue = queue.Queue() self.running = True self.start() + def handle(self): model = self.server.model model.enqueue(model.on_connect, self) try: - buf = [] + buf = b'' while True: data = self.request.recv(BUFFER_SIZE) if not data: break - buf.extend(data.replace('\r\n', '\n')) - while '\n' in buf: - index = buf.index('\n') - line = ''.join(buf[:index]) + buf += data.replace(b'\r\n', b'\n') + while b'\n' in buf: + index = buf.index(b'\n') + line = buf[:index] buf = buf[index + 1:] if not line: continue - if line[0] == POSITION: + + command = line.decode("utf-8") + if command[0] == POSITION: if self.position_limiter.tick(): log('RATE', self.client_id) self.stop() @@ -131,46 +141,52 @@ def handle(self): log('RATE', self.client_id) self.stop() return - model.enqueue(model.on_data, self, line) + model.enqueue(model.on_data, self, command) finally: model.enqueue(model.on_disconnect, self) + def finish(self): self.running = False + def stop(self): self.request.close() + def start(self): thread = threading.Thread(target=self.run) thread.setDaemon(True) thread.start() + def run(self): while self.running: try: - buf = [] + buf = b'' try: - buf.append(self.queue.get(timeout=5)) + buf += self.queue.get(timeout=5) try: while True: - buf.append(self.queue.get(False)) - except Queue.Empty: + buf += self.queue.get_nowait() + except queue.Empty: pass - except Queue.Empty: + except queue.Empty: continue - data = ''.join(buf) - self.request.sendall(data) + self.request.sendall(buf) except Exception: self.request.close() raise + def send_raw(self, data): if data: self.queue.put(data) + def send(self, *args): self.send_raw(packet(*args)) + class Model(object): def __init__(self, seed): self.world = World(seed) self.clients = [] - self.queue = Queue.Queue() + self.queue = queue.Queue() self.commands = { AUTHENTICATE: self.on_authenticate, CHUNK: self.on_chunk, @@ -189,10 +205,12 @@ def __init__(self, seed): (re.compile(r'^/help(?:\s+(\S+))?$'), self.on_help), (re.compile(r'^/list$'), self.on_list), ] + def start(self): thread = threading.Thread(target=self.run) thread.setDaemon(True) thread.start() + def run(self): self.connection = sqlite3.connect(DB_PATH) self.create_tables() @@ -204,19 +222,24 @@ def run(self): self.dequeue() except Exception: traceback.print_exc() + def enqueue(self, func, *args, **kwargs): self.queue.put((func, args, kwargs)) + def dequeue(self): try: func, args, kwargs = self.queue.get(timeout=5) func(*args, **kwargs) - except Queue.Empty: + except queue.Empty: pass + def execute(self, *args, **kwargs): return self.connection.execute(*args, **kwargs) + def commit(self): self.last_commit = time.time() self.connection.commit() + def create_tables(self): queries = [ 'create table if not exists block (' @@ -262,10 +285,12 @@ def create_tables(self): ] for query in queries: self.execute(query) + def get_default_block(self, x, y, z): p, q = chunked(x), chunked(z) chunk = self.world.get_chunk(p, q) return chunk.get((x, y, z), 0) + def get_block(self, x, y, z): query = ( 'select w from block where ' @@ -276,12 +301,14 @@ def get_block(self, x, y, z): if rows: return rows[0][0] return self.get_default_block(x, y, z) + def next_client_id(self): result = 1 client_ids = set(x.client_id for x in self.clients) while result in client_ids: result += 1 return result + def on_connect(self, client): client.client_id = self.next_client_id() client.nick = 'guest%d' % client.client_id @@ -296,18 +323,21 @@ def on_connect(self, client): self.send_positions(client) self.send_nick(client) self.send_nicks(client) - def on_data(self, client, data): + + def on_data(self, client, data: str): #log('RECV', client.client_id, data) args = data.split(',') command, args = args[0], args[1:] if command in self.commands: func = self.commands[command] func(client, *args) + def on_disconnect(self, client): log('DISC', client.client_id, *client.client_address) self.clients.remove(client) self.send_disconnect(client) self.send_talk('%s has disconnected from the server.' % client.nick) + def on_version(self, client, version): if client.version is not None: return @@ -317,7 +347,9 @@ def on_version(self, client, version): return client.version = version # TODO: client.start() here + def on_authenticate(self, client, username, access_token): + print(client, username, access_token) user_id = None if username and access_token: payload = { @@ -336,8 +368,9 @@ def on_authenticate(self, client, username, access_token): self.send_nick(client) # TODO: has left message if was already authenticated self.send_talk('%s has joined the game.' % client.nick) + def on_chunk(self, client, p, q, key=0): - packets = [] + packets = b'' p, q, key = map(int, (p, q, key)) query = ( 'select rowid, x, y, z, w from block where ' @@ -348,7 +381,7 @@ def on_chunk(self, client, p, q, key=0): blocks = 0 for rowid, x, y, z, w in rows: blocks += 1 - packets.append(packet(BLOCK, p, q, x, y, z, w)) + packets += packet(BLOCK, p, q, x, y, z, w) max_rowid = max(max_rowid, rowid) query = ( 'select x, y, z, w from light where ' @@ -358,7 +391,7 @@ def on_chunk(self, client, p, q, key=0): lights = 0 for x, y, z, w in rows: lights += 1 - packets.append(packet(LIGHT, p, q, x, y, z, w)) + packets += packet(LIGHT, p, q, x, y, z, w) query = ( 'select x, y, z, face, text from sign where ' 'p = :p and q = :q;' @@ -367,13 +400,14 @@ def on_chunk(self, client, p, q, key=0): signs = 0 for x, y, z, face, text in rows: signs += 1 - packets.append(packet(SIGN, p, q, x, y, z, face, text)) + packets += packet(SIGN, p, q, x, y, z, face, text) if blocks: - packets.append(packet(KEY, p, q, max_rowid)) + packets += packet(KEY, p, q, max_rowid) if blocks or lights or signs: - packets.append(packet(REDRAW, p, q)) - packets.append(packet(CHUNK, p, q)) - client.send_raw(''.join(packets)) + packets += packet(REDRAW, p, q) + packets += packet(CHUNK, p, q) + client.send_raw(packets) + def on_block(self, client, x, y, z, w): x, y, z, w = map(int, (x, y, z, w)) p, q = chunked(x), chunked(z) @@ -402,7 +436,7 @@ def on_block(self, client, x, y, z, w): ) if RECORD_HISTORY: self.execute(query, dict(timestamp=time.time(), - user_id=client.user_id, x=x, y=y, z=z, w=w)) + user_id=client.user_id, x=x, y=y, z=z, w=w)) query = ( 'insert or replace into block (p, q, x, y, z, w) ' 'values (:p, :q, :x, :y, :z, :w);' @@ -431,6 +465,7 @@ def on_block(self, client, x, y, z, w): 'x = :x and y = :y and z = :z;' ) self.execute(query, dict(x=x, y=y, z=z)) + def on_light(self, client, x, y, z, w): x, y, z, w = map(int, (x, y, z, w)) p, q = chunked(x), chunked(z) @@ -453,6 +488,7 @@ def on_light(self, client, x, y, z, w): ) self.execute(query, dict(p=p, q=q, x=x, y=y, z=z, w=w)) self.send_light(client, p, q, x, y, z, w) + def on_sign(self, client, x, y, z, face, *args): if AUTH_REQUIRED and client.user_id is None: client.send(TALK, 'Only logged in users are allowed to build.') @@ -472,7 +508,7 @@ def on_sign(self, client, x, y, z, face, *args): 'values (:p, :q, :x, :y, :z, :face, :text);' ) self.execute(query, - dict(p=p, q=q, x=x, y=y, z=z, face=face, text=text)) + dict(p=p, q=q, x=x, y=y, z=z, face=face, text=text)) else: query = ( 'delete from sign where ' @@ -480,10 +516,12 @@ def on_sign(self, client, x, y, z, face, *args): ) self.execute(query, dict(x=x, y=y, z=z, face=face)) self.send_sign(client, p, q, x, y, z, face, text) + def on_position(self, client, x, y, z, rx, ry): x, y, z, rx, ry = map(float, (x, y, z, rx, ry)) client.position = (x, y, z, rx, ry) self.send_position(client) + def on_talk(self, client, *args): text = ','.join(args) if text.startswith('/'): @@ -505,6 +543,7 @@ def on_talk(self, client, *args): client.send(TALK, 'Unrecognized nick: "%s"' % nick) else: self.send_talk('%s> %s' % (client.nick, text)) + def on_nick(self, client, nick=None): if AUTH_REQUIRED: client.send(TALK, 'You cannot change your nick on this server.') @@ -515,10 +554,12 @@ def on_nick(self, client, nick=None): self.send_talk('%s is now known as %s' % (client.nick, nick)) client.nick = nick self.send_nick(client) + def on_spawn(self, client): client.position = SPAWN_POINT client.send(YOU, client.client_id, *client.position) self.send_position(client) + def on_goto(self, client, nick=None): if nick is None: clients = [x for x in self.clients if x != client] @@ -530,6 +571,7 @@ def on_goto(self, client, nick=None): client.position = other.position client.send(YOU, client.client_id, *client.position) self.send_position(client) + def on_pq(self, client, p, q): p, q = map(int, (p, q)) if abs(p) > 1000 or abs(q) > 1000: @@ -537,32 +579,39 @@ def on_pq(self, client, p, q): client.position = (p * CHUNK_SIZE, 0, q * CHUNK_SIZE, 0, 0) client.send(YOU, client.client_id, *client.position) self.send_position(client) + def on_help(self, client, topic=None): if topic is None: client.send(TALK, 'Type "t" to chat. Type "/" to type commands:') - client.send(TALK, '/goto [NAME], /help [TOPIC], /list, /login NAME, /logout, /nick') - client.send(TALK, '/offline [FILE], /online HOST [PORT], /pq P Q, /spawn, /view N') + client.send( + TALK, '/goto [NAME], /help [TOPIC], /list, /login NAME, /logout, /nick') + client.send( + TALK, '/offline [FILE], /online HOST [PORT], /pq P Q, /spawn, /view N') return topic = topic.lower().strip() if topic == 'goto': client.send(TALK, 'Help: /goto [NAME]') client.send(TALK, 'Teleport to another user.') - client.send(TALK, 'If NAME is unspecified, a random user is chosen.') + client.send( + TALK, 'If NAME is unspecified, a random user is chosen.') elif topic == 'list': client.send(TALK, 'Help: /list') client.send(TALK, 'Display a list of connected users.') elif topic == 'login': client.send(TALK, 'Help: /login NAME') client.send(TALK, 'Switch to another registered username.') - client.send(TALK, 'The login server will be re-contacted. The username is case-sensitive.') + client.send( + TALK, 'The login server will be re-contacted. The username is case-sensitive.') elif topic == 'logout': client.send(TALK, 'Help: /logout') client.send(TALK, 'Unauthenticate and become a guest user.') - client.send(TALK, 'Automatic logins will not occur again until the /login command is re-issued.') + client.send( + TALK, 'Automatic logins will not occur again until the /login command is re-issued.') elif topic == 'offline': client.send(TALK, 'Help: /offline [FILE]') client.send(TALK, 'Switch to offline mode.') - client.send(TALK, 'FILE specifies the save file to use and defaults to "craft".') + client.send( + TALK, 'FILE specifies the save file to use and defaults to "craft".') elif topic == 'online': client.send(TALK, 'Help: /online HOST [PORT]') client.send(TALK, 'Connect to the specified server.') @@ -578,54 +627,65 @@ def on_help(self, client, topic=None): elif topic == 'view': client.send(TALK, 'Help: /view N') client.send(TALK, 'Set viewing distance, 1 - 24.') + def on_list(self, client): client.send(TALK, - 'Players: %s' % ', '.join(x.nick for x in self.clients)) + 'Players: %s' % ', '.join(x.nick for x in self.clients)) + def send_positions(self, client): for other in self.clients: if other == client: continue client.send(POSITION, other.client_id, *other.position) + def send_position(self, client): for other in self.clients: if other == client: continue other.send(POSITION, client.client_id, *client.position) + def send_nicks(self, client): for other in self.clients: if other == client: continue client.send(NICK, other.client_id, other.nick) + def send_nick(self, client): for other in self.clients: other.send(NICK, client.client_id, client.nick) + def send_disconnect(self, client): for other in self.clients: if other == client: continue other.send(DISCONNECT, client.client_id) + def send_block(self, client, p, q, x, y, z, w): for other in self.clients: if other == client: continue other.send(BLOCK, p, q, x, y, z, w) other.send(REDRAW, p, q) + def send_light(self, client, p, q, x, y, z, w): for other in self.clients: if other == client: continue other.send(LIGHT, p, q, x, y, z, w) other.send(REDRAW, p, q) + def send_sign(self, client, p, q, x, y, z, face, text): for other in self.clients: if other == client: continue other.send(SIGN, p, q, x, y, z, face, text) + def send_talk(self, text): log(text) for client in self.clients: client.send(TALK, text) + def cleanup(): world = World(None) conn = sqlite3.connect(DB_PATH) @@ -636,7 +696,7 @@ def cleanup(): count = 0 total = 0 delete_query = 'delete from block where x = %d and y = %d and z = %d;' - print 'begin;' + print('begin;') for p, q in chunks: chunk = world.create_chunk(p, q) query = 'select x, y, z, w from block where p = :p and q = :q;' @@ -650,11 +710,12 @@ def cleanup(): original = chunk.get((x, y, z), 0) if w == original or original in INDESTRUCTIBLE_ITEMS: count += 1 - print delete_query % (x, y, z) + print(delete_query % (x, y, z)) conn.close() - print 'commit;' + print('commit;') print >> sys.stderr, '%d of %d blocks will be cleaned up' % (count, total) + def main(): if len(sys.argv) == 2 and sys.argv[1] == 'cleanup': cleanup() @@ -671,5 +732,6 @@ def main(): server.model = model server.serve_forever() + if __name__ == '__main__': main() diff --git a/world.py b/world.py index 2426b5dcd..37c6976b4 100644 --- a/world.py +++ b/world.py @@ -8,35 +8,47 @@ WORLD_FUNC = CFUNCTYPE(None, c_int, c_int, c_int, c_int, c_void_p) + def dll_seed(x): dll.seed(x) + def dll_create_world(p, q): result = {} + def world_func(x, y, z, w, arg): result[(x, y, z)] = w dll.create_world(p, q, WORLD_FUNC(world_func), None) return result + dll.simplex2.restype = c_float dll.simplex2.argtypes = [c_float, c_float, c_int, c_float, c_float] + + def dll_simplex2(x, y, octaves=1, persistence=0.5, lacunarity=2.0): return dll.simplex2(x, y, octaves, persistence, lacunarity) + dll.simplex3.restype = c_float dll.simplex3.argtypes = [c_float, c_float, c_float, c_int, c_float, c_float] + + def dll_simplex3(x, y, z, octaves=1, persistence=0.5, lacunarity=2.0): return dll.simplex3(x, y, z, octaves, persistence, lacunarity) + class World(object): def __init__(self, seed=None, cache_size=64): self.seed = seed self.cache = OrderedDict() self.cache_size = cache_size + def create_chunk(self, p, q): if self.seed is not None: dll_seed(self.seed) return dll_create_world(p, q) + def get_chunk(self, p, q): try: chunk = self.cache.pop((p, q))