1#!/usr/bin/env python2.7
2from math import floor
3from craft_world import World
4import Queue
5import SocketServer
6import datetime
7import random
8import re
9import requests
10import sqlite3
11import sys
12import threading
13import time
14import traceback
15
16DEFAULT_HOST = '0.0.0.0'
17DEFAULT_PORT = 4080
18
19DB_PATH = 'craft.db'
20LOG_PATH = 'log.txt'
21
22CHUNK_SIZE = 32
23BUFFER_SIZE = 4096
24COMMIT_INTERVAL = 5
25
26DAY_LENGTH = 600
27SPAWN_POINT = (0, 0, 0, 0, 0)
28RATE_LIMIT = False
29RECORD_HISTORY = False
30INDESTRUCTIBLE_ITEMS = set([16])
31ALLOWED_ITEMS = set([
32    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
33    17, 18, 19, 20, 21, 22, 23,
34    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
35    48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63])
36
37AUTHENTICATE = 'A'
38BLOCK = 'B'
39CHUNK = 'C'
40DISCONNECT = 'D'
41KEY = 'K'
42LIGHT = 'L'
43NICK = 'N'
44POSITION = 'P'
45REDRAW = 'R'
46SIGN = 'S'
47TALK = 'T'
48TIME = 'E'
49VERSION = 'V'
50YOU = 'U'
51
52try:
53    from config import *
54except ImportError:
55    pass
56
57def log(*args):
58    now = datetime.datetime.utcnow()
59    line = ' '.join(map(str, (now,) + args))
60    print line
61    with open(LOG_PATH, 'a') as fp:
62        fp.write('%s\n' % line)
63
64def chunked(x):
65    return int(floor(round(x) / CHUNK_SIZE))
66
67def packet(*args):
68    return '%s\n' % ','.join(map(str, args))
69
70class RateLimiter(object):
71    def __init__(self, rate, per):
72        self.rate = float(rate)
73        self.per = float(per)
74        self.allowance = self.rate
75        self.last_check = time.time()
76    def tick(self):
77        if not RATE_LIMIT:
78            return False
79        now = time.time()
80        elapsed = now - self.last_check
81        self.last_check = now
82        self.allowance += elapsed * (self.rate / self.per)
83        if self.allowance > self.rate:
84            self.allowance = self.rate
85        if self.allowance < 1:
86            return True # too fast
87        else:
88            self.allowance -= 1
89            return False # okay
90
91class Server(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
92    allow_reuse_address = True
93    daemon_threads = True
94
95class Handler(SocketServer.BaseRequestHandler):
96    def setup(self):
97        self.position_limiter = RateLimiter(100, 5)
98        self.limiter = RateLimiter(1000, 10)
99        self.version = None
100        self.client_id = None
101        self.user_id = None
102        self.nick = None
103        self.queue = Queue.Queue()
104        self.running = True
105        self.start()
106    def handle(self):
107        model = self.server.model
108        model.enqueue(model.on_connect, self)
109        try:
110            buf = []
111            while True:
112                data = self.request.recv(BUFFER_SIZE)
113                if not data:
114                    break
115                buf.extend(data.replace('\r\n', '\n'))
116                while '\n' in buf:
117                    index = buf.index('\n')
118                    line = ''.join(buf[:index])
119                    buf = buf[index + 1:]
120                    if not line:
121                        continue
122                    if line[0] == POSITION:
123                        if self.position_limiter.tick():
124                            log('RATE', self.client_id)
125                            self.stop()
126                            return
127                    else:
128                        if self.limiter.tick():
129                            log('RATE', self.client_id)
130                            self.stop()
131                            return
132                    model.enqueue(model.on_data, self, line)
133        finally:
134            model.enqueue(model.on_disconnect, self)
135    def finish(self):
136        self.running = False
137    def stop(self):
138        self.request.close()
139    def start(self):
140        thread = threading.Thread(target=self.run)
141        thread.setDaemon(True)
142        thread.start()
143    def run(self):
144        while self.running:
145            try:
146                buf = []
147                try:
148                    buf.append(self.queue.get(timeout=5))
149                    try:
150                        while True:
151                            buf.append(self.queue.get(False))
152                    except Queue.Empty:
153                        pass
154                except Queue.Empty:
155                    continue
156                data = ''.join(buf)
157                self.request.sendall(data)
158            except Exception:
159                self.request.close()
160                raise
161    def send_raw(self, data):
162        if data:
163            self.queue.put(data)
164    def send(self, *args):
165        self.send_raw(packet(*args))
166
167class Model(object):
168    def __init__(self, seed):
169        self.world = World(seed)
170        self.clients = []
171        self.queue = Queue.Queue()
172        self.commands = {
173            AUTHENTICATE: self.on_authenticate,
174            CHUNK: self.on_chunk,
175            BLOCK: self.on_block,
176            LIGHT: self.on_light,
177            POSITION: self.on_position,
178            TALK: self.on_talk,
179            SIGN: self.on_sign,
180            VERSION: self.on_version,
181        }
182        self.patterns = [
183            (re.compile(r'^/spawn$'), self.on_spawn),
184            (re.compile(r'^/goto(?:\s+(\S+))?$'), self.on_goto),
185            (re.compile(r'^/pq\s+(-?[0-9]+)\s*,?\s*(-?[0-9]+)$'), self.on_pq),
186            (re.compile(r'^/help(?:\s+(\S+))?$'), self.on_help),
187            (re.compile(r'^/list$'), self.on_list),
188        ]
189    def start(self):
190        thread = threading.Thread(target=self.run)
191        thread.setDaemon(True)
192        thread.start()
193    def run(self):
194        self.connection = sqlite3.connect(DB_PATH)
195        self.create_tables()
196        self.commit()
197        while True:
198            try:
199                if time.time() - self.last_commit > COMMIT_INTERVAL:
200                    self.commit()
201                self.dequeue()
202            except Exception:
203                traceback.print_exc()
204    def enqueue(self, func, *args, **kwargs):
205        self.queue.put((func, args, kwargs))
206    def dequeue(self):
207        try:
208            func, args, kwargs = self.queue.get(timeout=5)
209            func(*args, **kwargs)
210        except Queue.Empty:
211            pass
212    def execute(self, *args, **kwargs):
213        return self.connection.execute(*args, **kwargs)
214    def commit(self):
215        self.last_commit = time.time()
216        self.connection.commit()
217    def create_tables(self):
218        queries = [
219            'create table if not exists block ('
220            '    p int not null,'
221            '    q int not null,'
222            '    x int not null,'
223            '    y int not null,'
224            '    z int not null,'
225            '    w int not null'
226            ');',
227            'create unique index if not exists block_pqxyz_idx on '
228            '    block (p, q, x, y, z);',
229            'create table if not exists light ('
230            '    p int not null,'
231            '    q int not null,'
232            '    x int not null,'
233            '    y int not null,'
234            '    z int not null,'
235            '    w int not null'
236            ');',
237            'create unique index if not exists light_pqxyz_idx on '
238            '    light (p, q, x, y, z);',
239            'create table if not exists sign ('
240            '    p int not null,'
241            '    q int not null,'
242            '    x int not null,'
243            '    y int not null,'
244            '    z int not null,'
245            '    face int not null,'
246            '    text text not null'
247            ');',
248            'create index if not exists sign_pq_idx on sign (p, q);',
249            'create unique index if not exists sign_xyzface_idx on '
250            '    sign (x, y, z, face);',
251            'create table if not exists block_history ('
252            '   timestamp real not null,'
253            '   user_id int not null,'
254            '   x int not null,'
255            '   y int not null,'
256            '   z int not null,'
257            '   w int not null'
258            ');',
259        ]
260        for query in queries:
261            self.execute(query)
262    def get_default_block(self, x, y, z):
263        p, q = chunked(x), chunked(z)
264        chunk = self.world.get_chunk(p, q)
265        return chunk.get((x, y, z), 0)
266    def get_block(self, x, y, z):
267        query = (
268            'select w from block where '
269            'p = :p and q = :q and x = :x and y = :y and z = :z;'
270        )
271        p, q = chunked(x), chunked(z)
272        rows = list(self.execute(query, dict(p=p, q=q, x=x, y=y, z=z)))
273        if rows:
274            return rows[0][0]
275        return self.get_default_block(x, y, z)
276    def next_client_id(self):
277        result = 1
278        client_ids = set(x.client_id for x in self.clients)
279        while result in client_ids:
280            result += 1
281        return result
282    def on_connect(self, client):
283        client.client_id = self.next_client_id()
284        client.nick = 'guest%d' % client.client_id
285        log('CONN', client.client_id, *client.client_address)
286        client.position = SPAWN_POINT
287        self.clients.append(client)
288        client.send(YOU, client.client_id, *client.position)
289        client.send(TIME, time.time(), DAY_LENGTH)
290        client.send(TALK, 'Welcome to Craft!')
291        client.send(TALK, 'Type "/help" for a list of commands.')
292        self.send_position(client)
293        self.send_positions(client)
294        self.send_nick(client)
295        self.send_nicks(client)
296    def on_data(self, client, data):
297        #log('RECV', client.client_id, data)
298        args = data.split(',')
299        command, args = args[0], args[1:]
300        if command in self.commands:
301            func = self.commands[command]
302            func(client, *args)
303    def on_disconnect(self, client):
304        log('DISC', client.client_id, *client.client_address)
305        self.clients.remove(client)
306        self.send_disconnect(client)
307        self.send_talk('%s has disconnected from the server.' % client.nick)
308    def on_version(self, client, version):
309        if client.version is not None:
310            return
311        version = int(version)
312        if version != 1:
313            client.stop()
314            return
315        client.version = version
316        # TODO: client.start() here
317    def on_authenticate(self, client, username, access_token):
318        user_id = None
319        if username and access_token:
320            url = 'https://craft.michaelfogleman.com/api/1/access'
321            payload = {
322                'username': username,
323                'access_token': access_token,
324            }
325            response = requests.post(url, data=payload)
326            if response.status_code == 200 and response.text.isdigit():
327                user_id = int(response.text)
328        client.user_id = user_id
329        if user_id is None:
330            client.nick = 'guest%d' % client.client_id
331            client.send(TALK, 'Visit craft.michaelfogleman.com to register!')
332        else:
333            client.nick = username
334        self.send_nick(client)
335        # TODO: has left message if was already authenticated
336        self.send_talk('%s has joined the game.' % client.nick)
337    def on_chunk(self, client, p, q, key=0):
338        packets = []
339        p, q, key = map(int, (p, q, key))
340        query = (
341            'select rowid, x, y, z, w from block where '
342            'p = :p and q = :q and rowid > :key;'
343        )
344        rows = self.execute(query, dict(p=p, q=q, key=key))
345        max_rowid = 0
346        blocks = 0
347        for rowid, x, y, z, w in rows:
348            blocks += 1
349            packets.append(packet(BLOCK, p, q, x, y, z, w))
350            max_rowid = max(max_rowid, rowid)
351        query = (
352            'select x, y, z, w from light where '
353            'p = :p and q = :q;'
354        )
355        rows = self.execute(query, dict(p=p, q=q))
356        lights = 0
357        for x, y, z, w in rows:
358            lights += 1
359            packets.append(packet(LIGHT, p, q, x, y, z, w))
360        query = (
361            'select x, y, z, face, text from sign where '
362            'p = :p and q = :q;'
363        )
364        rows = self.execute(query, dict(p=p, q=q))
365        signs = 0
366        for x, y, z, face, text in rows:
367            signs += 1
368            packets.append(packet(SIGN, p, q, x, y, z, face, text))
369        if blocks:
370            packets.append(packet(KEY, p, q, max_rowid))
371        if blocks or lights or signs:
372            packets.append(packet(REDRAW, p, q))
373        packets.append(packet(CHUNK, p, q))
374        client.send_raw(''.join(packets))
375    def on_block(self, client, x, y, z, w):
376        x, y, z, w = map(int, (x, y, z, w))
377        p, q = chunked(x), chunked(z)
378        previous = self.get_block(x, y, z)
379        message = None
380        if client.user_id is None:
381            message = 'Only logged in users are allowed to build.'
382        elif y <= 0 or y > 255:
383            message = 'Invalid block coordinates.'
384        elif w not in ALLOWED_ITEMS:
385            message = 'That item is not allowed.'
386        elif w and previous:
387            message = 'Cannot create blocks in a non-empty space.'
388        elif not w and not previous:
389            message = 'That space is already empty.'
390        elif previous in INDESTRUCTIBLE_ITEMS:
391            message = 'Cannot destroy that type of block.'
392        if message is not None:
393            client.send(BLOCK, p, q, x, y, z, previous)
394            client.send(REDRAW, p, q)
395            client.send(TALK, message)
396            return
397        query = (
398            'insert into block_history (timestamp, user_id, x, y, z, w) '
399            'values (:timestamp, :user_id, :x, :y, :z, :w);'
400        )
401        if RECORD_HISTORY:
402            self.execute(query, dict(timestamp=time.time(),
403                user_id=client.user_id, x=x, y=y, z=z, w=w))
404        query = (
405            'insert or replace into block (p, q, x, y, z, w) '
406            'values (:p, :q, :x, :y, :z, :w);'
407        )
408        self.execute(query, dict(p=p, q=q, x=x, y=y, z=z, w=w))
409        self.send_block(client, p, q, x, y, z, w)
410        for dx in range(-1, 2):
411            for dz in range(-1, 2):
412                if dx == 0 and dz == 0:
413                    continue
414                if dx and chunked(x + dx) == p:
415                    continue
416                if dz and chunked(z + dz) == q:
417                    continue
418                np, nq = p + dx, q + dz
419                self.execute(query, dict(p=np, q=nq, x=x, y=y, z=z, w=-w))
420                self.send_block(client, np, nq, x, y, z, -w)
421        if w == 0:
422            query = (
423                'delete from sign where '
424                'x = :x and y = :y and z = :z;'
425            )
426            self.execute(query, dict(x=x, y=y, z=z))
427            query = (
428                'update light set w = 0 where '
429                'x = :x and y = :y and z = :z;'
430            )
431            self.execute(query, dict(x=x, y=y, z=z))
432    def on_light(self, client, x, y, z, w):
433        x, y, z, w = map(int, (x, y, z, w))
434        p, q = chunked(x), chunked(z)
435        block = self.get_block(x, y, z)
436        message = None
437        if client.user_id is None:
438            message = 'Only logged in users are allowed to build.'
439        elif block == 0:
440            message = 'Lights must be placed on a block.'
441        elif w < 0 or w > 15:
442            message = 'Invalid light value.'
443        if message is not None:
444            # TODO: client.send(LIGHT, p, q, x, y, z, previous)
445            client.send(REDRAW, p, q)
446            client.send(TALK, message)
447            return
448        query = (
449            'insert or replace into light (p, q, x, y, z, w) '
450            'values (:p, :q, :x, :y, :z, :w);'
451        )
452        self.execute(query, dict(p=p, q=q, x=x, y=y, z=z, w=w))
453        self.send_light(client, p, q, x, y, z, w)
454    def on_sign(self, client, x, y, z, face, *args):
455        if client.user_id is None:
456            client.send(TALK, 'Only logged in users are allowed to build.')
457            return
458        text = ','.join(args)
459        x, y, z, face = map(int, (x, y, z, face))
460        if y <= 0 or y > 255:
461            return
462        if face < 0 or face > 7:
463            return
464        if len(text) > 48:
465            return
466        p, q = chunked(x), chunked(z)
467        if text:
468            query = (
469                'insert or replace into sign (p, q, x, y, z, face, text) '
470                'values (:p, :q, :x, :y, :z, :face, :text);'
471            )
472            self.execute(query,
473                dict(p=p, q=q, x=x, y=y, z=z, face=face, text=text))
474        else:
475            query = (
476                'delete from sign where '
477                'x = :x and y = :y and z = :z and face = :face;'
478            )
479            self.execute(query, dict(x=x, y=y, z=z, face=face))
480        self.send_sign(client, p, q, x, y, z, face, text)
481    def on_position(self, client, x, y, z, rx, ry):
482        x, y, z, rx, ry = map(float, (x, y, z, rx, ry))
483        client.position = (x, y, z, rx, ry)
484        self.send_position(client)
485    def on_talk(self, client, *args):
486        text = ','.join(args)
487        if text.startswith('/'):
488            for pattern, func in self.patterns:
489                match = pattern.match(text)
490                if match:
491                    func(client, *match.groups())
492                    break
493            else:
494                client.send(TALK, 'Unrecognized command: "%s"' % text)
495        elif text.startswith('@'):
496            nick = text[1:].split(' ', 1)[0]
497            for other in self.clients:
498                if other.nick == nick:
499                    client.send(TALK, '%s> %s' % (client.nick, text))
500                    other.send(TALK, '%s> %s' % (client.nick, text))
501                    break
502            else:
503                client.send(TALK, 'Unrecognized nick: "%s"' % nick)
504        else:
505            self.send_talk('%s> %s' % (client.nick, text))
506    def on_spawn(self, client):
507        client.position = SPAWN_POINT
508        client.send(YOU, client.client_id, *client.position)
509        self.send_position(client)
510    def on_goto(self, client, nick=None):
511        if nick is None:
512            clients = [x for x in self.clients if x != client]
513            other = random.choice(clients) if clients else None
514        else:
515            nicks = dict((client.nick, client) for client in self.clients)
516            other = nicks.get(nick)
517        if other:
518            client.position = other.position
519            client.send(YOU, client.client_id, *client.position)
520            self.send_position(client)
521    def on_pq(self, client, p, q):
522        p, q = map(int, (p, q))
523        if abs(p) > 1000 or abs(q) > 1000:
524            return
525        client.position = (p * CHUNK_SIZE, 0, q * CHUNK_SIZE, 0, 0)
526        client.send(YOU, client.client_id, *client.position)
527        self.send_position(client)
528    def on_help(self, client, topic=None):
529        if topic is None:
530            client.send(TALK, 'Type "t" to chat. Type "/" to type commands:')
531            client.send(TALK, '/goto [NAME], /help [TOPIC], /list, /login NAME, /logout')
532            client.send(TALK, '/offline [FILE], /online HOST [PORT], /pq P Q, /spawn, /view N')
533            return
534        topic = topic.lower().strip()
535        if topic == 'goto':
536            client.send(TALK, 'Help: /goto [NAME]')
537            client.send(TALK, 'Teleport to another user.')
538            client.send(TALK, 'If NAME is unspecified, a random user is chosen.')
539        elif topic == 'list':
540            client.send(TALK, 'Help: /list')
541            client.send(TALK, 'Display a list of connected users.')
542        elif topic == 'login':
543            client.send(TALK, 'Help: /login NAME')
544            client.send(TALK, 'Switch to another registered username.')
545            client.send(TALK, 'The login server will be re-contacted. The username is case-sensitive.')
546        elif topic == 'logout':
547            client.send(TALK, 'Help: /logout')
548            client.send(TALK, 'Unauthenticate and become a guest user.')
549            client.send(TALK, 'Automatic logins will not occur again until the /login command is re-issued.')
550        elif topic == 'offline':
551            client.send(TALK, 'Help: /offline [FILE]')
552            client.send(TALK, 'Switch to offline mode.')
553            client.send(TALK, 'FILE specifies the save file to use and defaults to "craft".')
554        elif topic == 'online':
555            client.send(TALK, 'Help: /online HOST [PORT]')
556            client.send(TALK, 'Connect to the specified server.')
557        elif topic == 'pq':
558            client.send(TALK, 'Help: /pq P Q')
559            client.send(TALK, 'Teleport to the specified chunk.')
560        elif topic == 'spawn':
561            client.send(TALK, 'Help: /spawn')
562            client.send(TALK, 'Teleport back to the spawn point.')
563        elif topic == 'view':
564            client.send(TALK, 'Help: /view N')
565            client.send(TALK, 'Set viewing distance, 1 - 24.')
566    def on_list(self, client):
567        client.send(TALK,
568            'Players: %s' % ', '.join(x.nick for x in self.clients))
569    def send_positions(self, client):
570        for other in self.clients:
571            if other == client:
572                continue
573            client.send(POSITION, other.client_id, *other.position)
574    def send_position(self, client):
575        for other in self.clients:
576            if other == client:
577                continue
578            other.send(POSITION, client.client_id, *client.position)
579    def send_nicks(self, client):
580        for other in self.clients:
581            if other == client:
582                continue
583            client.send(NICK, other.client_id, other.nick)
584    def send_nick(self, client):
585        for other in self.clients:
586            other.send(NICK, client.client_id, client.nick)
587    def send_disconnect(self, client):
588        for other in self.clients:
589            if other == client:
590                continue
591            other.send(DISCONNECT, client.client_id)
592    def send_block(self, client, p, q, x, y, z, w):
593        for other in self.clients:
594            if other == client:
595                continue
596            other.send(BLOCK, p, q, x, y, z, w)
597            other.send(REDRAW, p, q)
598    def send_light(self, client, p, q, x, y, z, w):
599        for other in self.clients:
600            if other == client:
601                continue
602            other.send(LIGHT, p, q, x, y, z, w)
603            other.send(REDRAW, p, q)
604    def send_sign(self, client, p, q, x, y, z, face, text):
605        for other in self.clients:
606            if other == client:
607                continue
608            other.send(SIGN, p, q, x, y, z, face, text)
609    def send_talk(self, text):
610        log(text)
611        for client in self.clients:
612            client.send(TALK, text)
613
614def cleanup():
615    world = World(None)
616    conn = sqlite3.connect(DB_PATH)
617    query = 'select x, y, z from block order by rowid desc limit 1;'
618    last = list(conn.execute(query))[0]
619    query = 'select distinct p, q from block;'
620    chunks = list(conn.execute(query))
621    count = 0
622    total = 0
623    delete_query = 'delete from block where x = %d and y = %d and z = %d;'
624    print 'begin;'
625    for p, q in chunks:
626        chunk = world.create_chunk(p, q)
627        query = 'select x, y, z, w from block where p = :p and q = :q;'
628        rows = conn.execute(query, {'p': p, 'q': q})
629        for x, y, z, w in rows:
630            if chunked(x) != p or chunked(z) != q:
631                continue
632            total += 1
633            if (x, y, z) == last:
634                continue
635            original = chunk.get((x, y, z), 0)
636            if w == original or original in INDESTRUCTIBLE_ITEMS:
637                count += 1
638                print delete_query % (x, y, z)
639    conn.close()
640    print 'commit;'
641    print >> sys.stderr, '%d of %d blocks will be cleaned up' % (count, total)
642
643def main():
644    if len(sys.argv) == 2 and sys.argv[1] == 'cleanup':
645        cleanup()
646        return
647    host, port = DEFAULT_HOST, DEFAULT_PORT
648    if len(sys.argv) > 1:
649        host = sys.argv[1]
650    if len(sys.argv) > 2:
651        port = int(sys.argv[2])
652    log('SERV', host, port)
653    model = Model(None)
654    model.start()
655    server = Server((host, port), Handler)
656    server.model = model
657    server.serve_forever()
658
659if __name__ == '__main__':
660    main()
661