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