class CubeWorldServer: exit_code = None world = None old_time = None skip_index = 0 def __init__(self, loop, config): self.loop = loop self.config = config base = config.base # game-related self.update_packet = packets.ServerUpdate() self.update_packet.reset() self.connections = set() self.players = MultikeyDict() self.updated_chunks = set() use_same_loop = base.update_fps == base.network_fps if use_same_loop: def update_callback(): self.update() self.send_update() else: update_callback = self.update self.update_loop = LoopingCall(update_callback) self.update_loop.start(1.0 / base.update_fps, now=False) if use_same_loop: self.send_loop = self.update_loop else: self.send_loop = LoopingCall(self.send_update) self.send_loop.start(1.0 / base.network_fps, now=False) self.mission_loop = LoopingCall(self.update_missions) self.mission_loop.start(base.mission_update_rate, now=False) # world self.world = World(self, self.loop, base.seed, use_tgen=base.use_tgen, use_entities=base.use_entities, chunk_retire_time=base.chunk_retire_time, debug=base.world_debug_info) if base.world_debug_file is not None: debug_fp = open(base.world_debug_file, 'wb') self.world.set_debug(debug_fp) # server-related self.git_rev = base.get('git_rev', None) self.passwords = {} for k, v in base.passwords.items(): self.passwords[k.lower()] = v self.scripts = ScriptManager() for script in base.scripts: self.load_script(script) # time self.extra_elapsed_time = 0.0 self.start_time = loop.time() self.set_clock('12:00') # start listening self.loop.set_exception_handler(self.exception_handler) self.loop.create_task(self.create_server(self.build_protocol, port=base.port, family=socket.AF_INET)) def exception_handler(self, loop, context): exception = context.get('exception') if isinstance(exception, TimeoutError): pass else: loop.default_exception_handler(context) def build_protocol(self): return CubeWorldConnection(self) def drop_item(self, item_data, pos): item = packets.ChunkItemData() item.drop_time = 750 # XXX provide sane values for these item.scale = 0.1 item.rotation = 185.0 item.something3 = item.something5 = item.something6 = 0 item.pos = pos item.item_data = item_data self.world.get_chunk(get_chunk(pos)).add_item(item) def add_packet_list(self, items, l, size): for item in iterate_packet_list(l): items.append(item.data.copy()) def handle_tgen_packets(self, in_queue): if in_queue is None: return p = self.update_packet self.add_packet_list(p.player_hits, in_queue.player_hits, in_queue.player_hits_size) self.add_packet_list(p.sound_actions, in_queue.sound_actions, in_queue.sound_actions_size) self.add_packet_list(p.particles, in_queue.particles, in_queue.particles_size) self.add_packet_list(p.block_actions, in_queue.block_actions, in_queue.block_actions_size) self.add_packet_list(p.shoot_actions, in_queue.shoot_packets, in_queue.shoot_packets_size) self.add_packet_list(p.kill_actions, in_queue.kill_actions, in_queue.kill_actions_size) self.add_packet_list(p.damage_actions, in_queue.damage_actions, in_queue.damage_actions_size) self.add_packet_list(p.passive_actions, in_queue.passive_packets, in_queue.passive_packets_size) self.add_packet_list(p.missions, in_queue.missions, in_queue.missions_size) def update_missions(self): max_dist = self.config.base.mission_max_distance p = self.update_packet added = set() for connection in self.players.values(): player_entity = connection.entity if player_entity is None: continue min_pos = (player_entity.pos - max_dist) // constants.MISSION_SCALE max_pos = (player_entity.pos + max_dist) // constants.MISSION_SCALE for x in range(min_pos.x, max_pos.x): for y in range(min_pos.y, max_pos.y): if (x, y) in added: continue added.add((x, y)) reg_x = x // constants.MISSIONS_IN_REGION reg_y = y // constants.MISSIONS_IN_REGION try: reg = self.world.get_region((reg_x, reg_y)) except KeyError: continue local_x = x % constants.MISSIONS_IN_REGION local_y = y % constants.MISSIONS_IN_REGION try: m = reg.get_mission((local_x, local_y)) except (IndexError, ValueError): continue mission_packet = packets.MissionPacket() mission_packet.x = x mission_packet.y = y mission_packet.something1 = 0 mission_packet.something2 = 0 mission_packet.info = m.info p.missions.append(mission_packet) def send_entity_data(self, entity): base = self.config.base # full entity packet for new, close players entity_packet.set_entity(entity, entity.entity_id) full = packets.write_packet(entity_packet) # pos entity packet if not entity.is_tgen: entity_packet.set_entity(entity, entity.entity_id, entitydata.POS_FLAG) only_pos = packets.write_packet(entity_packet) # reduced rate packet skip_reduced = self.skip_index != 0 entity_packet.set_entity(entity, entity.entity_id, entitydata.POS_FLAG) reduced = packets.write_packet(entity_packet) max_distance = base.max_distance max_reduce_distance = base.max_reduce_distance old_close_players = entity.close_players new_close_players = {} for connection in self.players.values(): player_entity = connection.entity if player_entity is None: continue if entity is player_entity: continue if entity.full_update: connection.send_data(full) new_close_players[connection] = entity.copy() continue dist = (player_entity.pos - entity.pos).length if dist > max_distance: if not entity.is_tgen: connection.send_data(only_pos) continue old_ref = old_close_players.get(connection, None) if old_ref is None: connection.send_data(full) new_close_players[connection] = entity.copy() continue if dist > max_reduce_distance and skip_reduced: connection.send_data(reduced) new_close_players[connection] = old_ref continue new_mask = entitydata.get_mask(old_ref, entity) entity_packet.set_entity(entity, entity.entity_id, new_mask) connection.send_packet(entity_packet) new_close_players[connection] = entity.copy() entity.close_players = new_close_players entity.full_update = False def update(self): self.scripts.call('update') out_packets = self.world.update(self.update_loop.dt) self.handle_tgen_packets(out_packets) def send_update(self): self.skip_index = (self.skip_index + 1) % self.config.base.reduce_skip for entity in self.world.entities.values(): self.send_entity_data(entity) self.broadcast_packet(update_finished_packet) # other updates update_packet = self.update_packet for chunk in self.updated_chunks: chunk.on_update(update_packet) if not update_packet.is_empty(): self.broadcast_packet(update_packet) update_packet.reset() # reset drop times for chunk in self.updated_chunks: chunk.on_post_update() self.updated_chunks.clear() # time update new_time = (self.get_time(), self.get_day()) if new_time != self.old_time: time_packet.time = new_time[0] time_packet.day = new_time[1] self.broadcast_packet(time_packet) self.old_time = new_time def send_chat(self, value): chat_packet.entity_id = 0 chat_packet.value = value self.broadcast_packet(chat_packet) def play_sound(self, name, pos=None, pitch=1.0, volume=1.0): sound = packets.SoundAction() sound.set_name(name) sound.pitch = pitch sound.volume = volume if pos is not None: sound.pos = pos self.update_packet.sound_action.append(sound) return extra_server_update.reset() for player in self.players.values(): sound.pos = player.entity.pos extra_server_update.sound_actions = [sound] player.send_packet(extra_server_update) def broadcast_packet(self, packet): data = packets.write_packet(packet) for player in self.players.values(): player.send_data(data) # line/string formatting options based on config def format(self, value): format_dict = {'server_name': self.config.base.server_name} return value % format_dict def format_lines(self, value): lines = [] for line in value: lines.append(self.format(line)) return lines # script methods def load_script(self, name, update=False): try: return self.scripts[name] except KeyError: pass try: mod = __import__('scripts.%s' % name, globals(), locals(), [name]) if update: importlib.reload(mod) except ImportError as e: traceback.print_exc() return None script = mod.get_class()(self) print('Loaded script %r' % name) return script def unload_script(self, name): try: self.scripts[name].unload() except KeyError: return False print('Unloaded script %r' % name) return True def call_command(self, user, command, args): """ Calls a command from an external interface, e.g. IRC, console """ return self.scripts.call('on_command', user=user, command=command, args=args).result def get_mode(self): return self.scripts.call('get_mode').result # command convenience methods (for /help) def get_commands(self): for script in self.scripts.get(): if script.commands is None: continue for command in script.commands.values(): yield command def get_command(self, name): for script in self.scripts.get(): if script.commands is None: continue name = script.aliases.get(name, name) command = script.commands.get(name, None) if command: return command # binary data store methods def load_data(self, name, default=None): path = os.path.join(self.config.base.save_path, f'{name}.dat') try: with open(path, 'r', newline=None) as fp: data = fp.read() except IOError: return default return eval(data) def save_data(self, name, value): os.makedirs(self.config.base.save_path, exist_ok=True) path = os.path.join(self.config.base.save_path, f'{name}.dat') data = pprint.pformat(value, width=1) with open(path, 'w') as fp: fp.write(data) # time methods def set_clock(self, value): day = self.get_day() time = parse_clock(value) self.start_time = self.loop.time() self.extra_elapsed_time = day * constants.MAX_TIME + time def get_elapsed_time(self): dt = self.loop.time() - self.start_time dt *= self.config.base.time_modifier * constants.NORMAL_TIME_SPEED return dt * 1000 + self.extra_elapsed_time def get_time(self): return int(self.get_elapsed_time() % constants.MAX_TIME) def get_day(self): return int(self.get_elapsed_time() / constants.MAX_TIME) def get_clock(self): return get_clock_string(self.get_time()) # stop/restart def stop(self, code=None): print('Stopping...') self.exit_code = code if self.world: self.world.stop() self.scripts.unload() self.loop.stop() # asyncio wrappers def get_interface(self): return self.config.base.network_interface def create_datagram_endpoint(self, *arg, port=0, **kw): host = self.get_interface() addr = (host, port) return self.loop.create_datagram_endpoint(*arg, local_addr=addr, **kw) def create_server(self, *arg, **kw): return self.loop.create_server(*arg, host=self.get_interface(), **kw) def connect_connection(self, *arg, **kw): host = self.get_interface() return self.loop.create_connection(*arg, local_addr=(host, 0), **kw)
class CubeWorldConnection(Protocol): """ Protocol used for players """ has_joined = False entity_id = None entity_data = None disconnected = False scripts = None def __init__(self, server, addr): self.address = addr self.server = server # connection methods def connectionMade(self): server = self.server if len(server.connections) >= server.config.base.max_players: self.send_packet(server_full_packet) self.disconnect() return self.packet_handlers = { ClientVersion.packet_id: self.on_version_packet, EntityUpdate.packet_id: self.on_entity_packet, ClientChatMessage.packet_id: self.on_chat_packet, InteractPacket.packet_id: self.on_interact_packet, HitPacket.packet_id: self.on_hit_packet, ShootPacket.packet_id: self.on_shoot_packet } self.packet_handler = PacketHandler(CS_PACKETS, self.on_packet) server.connections.add(self) self.rights = AttributeSet() self.scripts = ScriptManager() server.scripts.call('on_new_connection', connection=self) def dataReceived(self, data): self.packet_handler.feed(data) def disconnect(self, reason=None): self.transport.loseConnection() self.connectionLost(reason) def connectionLost(self, reason): if self.disconnected: return self.disconnected = True self.server.connections.discard(self) if self.has_joined: del self.server.players[self] print 'Player %s left' % self.name if self.entity_data is not None: del self.server.entities[self.entity_id] if self.entity_id is not None: self.server.entity_ids.put_back(self.entity_id) if self.scripts is not None: self.scripts.unload() # packet methods def send_packet(self, packet): self.transport.write(write_packet(packet)) def on_packet(self, packet): if self.disconnected: return if packet is None: print 'Invalid packet received' self.disconnect() raise StopIteration() handler = self.packet_handlers.get(packet.packet_id, None) if handler is None: # print 'Unhandled client packet: %s' % packet.packet_id return handler(packet) def on_version_packet(self, packet): if packet.version != constants.CLIENT_VERSION: mismatch_packet.version = constants.CLIENT_VERSION self.send_packet(mismatch_packet) self.disconnect() return server = self.server self.entity_id = server.entity_ids.pop() join_packet.entity_id = self.entity_id self.send_packet(join_packet) seed_packet.seed = server.config.base.seed self.send_packet(seed_packet) def on_entity_packet(self, packet): if self.entity_data is None: self.entity_data = create_entity_data() self.server.entities[self.entity_id] = self.entity_data mask = packet.update_entity(self.entity_data) self.entity_data.mask |= mask if not self.has_joined and getattr(self.entity_data, 'name', None): self.on_join() return self.scripts.call('on_entity_update', mask=mask) # XXX clean this up if entity.is_pos_set(mask): self.scripts.call('on_pos_update') if entity.is_mode_set(mask): self.scripts.call('on_mode_update') if entity.is_class_set(mask): self.scripts.call('on_class_update') if entity.is_name_set(mask): self.scripts.call('on_name_update') if entity.is_multiplier_set(mask): self.scripts.call('on_multiplier_update') if entity.is_level_set(mask): self.scripts.call('on_level_update') if entity.is_equipment_set(mask): self.scripts.call('on_equipment_update') if entity.is_skill_set(mask): self.scripts.call('on_skill_update') if entity.is_appearance_set(mask): self.scripts.call('on_appearance_update') if entity.is_charged_mp_set(mask): self.scripts.call('on_charged_mp_update') if entity.is_flags_set(mask): self.scripts.call('on_flags_update') if entity.is_consumable_set(mask): self.scripts.call('on_consumable_update') def on_chat_packet(self, packet): message = filter_string(packet.value).strip() if not message: return message = self.on_chat(message) if not message: return chat_packet.entity_id = self.entity_id chat_packet.value = message self.server.broadcast_packet(chat_packet) print '%s: %s' % (self.name, message) def on_interact_packet(self, packet): interact_type = packet.interact_type item = packet.item_data if interact_type == INTERACT_DROP: pos = self.position.copy() pos.z -= constants.BLOCK_SCALE if self.scripts.call('on_drop', item=item, pos=pos).result is False: return self.server.drop_item(packet.item_data, pos) elif interact_type == INTERACT_PICKUP: chunk = (packet.chunk_x, packet.chunk_y) try: item = self.server.remove_item(chunk, packet.item_index) except IndexError: return self.give_item(item) def on_hit_packet(self, packet): try: target = self.server.entities[packet.target_id] except KeyError: return if self.scripts.call('on_hit', target=target, packet=packet) is False: return self.server.update_packet.player_hits.append(packet) if target.hp <= 0: return target.hp -= packet.damage if target.hp <= 0: self.scripts.call('on_kill', target=target) def on_shoot_packet(self, packet): self.server.update_packet.shoot_actions.append(packet) # handlers def on_join(self): if self.scripts.call('on_join').result is False: return print 'Player %s joined' % self.name for player in self.server.players.values(): entity_packet.set_entity(player.entity_data, player.entity_id) self.send_packet(entity_packet) self.server.players[(self.entity_id,)] = self self.has_joined = True def on_command(self, command, parameters): self.scripts.call('on_command', command=command, args=parameters) def on_chat(self, message): if message.startswith('/'): command, args = parse_command(message[1:]) self.on_command(command, args) return event = self.scripts.call('on_chat', message=message) if event.result is False: return return event.message # other methods def send_chat(self, value): packet = ServerChatMessage() packet.entity_id = 0 packet.value = value self.send_packet(packet) def give_item(self, item): action = PickupAction() action.entity_id = self.entity_id action.item_data = item self.server.update_packet.pickups.append(action) def send_lines(self, lines): current_time = 0 for line in lines: reactor.callLater(current_time, self.send_chat, line) current_time += 2 def kick(self): self.send_chat('You have been kicked') self.disconnect() self.server.send_chat('%s has been kicked' % self.name) # convienience methods @property def position(self): if self.entity_data is None: return None return self.entity_data.pos @property def name(self): if self.entity_data is None: return None return self.entity_data.name
class CubeWorldConnection(asyncio.Protocol): """ Protocol used for players """ has_joined = False entity_id = None entity = None disconnected = False scripts = None chunk = None mounted_entity = None def __init__(self, server): self.server = server self.world = server.world self.loop = server.loop def connection_made(self, transport): self.transport = transport self.address = transport.get_extra_info('peername') accept = self.server.scripts.call('on_connection_attempt', address=self.address).result # hardban if accept is False: self.transport.abort() self.disconnected = True return # enable TCP_NODELAY sock = transport.get_extra_info('socket') sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) # ban with message if accept is not None: join_packet.entity_id = 1 self.send_packet(join_packet) def disconnect(): self.disconnected = False chat_packet.entity_id = 0 chat_packet.value = accept self.send_packet(chat_packet) self.disconnected = True self.transport.close() # need to add a small delay, since the client will otherwise # ignore our chat message self.loop.call_later(0.1, disconnect) self.disconnected = True self.transport.pause_reading() return server = self.server if len(server.connections) >= server.config.base.max_players: self.send_packet(server_full_packet) self.disconnect() return self.packet_handlers = { packets.ClientVersion.packet_id: self.on_version_packet, packets.EntityUpdate.packet_id: self.on_entity_packet, packets.ClientChatMessage.packet_id: self.on_chat_packet, packets.InteractPacket.packet_id: self.on_interact_packet, packets.HitPacket.packet_id: self.on_hit_packet, packets.ShootPacket.packet_id: self.on_shoot_packet, packets.PassivePacket.packet_id: self.on_passive_packet, packets.ChunkDiscovered.packet_id: self.on_discover_packet } self.packet_handler = packets.PacketHandler(packets.CS_PACKETS, self.on_packet) server.connections.add(self) self.rights = AttributeSet() self.scripts = ScriptManager() server.scripts.call('on_new_connection', connection=self) def data_received(self, data): if self.is_closing(): return self.packet_handler.feed(data) def is_closing(self): return self.disconnected or self.transport.is_closing() def disconnect(self, reason=None): if self.is_closing(): return self.transport.close() self.connection_lost(reason) def connection_lost(self, reason): if self.disconnected: return self.disconnected = True self.server.connections.discard(self) if self.has_joined: del self.server.players[self] print('Player %s left' % self.name) if self.entity is not None: self.entity.destroy() if self.entity_id is not None: # need to handle this here, since the player may not have an # entity yet self.world.entity_ids.put_back(self.entity_id) if self.scripts is not None: self.scripts.unload() # packet methods def send_data(self, data): if self.is_closing(): return self.transport.write(data) def send_packet(self, packet): if self.is_closing(): return data = packets.write_packet(packet) self.transport.write(data) def on_packet(self, packet): if self.is_closing(): return if packet is None: self.on_invalid_packet('data') return handler = self.packet_handlers.get(packet.packet_id, None) if handler is None: # print 'Unhandled client packet: %s' % packet.packet_id return handler(packet) def on_version_packet(self, packet): if packet.version != constants.CLIENT_VERSION: mismatch_packet.version = constants.CLIENT_VERSION self.send_packet(mismatch_packet) self.disconnect() return self.entity_id = self.world.entity_ids.pop() join_packet.entity_id = self.entity_id self.send_packet(join_packet) seed_packet.seed = self.server.config.base.seed self.send_packet(seed_packet) def on_entity_packet(self, packet): if self.entity is None: self.entity = self.world.create_entity(self.entity_id) self.entity.connection = self self.old_entity = self.entity.copy() mask = packet.update_entity(self.entity) if not self.has_joined and entitydata.is_name_set(mask): self.on_join() return self.scripts.call('on_entity_update', mask=mask) # XXX clean this up if entitydata.is_pos_set(mask): self.on_pos_update() if entitydata.is_vel_set(mask): if self.mounted_entity: self.mount(None) if entitydata.is_mode_set(mask): self.scripts.call('on_mode_update') if entitydata.is_class_set(mask): self.scripts.call('on_class_update') if entitydata.is_name_set(mask): self.on_name_update() if entitydata.is_multiplier_set(mask): self.scripts.call('on_multiplier_update') if entitydata.is_level_set(mask): self.scripts.call('on_level_update') if entitydata.is_equipment_set(mask): self.scripts.call('on_equipment_update') if entitydata.is_skill_set(mask): self.scripts.call('on_skill_update') if entitydata.is_appearance_set(mask): self.scripts.call('on_appearance_update') if entitydata.is_charged_mp_set(mask): self.scripts.call('on_charged_mp_update') if entitydata.is_flags_set(mask): self.scripts.call('on_flags_update') if entitydata.is_consumable_set(mask): self.scripts.call('on_consumable_update') def mount(self, entity): if self.mounted_entity: self.mounted_entity.on_unmount(self) self.mounted_entity = entity def on_name_update(self): if self.old_entity.name: print(self.old_entity.name, 'changed name to', self.entity.name) if self.entity: self.entity.full_update = True self.scripts.call('on_name_update') def on_pos_update(self): try: chunk_pos = get_chunk(self.position) except ValueError: self.on_invalid_packet('position') return if not self.chunk or chunk_pos != self.chunk.pos: self.chunk = self.world.get_chunk(chunk_pos) self.scripts.call('on_pos_update') def on_chat_packet(self, packet): message = filter_string(packet.value).strip() if not message: return message = self.on_chat(message) if not message: return chat_packet.entity_id = self.entity_id chat_packet.value = message self.server.broadcast_packet(chat_packet) print('%s: %s' % (self.name, message)) def on_interact_packet(self, packet): interact_type = packet.interact_type item = packet.item_data if interact_type == packets.INTERACT_DROP: pos = self.position.copy() pos.z -= constants.BLOCK_SCALE if self.scripts.call('on_drop', item=item, pos=pos).result is False: return self.server.drop_item(packet.item_data, pos) elif interact_type == packets.INTERACT_PICKUP: chunk = self.world.get_chunk((packet.chunk_x, packet.chunk_y)) try: item = chunk.remove_item(packet.item_index) except IndexError: return self.give_item(item) elif interact_type == packets.INTERACT_NORMAL: chunk = self.world.get_chunk((packet.chunk_x, packet.chunk_y)) try: chunk.get_entity(packet.item_index).interact(self) except KeyError: return def on_hit_packet(self, packet): try: target = self.world.entities[packet.target_id] except KeyError: return if self.scripts.call('on_hit', target=target, packet=packet).result is False: return self.server.update_packet.player_hits.append(packet) if target.is_tgen: self.world.add_hit(packet) return if target.hp <= 0: return target.hp -= packet.damage if target.hp > 0: return self.scripts.call('on_kill', target=target) if not target.connection: return target.connection.scripts.call('on_die', killer=self.entity) def on_shoot_packet(self, packet): self.server.update_packet.shoot_actions.append(packet) def on_passive_packet(self, packet): self.world.add_passive(packet) self.server.update_packet.passive_actions.append(packet) def on_discover_packet(self, packet): # update static entities on client extra_server_update.reset() pos = (packet.x, packet.y) if pos in self.server.world.chunks: chunk = self.server.world.chunks[pos] chunk.on_update(extra_server_update) for static in chunk.static_entities.values(): if not static.changed: continue extra_server_update.static_entities.append(static.packet) self.send_packet(extra_server_update) # handlers def on_invalid_packet(self, message): name = self.name or self.entity_id or self.address[0] print('Received invalid %r data from %r, ' 'disconnecting' % (message, name)) self.packet_handler.stop() self.disconnect() def on_join(self): if self.scripts.call('on_join').result is False: return print('Player %s joined' % self.name) for entity_id, entity in self.server.world.entities.items(): entity_packet.set_entity(entity, entity_id) self.send_packet(entity_packet) self.server.players[(self.entity_id,)] = self self.has_joined = True def on_command(self, command, parameters): self.scripts.call('on_command', command=command, args=parameters) def on_chat(self, message): if message.startswith('/'): command, args = parse_command(message[1:]) self.on_command(command, args) return event = self.scripts.call('on_chat', message=message) if event.result is False: return return event.message # other methods def play_sound(self, name, pos=None, pitch=1.0, volume=1.0): extra_server_update.reset() sound = packets.SoundAction() sound.set_name(name) if pos is None: pos = self.entity.pos sound.pos = pos sound.pitch = pitch sound.volume = volume extra_server_update.sound_actions.append(sound) self.send_packet(extra_server_update) def send_chat(self, value): chat_packet.entity_id = 0 chat_packet.value = value self.send_packet(chat_packet) def give_item(self, item): action = packets.PickupAction() action.entity_id = self.entity_id action.item_data = item self.server.update_packet.pickups.append(action) def send_lines(self, lines): current_time = 0 for line in lines: self.loop.call_later(current_time, self.send_chat, line) current_time += 2 def kick(self, reason=None): postfix = ': %s' % reason if reason is not None else '' self.send_chat('You have been kicked%s' % postfix) self.server.send_chat('%s has been kicked%s' % (self.name, postfix)) self.disconnect() # convenience methods @property def position(self): if self.entity is None: return None return self.entity.pos @property def name(self): if self.entity is None: return None return self.entity.name
class CubeWorldServer: exit_code = None world = None def __init__(self, loop, config): self.loop = loop self.config = config base = config.base # game-related self.update_packet = packets.ServerUpdate() self.update_packet.reset() self.connections = set() self.players = MultikeyDict() self.chunks = {} self.updated_chunks = set() self.update_loop = LoopingCall(self.update) self.update_loop.start(1.0 / base.update_fps, now=False) # world self.world = World(self, self.loop, base.seed, base.use_tgen, base.use_entities) # server-related self.git_rev = base.get('git_rev', None) self.passwords = {} for k, v in base.passwords.items(): self.passwords[k.lower()] = v self.scripts = ScriptManager() for script in base.scripts: self.load_script(script) # time self.extra_elapsed_time = 0.0 self.start_time = loop.time() self.set_clock('12:00') # start listening asyncio.Task(self.create_server(self.build_protocol, port=base.port)) def build_protocol(self): return CubeWorldConnection(self) def drop_item(self, item_data, pos): item = packets.ChunkItemData() item.drop_time = 750 # XXX provide sane values for these item.scale = 0.1 item.rotation = 185.0 item.something3 = item.something5 = item.something6 = 0 item.pos = pos item.item_data = item_data self.world.get_chunk(get_chunk(pos)).add_item(item) def update(self): self.scripts.call('update') # entity updates for entity_id, entity in self.world.entities.items(): entity_packet.set_entity(entity, entity_id, entity.mask) entity.mask = 0 self.broadcast_packet(entity_packet) self.broadcast_packet(update_finished_packet) # other updates update_packet = self.update_packet for chunk in self.updated_chunks: chunk.on_update(update_packet) self.broadcast_packet(update_packet) update_packet.reset() # reset drop times for chunk in self.updated_chunks: chunk.on_post_update() self.updated_chunks.clear() # time update time_packet.time = self.get_time() time_packet.day = self.get_day() self.broadcast_packet(time_packet) def send_chat(self, value): chat_packet.entity_id = 0 chat_packet.value = value self.broadcast_packet(chat_packet) def play_sound(self, name, pos=None, pitch=1.0, volume=1.0): sound = packets.SoundAction() sound.set_name(name) sound.pitch = pitch sound.volume = volume if pos is not None: sound.pos = pos self.update_packet.sound_action.append(sound) return extra_server_update.reset() for player in self.players.values(): sound.pos = player.entity.pos extra_server_update.sound_actions = [sound] player.send_packet(extra_server_update) def broadcast_packet(self, packet): data = packets.write_packet(packet) for player in self.players.values(): player.transport.write(data) # line/string formatting options based on config def format(self, value): format_dict = {'server_name': self.config.base.server_name} return value % format_dict def format_lines(self, value): lines = [] for line in value: lines.append(self.format(line)) return lines # script methods def load_script(self, name, update=False): try: return self.scripts[name] except KeyError: pass try: mod = __import__('scripts.%s' % name, globals(), locals(), [name]) if update: importlib.reload(mod) except ImportError as e: traceback.print_exc() return None script = mod.get_class()(self) print('Loaded script %r' % name) return script def unload_script(self, name): try: self.scripts[name].unload() except KeyError: return False print('Unloaded script %r' % name) return True def call_command(self, user, command, args): """ Calls a command from an external interface, e.g. IRC, console """ return self.scripts.call('on_command', user=user, command=command, args=args).result def get_mode(self): return self.scripts.call('get_mode').result # command convenience methods (for /help) def get_commands(self): for script in self.scripts.get(): if script.commands is None: continue for command in script.commands.values(): yield command def get_command(self, name): for script in self.scripts.get(): if script.commands is None: continue name = script.aliases.get(name, name) command = script.commands.get(name, None) if command: return command # binary data store methods def load_data(self, name, default=None): path = './save/%s.dat' % name try: with open(path, 'rU') as fp: data = fp.read() except IOError: return default return eval(data) def save_data(self, name, value): path = './save/%s.dat' % name data = pprint.pformat(value, width=1) with open(path, 'w') as fp: fp.write(data) # time methods def set_clock(self, value): day = self.get_day() time = parse_clock(value) self.start_time = self.loop.time() self.extra_elapsed_time = day * constants.MAX_TIME + time def get_elapsed_time(self): dt = self.loop.time() - self.start_time dt *= self.config.base.time_modifier * constants.NORMAL_TIME_SPEED return dt * 1000 + self.extra_elapsed_time def get_time(self): return int(self.get_elapsed_time() % constants.MAX_TIME) def get_day(self): return int(self.get_elapsed_time() / constants.MAX_TIME) def get_clock(self): return get_clock_string(self.get_time()) # stop/restart def stop(self, code=None): print('Stopping...') self.exit_code = code if self.world: self.world.stop() self.scripts.unload() self.loop.stop() # asyncio wrappers def get_interface(self): return self.config.base.network_interface def create_datagram_endpoint(self, *arg, port=0, **kw): host = self.get_interface() addr = (host, port) return self.loop.create_datagram_endpoint(*arg, local_addr=addr, **kw) def create_server(self, *arg, **kw): return self.loop.create_server(*arg, host=self.get_interface(), **kw) def connect_connection(self, *arg, **kw): host = self.get_interface() return self.loop.create_connection(*arg, local_addr=(host, 0), **kw)
class CubeWorldConnection(asyncio.Protocol): """ Protocol used for players """ has_joined = False entity_id = None entity = None disconnected = False scripts = None chunk = None mounted_entity = None def __init__(self, server): self.server = server self.world = server.world self.loop = server.loop def connection_made(self, transport): self.transport = transport self.address = transport.get_extra_info('peername') accept = self.server.scripts.call('on_connection_attempt', address=self.address).result # hardban if accept is False: self.transport.abort() self.disconnected = True return # enable TCP_NODELAY sock = transport.get_extra_info('socket') sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) # ban with message if accept is not None: join_packet.entity_id = 1 self.send_packet(join_packet) def disconnect(): chat_packet.entity_id = 0 chat_packet.value = accept self.send_packet(chat_packet) self.transport.close() # need to add a small delay, since the client will otherwise # ignore our chat message self.loop.call_later(0.1, disconnect) self.disconnected = True self.transport.pause_reading() return server = self.server if len(server.connections) >= server.config.base.max_players: self.send_packet(server_full_packet) self.disconnect() return self.packet_handlers = { packets.ClientVersion.packet_id: self.on_version_packet, packets.EntityUpdate.packet_id: self.on_entity_packet, packets.ClientChatMessage.packet_id: self.on_chat_packet, packets.InteractPacket.packet_id: self.on_interact_packet, packets.HitPacket.packet_id: self.on_hit_packet, packets.ShootPacket.packet_id: self.on_shoot_packet } self.packet_handler = packets.PacketHandler(packets.CS_PACKETS, self.on_packet) server.connections.add(self) self.rights = AttributeSet() self.scripts = ScriptManager() server.scripts.call('on_new_connection', connection=self) def data_received(self, data): self.packet_handler.feed(data) def disconnect(self, reason=None): if self.disconnected: return self.transport.close() self.connection_lost(reason) def connection_lost(self, reason): if self.disconnected: return self.disconnected = True self.server.connections.discard(self) if self.has_joined: del self.server.players[self] print('Player %s left' % self.name) if self.entity is not None: self.entity.destroy() if self.entity_id is not None: # XXX handle this in Entity.destroy? self.world.entity_ids.put_back(self.entity_id) if self.scripts is not None: self.scripts.unload() # packet methods def send_packet(self, packet): if self.disconnected: return self.transport.write(packets.write_packet(packet)) def on_packet(self, packet): if self.disconnected: return if packet is None: self.on_invalid_packet('data') return handler = self.packet_handlers.get(packet.packet_id, None) if handler is None: # print 'Unhandled client packet: %s' % packet.packet_id return handler(packet) def on_version_packet(self, packet): if packet.version != constants.CLIENT_VERSION: mismatch_packet.version = constants.CLIENT_VERSION self.send_packet(mismatch_packet) self.disconnect() return self.entity_id = self.world.entity_ids.pop() join_packet.entity_id = self.entity_id self.send_packet(join_packet) seed_packet.seed = self.server.config.base.seed self.send_packet(seed_packet) def on_entity_packet(self, packet): if self.entity is None: self.entity = self.world.create_entity(self.entity_id) self.entity.connection = self mask = packet.update_entity(self.entity) self.entity.mask |= mask if not self.has_joined and getattr(self.entity, 'name', None): self.on_join() return self.scripts.call('on_entity_update', mask=mask) # XXX clean this up if entitydata.is_pos_set(mask): self.on_pos_update() if entitydata.is_vel_set(mask): if self.mounted_entity: self.mount(None) if entitydata.is_mode_set(mask): self.scripts.call('on_mode_update') if entitydata.is_class_set(mask): self.scripts.call('on_class_update') if entitydata.is_name_set(mask): self.scripts.call('on_name_update') if entitydata.is_multiplier_set(mask): self.scripts.call('on_multiplier_update') if entitydata.is_level_set(mask): self.scripts.call('on_level_update') if entitydata.is_equipment_set(mask): self.scripts.call('on_equipment_update') if entitydata.is_skill_set(mask): self.scripts.call('on_skill_update') if entitydata.is_appearance_set(mask): self.scripts.call('on_appearance_update') if entitydata.is_charged_mp_set(mask): self.scripts.call('on_charged_mp_update') if entitydata.is_flags_set(mask): self.scripts.call('on_flags_update') if entitydata.is_consumable_set(mask): self.scripts.call('on_consumable_update') def mount(self, entity): if self.mounted_entity: self.mounted_entity.on_unmount(self) self.mounted_entity = entity def on_pos_update(self): try: chunk_pos = get_chunk(self.position) except ValueError: self.on_invalid_packet('position') return if not self.chunk or chunk_pos != self.chunk.pos: self.chunk = self.world.get_chunk(chunk_pos) self.scripts.call('on_pos_update') def on_chat_packet(self, packet): message = filter_string(packet.value).strip() if not message: return message = self.on_chat(message) if not message: return chat_packet.entity_id = self.entity_id chat_packet.value = message self.server.broadcast_packet(chat_packet) print('%s: %s' % (self.name, message)) def on_interact_packet(self, packet): interact_type = packet.interact_type item = packet.item_data if interact_type == packets.INTERACT_DROP: pos = self.position.copy() pos.z -= constants.BLOCK_SCALE if self.scripts.call('on_drop', item=item, pos=pos).result is False: return self.server.drop_item(packet.item_data, pos) elif interact_type == packets.INTERACT_PICKUP: chunk = self.world.get_chunk((packet.chunk_x, packet.chunk_y)) try: item = chunk.remove_item(packet.item_index) except IndexError: return self.give_item(item) elif interact_type == packets.INTERACT_NORMAL: chunk = self.world.get_chunk((packet.chunk_x, packet.chunk_y)) try: chunk.get_entity(packet.item_index).interact(self) except KeyError: return def on_hit_packet(self, packet): try: target = self.world.entities[packet.target_id] except KeyError: return if self.scripts.call('on_hit', target=target, packet=packet).result is False: return self.server.update_packet.player_hits.append(packet) if target.hp <= 0: return target.hp -= packet.damage if target.hp > 0: return self.scripts.call('on_kill', target=target) if not target.connection: return target.connection.scripts.call('on_die', killer=self.entity) def on_shoot_packet(self, packet): self.server.update_packet.shoot_actions.append(packet) # handlers def on_invalid_packet(self, message): name = self.name or self.entity_id or self.address[0] print('Received invalid %r data from %r, ' 'disconnecting' % (message, name)) self.packet_handler.stop() self.disconnect() def on_join(self): if self.scripts.call('on_join').result is False: return print('Player %s joined' % self.name) for player in self.server.players.values(): entity_packet.set_entity(player.entity, player.entity_id) self.send_packet(entity_packet) self.server.players[(self.entity_id, )] = self self.has_joined = True def on_command(self, command, parameters): self.scripts.call('on_command', command=command, args=parameters) def on_chat(self, message): if message.startswith('/'): command, args = parse_command(message[1:]) self.on_command(command, args) return event = self.scripts.call('on_chat', message=message) if event.result is False: return return event.message # other methods def play_sound(self, name, pos=None, pitch=1.0, volume=1.0): extra_server_update.reset() sound = packets.SoundAction() sound.set_name(name) if pos is None: pos = self.entity.pos sound.pos = pos sound.pitch = pitch sound.volume = volume extra_server_update.sound_actions.append(sound) self.send_packet(extra_server_update) def send_chat(self, value): chat_packet.entity_id = 0 chat_packet.value = value self.send_packet(chat_packet) def give_item(self, item): action = packets.PickupAction() action.entity_id = self.entity_id action.item_data = item self.server.update_packet.pickups.append(action) def send_lines(self, lines): current_time = 0 for line in lines: self.loop.call_later(current_time, self.send_chat, line) current_time += 2 def kick(self, reason=None): postfix = ': %s' % reason if reason is not None else '' self.send_chat('You have been kicked%s' % postfix) self.server.send_chat('%s has been kicked%s' % (self.name, postfix)) self.disconnect() # convenience methods @property def position(self): if self.entity is None: return None return self.entity.pos @property def name(self): if self.entity is None: return None return self.entity.name
class CubeWorldConnection(Protocol): """ Protocol used for players """ connection_state = 0 entity_id = None entity_data = None login_id = None change_index = -1 scripts = None chunk = None old_name = None old_pos = None old_health = None old_level = None old_xp = None # used for anti chat spamming time_last_chat = 0 chat_messages_burst = 0 # used for detecting dead connections time_last_packet = 0 time_last_rate = 0 packet_count = 0 packet_rate = 0 # used for basic DoS protection packet_burst = 0 def __init__(self, server, addr): self.address = addr self.server = server # connection methods def connectionMade(self): if self.connection_state != 0: self.disconnect('Unexpected data') return self.connection_state = 1 server = self.server if len(server.connections) >= server.config.base.max_players: # For being able to allow joining by external scritps although server is full ret = self.scripts.call('on_join_full_server').result if ret is not True: self.send_packet(server_full_packet) self.disconnect() self.connection_state = -1 print '[INFO] %s tried to join full server' % self.address.host return self.packet_handlers = { ClientVersion.packet_id: self.on_version_packet, EntityUpdate.packet_id: self.on_entity_packet, ClientChatMessage.packet_id: self.on_chat_packet, InteractPacket.packet_id: self.on_interact_packet, HitPacket.packet_id: self.on_hit_packet, ShootPacket.packet_id: self.on_shoot_packet } self.packet_handler = PacketHandler(CS_PACKETS, self.on_packet) server.connections.add(self) self.rights = AttributeSet() self.scripts = ScriptManager() server.scripts.call('on_new_connection', connection=self) def dataReceived(self, data): self.packet_handler.feed(data) def disconnect(self, reason=None): self.transport.loseConnection() self.connectionLost(reason) def connectionLost(self, reason): if self.connection_state < 0: return self.server.connections.discard(self) if self.connection_state >= 3: del self.server.players[self] print '[INFO] Player %s #%s left the game.' % (self.name, self.entity_id) self.server.send_chat('<<< %s #%s left the game' % (self.name, self.entity_id)) self.connection_state = -1 if self.entity_id is not None: self.server.world.unregister(self.entity_id) self.server.entity_ids.put_back(self.entity_id) if self.scripts is not None: self.scripts.unload() # packet methods def send_packet(self, packet): self.transport.write(write_packet(packet)) def on_packet(self, packet): if self.connection_state < 0: return if packet is None: print 'Invalid packet received' self.disconnect() raise StopIteration() handler = self.packet_handlers.get(packet.packet_id, None) if handler is None: # print 'Unhandled client packet: %s' % packet.packet_id return handler(packet) def on_version_packet(self, packet): if packet.version != constants.CLIENT_VERSION: mismatch_packet.version = constants.CLIENT_VERSION self.send_packet(mismatch_packet) self.disconnect(None) return server = self.server self.entity_id = server.entity_ids.pop() join_packet.entity_id = self.entity_id self.connection_state = 2 self.send_packet(join_packet) seed_packet.seed = server.config.base.seed self.send_packet(seed_packet) def on_entity_packet(self, packet): if self.entity_data is None: self.entity_data = create_entity_data() mask = packet.update_entity(self.entity_data) self.entity_data.mask |= mask if self.connection_state==2 and getattr(self.entity_data, 'name', None): self.on_join() return self.scripts.call('on_entity_update', mask=mask) # XXX clean this up if entity.is_pos_set(mask): self.on_pos_update() if entity.is_mode_set(mask): self.scripts.call('on_mode_update') if entity.is_class_set(mask): self.scripts.call('on_class_update') if entity.is_name_set(mask): self.scripts.call('on_name_update') if entity.is_multiplier_set(mask): self.scripts.call('on_multiplier_update') if entity.is_level_set(mask): self.scripts.call('on_level_update') if entity.is_equipment_set(mask): self.scripts.call('on_equipment_update') if entity.is_skill_set(mask): self.scripts.call('on_skill_update') if entity.is_appearance_set(mask): self.scripts.call('on_appearance_update') if entity.is_charged_mp_set(mask): self.scripts.call('on_charged_mp_update') if entity.is_flags_set(mask): self.scripts.call('on_flags_update') if entity.is_consumable_set(mask): self.scripts.call('on_consumable_update') def on_chunk(self, data): self.chunk = data def on_pos_update(self): if self.server.world: chunk = self.server.world.get_chunk_scaled(self.position.x, self.position.y) if chunk != self.chunk: self.chunk = chunk self.scripts.call('on_chunk_update') self.scripts.call('on_pos_update') def on_chat_packet(self, packet): message = filter_string(packet.value).strip() if not message: return message = self.on_chat(message) if not message: return chat_packet.entity_id = self.entity_id chat_packet.value = message self.server.broadcast_packet(chat_packet) print '[CHAT] %s: %s' % (self.name, message) def on_interact_packet(self, packet): interact_type = packet.interact_type item = packet.item_data if interact_type == INTERACT_DROP: pos = self.position.copy() pos.z -= constants.BLOCK_SCALE if self.scripts.call('on_drop', item=item, pos=pos).result is False: return self.server.drop_item(packet.item_data, pos) elif interact_type == INTERACT_PICKUP: try: item = self.server.remove_item(packet.chunk_x, packet.chunk_y, packet.item_index) except IndexError: return self.give_item(item) def on_hit_packet(self, packet): try: target = self.server.entities[packet.target_id] except KeyError: return if constants.MAX_DISTANCE > 0: edist = get_distance_3d(self.position.x, self.position.y, self.position.z, target.entity_data.pos.x, target.entity_data.pos.y, target.entity_data.pos.z) if edist > constants.MAX_DISTANCE: print '[ANTICHEAT BASE] Player %s tried to attack target that is %s away!' % (self.name, edist) self.kick('Range error') return if self.scripts.call('on_hit', target=target, packet=packet).result is False: return self.server.update_packet.player_hits.append(packet) if packet.damage <= 0: return if packet.damage > 1000: packet.damage = 1000 if target.hp > packet.damage: if self.scripts.call('on_damage', target=target, packet=packet).result is False: return target.hp -= packet.damage else: target.hp = 0 self.scripts.call('on_kill', target=target) def on_shoot_packet(self, packet): self.server.update_packet.shoot_actions.append(packet) def do_anticheat_actions(self): if not self.server.config.base.cheat_prevention: return False if not self.check_name(): return True if not self.check_pos(): return True self.last_pos = self.position if self.entity_data.entity_type < constants.ENTITY_TYPE_PLAYER_MIN_ID or self.entity_data.entity_type > constants.ENTITY_TYPE_PLAYER_MAX_ID: print '[ANTICHEAT BASE] Player %s tried to join with invalid entity type id: %s!' % (self.name, self.entity_data.entity_type) self.kick('Invalid entity type submitted') return True if self.entity_data.class_type < constants.ENTITY_CLASS_PLAYER_MIN_ID or self.entity_data.class_type > constants.ENTITY_CLASS_PLAYER_MAX_ID : self.kick('Invalid character class submitted') print '[ANTICHEAT BASE] Player %s tried to join with an invalid character class! Kicked.' % self.name return True if self.entity_data.hp > 1000: self.kick('Abnormal health points submitted') print '[ANTICHEAT BASE] Player %s tried to join with an abnormal health points! Kicked.' % self.name return True if self.entity_data.level < 1 or self.entity_data.level > constants.PLAYER_MAX_LEVEL: self.kick('Abnormal level submitted') print '[ANTICHEAT BASE] Player %s tried to join with an abnormal character level! Kicked.' % self.name return True # This seems to filter prevent cheaters from joining needed_xp = get_needed_total_xp(self.entity_data.level) if needed_xp > self.entity_data.current_xp: self.kick('Invalid character level') print '[ANTICHEAT BASE] Player %s tried to join with character level %s that is higher than total xp needed (%s/%s)! Kicked.' % (self.name, self.entity_data.level, self.entity_data.current_xp, needed_xp) return True #if self.entity_data.inventory...... in constants.FORBIDDEN_ITEMS_POSSESSION return False # handlers def on_join(self): if self.connection_state < 0: print '[WARNING] Connection of %s [%s] already has been invalidated before!' % (self.name, self.address.host) self.kick('Blocked join') return if self.connection_state != 2: print '[WARNING] Player %s [%s] tried to join in invalid state!' % (self.name, self.address.host) self.kick('Invalid state') return if self.check_name() is False: self.kick('Bad name') return # Call join script res = self.scripts.call('on_join').result if res is False: self.kick('Blocked join') print '[WARNING] Joining client %s blocked by script!' % self.address.host return if self.entity_data.level < self.server.config.base.join_level_min: print '[WARNING] Level of player %s [%s] is lower than minimum of %s' % (self.name, self.address.host, self.server.config.base.join_level_min) self.kick('Your level has to be at least %s' % self.server.config.base.join_level_min) return if self.entity_data.level > self.server.config.base.join_level_max: print '[WARNING] Level of player %s [%s] is higher than maximum of %s' % (self.name, self.address.host, self.server.config.base.join_level_max) self.kick('Your level has to be lower than %s' % self.server.config.base.join_level_max) return self.last_pos = self.position # we dont want cheaters being able joining the server if self.do_anticheat_actions(): self.server.send_chat('[ANTICHEAT] Player %s (%s) has been kicked for cheating.' % (self.name, get_entity_type_level_str(self.entity_data))) return print '>>> Player %s %s #%s [%s] joined the game' % (self.name, get_entity_type_level_str(self.entity_data), self.entity_id, self.address.host) self.server.send_chat('>>> %s #%s (%s) joined the game' % (self.name, self.entity_id, get_entity_type_level_str(self.entity_data))) # connection successful -> continue for player in self.server.players.values(): entity_packet.set_entity(player.entity_data, player.entity_id) self.send_packet(entity_packet) self.server.players[(self.entity_id,)] = self self.connection_state = 3 def on_command(self, command, parameters): self.scripts.call('on_command', command=command, args=parameters) if ( (not parameters) or (command == 'register') or (command == 'login') ): print '[COMMAND] %s: /%s' % (self.name, command) else: print '[COMMAND] %s: /%s %s' % (self.name, command, ' '.join(parameters)) def on_chat(self, message): if self.time_last_chat < int(reactor.seconds() - constants.ANTISPAM_LIMIT_CHAT): self.chat_messages_burst = 0 else: if self.chat_messages_burst < constants.ANTISPAM_BURST_CHAT: self.chat_messages_burst += 1 else: self.time_last_chat = reactor.seconds() res = self.scripts.call('on_spamming_chat').result if not res: # As we do not want to spam back only do this when # burst limit is reached for the first time if self.chat_messages_burst == constants.ANTISPAM_BURST_CHAT: if self.server.config.base.auto_kick_spam: self.kick('Kicked for chat spamming') else: self.send_chat('[ANTISPAM] Please do not spam in chat!') return if message.startswith('/'): command, args = parse_command(message[1:]) self.on_command(command, args) return event = self.scripts.call('on_chat', message=message) if event.result is False: return return event.message # other methods def send_chat(self, value): packet = ServerChatMessage() packet.entity_id = 0 packet.value = value self.send_packet(packet) def give_item(self, item): action = PickupAction() action.entity_id = self.entity_id action.item_data = item self.server.update_packet.pickups.append(action) def send_lines(self, lines): current_time = 0 for line in lines: reactor.callLater(current_time, self.send_chat, line) current_time += 2 def heal(self, amount=None, reason=None): if (amount is not None and amount <= 0) or (hp >= constants.PLAYER_MAX_HEALTH): return False if self.scripts.call('on_heal', amount, reason).result is False: return False if amount is None or amount + hp > constants.PLAYER_MAX_HEALTH: self.entity_data.hp = constants.PLAYER_MAX_HEALTH else: self.entity_data.hp += amount self.entity_data.changed = True for connection in self.server.connections.values(): entity_packet.set_entity(self.entity_data, self.entity_id) connection.send_packet(entity_packet) if reason is None: self.send_chat('[INFO] You have been healed.') elif reason is not False: self.send_chat(reason) def damage(self, damage=0, critical=0, stun_duration=0, reason=None): if self.scripts.call('on_damage', damage, critical, stun_duration, reason).result is False: return False packet = HitPacket() packet.entity_id = self.entity_id packet.target_id = self.entity_id packet.hit_type = HIT_NORMAL packet.damage = damage packet.critical = critical packet.stun_duration = stun_duration packet.something8 = 0 packet.pos = self.position packet.hit_dir = Vector3() packet.skill_hit = 0 packet.show_light = 0 # Processed by the server and clients in next update task run self.server.update_packet.player_hits.append(packet) self.entity_data.changed = True if reason: self.send_chat(reason) return True def kill(self, killer=None, reason=None): if not damage(self.entity_data.hp + 100, 1, 0): return False if self.scripts.call('on_kill', killer=killer, reason=reason).result is False: return False packet = KillAction() if killer is None: packet.entity_id = self.entity_id else: packet.entity_id = killer.entity_id packet.target_id = self.entity_id packet.xp_gained = 0 # Processed by the server and clients in next update task run self.server.update_packet.kill_actions.append(packet) self.entity_data.changed = True if reason is None: if killer is self: self.send_chat('You commited suicide') else: self.send_chat('You have been killed by %s' % killer.entity_data.name) elif reason is not False: self.send_chat(reason) return True def kick(self, reason=None): res = self.scripts.call('on_kick', reason=reason) if res is False: return if reason is None: self.send_chat('You have been kicked') elif reason is not False: self.send_chat(reason) self.disconnect() if self.entity_data.name: self.server.send_chat('<<< %s has been kicked' % self.entity_data.name) def teleport(self, to_x, to_y, to_z): res = self.scripts.call('on_teleport', pos=self.position) if res is False: return self.entity_data.pos.x = to_x self.entity_data.pos.y = to_y self.entity_data.pos.z = to_z # To not confuse anti cheating system self.last_pos = self.position self.entity_data.changed = True for connection in self.server.connections.values(): entity_packet.set_entity(self.entity_data, self.entity_id) connection.send_packet(entity_packet) self.send_chat('[INFO] You have been teleported.') def check_name(self): if self.old_name is None: return True if not self.name: self.kick('No name') print '[WARNING] %s had no name! Kicked.' % self.address.host return False if len(self.name) > constants.NAME_LENGTH_MAX: self.kick('Name to long') print '[WARNING] %s had name longer than %s characters! Kicked.' % (self.address.host, constants.NAME_LENGTH_MAX) return False self.entity_data.name = self.name.strip() if len(self.name) < constants.NAME_LENGTH_MIN: self.kick('Name to short') print '[WARNING] %s had name shorter than %s characters! Kicked.' % (self.address.host, constants.NAME_LENGTH_MIN) return False if re.search(self.server.config.base.name_filter, self.name) is None: self.kick('Illegal name') print '[WARNING] %s had illegal name! Kicked.' % self.address.host return False return True def check_pos(self): if self.old_pos is not None: if (self.position.x == self.old_pos.x) and (self.position.y == self.old_pos.y) and (self.position.z == self.old_pos.z): return True server = self.server cpres = self.scripts.call('on_pos_update').result if cpres is False: self.entity_data.x = self.old_pos.x self.entity_data.y = self.old_pos.y return True # check new coordinates and distances edist = get_distance_3d(self.old_pos.x, self.old_pos.y, self.old_pos.z, self.position.x, self.position.y, self.position.z) if edist > (reactor.seconds() * constants.MAX_MOVE_DISTANCE): self.entity_data.x = self.old_pos.x self.entity_data.y = self.old_pos.y self.entity_data.z = self.old_pos.z print 'Player %s moved to fast!' % self.name return False cxo = math.floor(self.old_pos.x / constants.CHUNK_SCALE) cyo = math.floor(self.old_pos.y / constants.CHUNK_SCALE) cxn = math.ceil(self.position.x / constants.CHUNK_SCALE) cyn = math.ceil(self.position.y / constants.CHUNK_SCALE) if (cxo != cxn) or (cyo != cyn): self.server.world.move_locatable(self.entity_id, self.position.x, self.position.y, self.position.z) print '%s entered chunk (%s,%s)' % (self.name, cxn, cyn) self.old_pos = self.position return True def check_items(self): server = self.server for slotindex in range(13): item = entity_data.equipment[slotindex] if not item or item.type == 0: continue if item.level < 0: self.kick('Illegal item') print '[INFO] Player %s #%s (%s) [%s] had item with level lover than 0' % (self.entity_data.name, self.entity_id, get_entity_type_level_str(self.entity_data), self.address.host) return False if item.material in self.server.config.base.forbid_item_possession: self.kick('Forbidden item') print '[INFO] Player %s #%s (%s) [%s] had forbidden item #%s' % (self.entity_data.name, self.entity_id, get_entity_type_level_str(self.entity_data), self.address.host, item.material) return False return True # convienience methods @property def position(self): if not self.entity_data.pos: return Vector3() return Vector3(self.entity_data.pos.x, self.entity_data.pos.y, self.entity_data.pos.z) @property def name(self): if not self.entity_data.name: return None return self.entity_data.name
class CubeWorldServer: exit_code = None world = None old_time = None skip_index = 0 def __init__(self, loop, config): self.loop = loop self.config = config base = config.base # game-related self.update_packet = packets.ServerUpdate() self.update_packet.reset() self.connections = set() self.players = MultikeyDict() self.updated_chunks = set() use_same_loop = base.update_fps == base.network_fps if use_same_loop: def update_callback(): self.update() self.send_update() else: update_callback = self.update self.update_loop = LoopingCall(update_callback) self.update_loop.start(1.0 / base.update_fps, now=False) if use_same_loop: self.send_loop = self.update_loop else: self.send_loop = LoopingCall(self.send_update) self.send_loop.start(1.0 / base.network_fps, now=False) self.mission_loop = LoopingCall(self.update_missions) self.mission_loop.start(base.mission_update_rate, now=False) # world self.world = World(self, self.loop, base.seed, use_tgen=base.use_tgen, use_entities=base.use_entities, chunk_retire_time=base.chunk_retire_time, debug=base.world_debug_info) if base.world_debug_file is not None: debug_fp = open(base.world_debug_file, 'wb') self.world.set_debug(debug_fp) # server-related self.git_rev = base.get('git_rev', None) self.passwords = {} for k, v in base.passwords.items(): self.passwords[k.lower()] = v self.scripts = ScriptManager() for script in base.scripts: self.load_script(script) # time self.extra_elapsed_time = 0.0 self.start_time = loop.time() self.set_clock('12:00') # start listening self.loop.set_exception_handler(self.exception_handler) self.loop.create_task( self.create_server(self.build_protocol, port=base.port, family=socket.AF_INET)) def exception_handler(self, loop, context): exception = context.get('exception') if isinstance(exception, TimeoutError): pass else: loop.default_exception_handler(context) def build_protocol(self): return CubeWorldConnection(self) def drop_item(self, item_data, pos): item = packets.ChunkItemData() item.drop_time = 750 # XXX provide sane values for these item.scale = 0.1 item.rotation = 185.0 item.something3 = item.something5 = item.something6 = 0 item.pos = pos item.item_data = item_data self.world.get_chunk(get_chunk(pos)).add_item(item) def add_packet_list(self, items, l, size): for item in iterate_packet_list(l): items.append(item.data.copy()) def handle_tgen_packets(self, in_queue): if in_queue is None: return p = self.update_packet self.add_packet_list(p.player_hits, in_queue.player_hits, in_queue.player_hits_size) self.add_packet_list(p.sound_actions, in_queue.sound_actions, in_queue.sound_actions_size) self.add_packet_list(p.particles, in_queue.particles, in_queue.particles_size) self.add_packet_list(p.block_actions, in_queue.block_actions, in_queue.block_actions_size) self.add_packet_list(p.shoot_actions, in_queue.shoot_packets, in_queue.shoot_packets_size) self.add_packet_list(p.kill_actions, in_queue.kill_actions, in_queue.kill_actions_size) self.add_packet_list(p.damage_actions, in_queue.damage_actions, in_queue.damage_actions_size) self.add_packet_list(p.passive_actions, in_queue.passive_packets, in_queue.passive_packets_size) self.add_packet_list(p.missions, in_queue.missions, in_queue.missions_size) def update_missions(self): max_dist = self.config.base.mission_max_distance p = self.update_packet added = set() for connection in self.players.values(): player_entity = connection.entity if player_entity is None: continue min_pos = (player_entity.pos - max_dist) // constants.MISSION_SCALE max_pos = (player_entity.pos + max_dist) // constants.MISSION_SCALE for x in range(min_pos.x, max_pos.x): for y in range(min_pos.y, max_pos.y): if (x, y) in added: continue added.add((x, y)) reg_x = x // constants.MISSIONS_IN_REGION reg_y = y // constants.MISSIONS_IN_REGION try: reg = self.world.get_region((reg_x, reg_y)) except KeyError: continue local_x = x % constants.MISSIONS_IN_REGION local_y = y % constants.MISSIONS_IN_REGION try: m = reg.get_mission((local_x, local_y)) except (IndexError, ValueError): continue mission_packet = packets.MissionPacket() mission_packet.x = x mission_packet.y = y mission_packet.something1 = 0 mission_packet.something2 = 0 mission_packet.info = m.info p.missions.append(mission_packet) def send_entity_data(self, entity): base = self.config.base # full entity packet for new, close players entity_packet.set_entity(entity, entity.entity_id) full = packets.write_packet(entity_packet) # pos entity packet if not entity.is_tgen: entity_packet.set_entity(entity, entity.entity_id, entitydata.POS_FLAG) only_pos = packets.write_packet(entity_packet) # reduced rate packet skip_reduced = self.skip_index != 0 entity_packet.set_entity(entity, entity.entity_id, entitydata.POS_FLAG) reduced = packets.write_packet(entity_packet) max_distance = base.max_distance max_reduce_distance = base.max_reduce_distance old_close_players = entity.close_players new_close_players = {} for connection in self.players.values(): player_entity = connection.entity if player_entity is None: continue if entity is player_entity: continue if entity.full_update: connection.send_data(full) new_close_players[connection] = entity.copy() continue dist = (player_entity.pos - entity.pos).length if dist > max_distance: if not entity.is_tgen: connection.send_data(only_pos) continue old_ref = old_close_players.get(connection, None) if old_ref is None: connection.send_data(full) new_close_players[connection] = entity.copy() continue if dist > max_reduce_distance and skip_reduced: connection.send_data(reduced) new_close_players[connection] = old_ref continue new_mask = entitydata.get_mask(old_ref, entity) entity_packet.set_entity(entity, entity.entity_id, new_mask) connection.send_packet(entity_packet) new_close_players[connection] = entity.copy() entity.close_players = new_close_players entity.full_update = False def update(self): self.scripts.call('update') out_packets = self.world.update(self.update_loop.dt) self.handle_tgen_packets(out_packets) def send_update(self): self.skip_index = (self.skip_index + 1) % self.config.base.reduce_skip for entity in self.world.entities.values(): self.send_entity_data(entity) self.broadcast_packet(update_finished_packet) # other updates update_packet = self.update_packet for chunk in self.updated_chunks: chunk.on_update(update_packet) if not update_packet.is_empty(): self.broadcast_packet(update_packet) update_packet.reset() # reset drop times for chunk in self.updated_chunks: chunk.on_post_update() self.updated_chunks.clear() # time update new_time = (self.get_time(), self.get_day()) if new_time != self.old_time: time_packet.time = new_time[0] time_packet.day = new_time[1] self.broadcast_packet(time_packet) self.old_time = new_time def send_chat(self, value): chat_packet.entity_id = 0 chat_packet.value = value self.broadcast_packet(chat_packet) def play_sound(self, name, pos=None, pitch=1.0, volume=1.0): sound = packets.SoundAction() sound.set_name(name) sound.pitch = pitch sound.volume = volume if pos is not None: sound.pos = pos self.update_packet.sound_action.append(sound) return extra_server_update.reset() for player in self.players.values(): sound.pos = player.entity.pos extra_server_update.sound_actions = [sound] player.send_packet(extra_server_update) def broadcast_packet(self, packet): data = packets.write_packet(packet) for player in self.players.values(): player.send_data(data) # line/string formatting options based on config def format(self, value): format_dict = {'server_name': self.config.base.server_name} return value % format_dict def format_lines(self, value): lines = [] for line in value: lines.append(self.format(line)) return lines # script methods def load_script(self, name, update=False): try: return self.scripts[name] except KeyError: pass try: mod = __import__('scripts.%s' % name, globals(), locals(), [name]) if update: importlib.reload(mod) except ImportError as e: traceback.print_exc() return None script = mod.get_class()(self) print('Loaded script %r' % name) return script def unload_script(self, name): try: self.scripts[name].unload() except KeyError: return False print('Unloaded script %r' % name) return True def call_command(self, user, command, args): """ Calls a command from an external interface, e.g. IRC, console """ return self.scripts.call('on_command', user=user, command=command, args=args).result def get_mode(self): return self.scripts.call('get_mode').result # command convenience methods (for /help) def get_commands(self): for script in self.scripts.get(): if script.commands is None: continue for command in script.commands.values(): yield command def get_command(self, name): for script in self.scripts.get(): if script.commands is None: continue name = script.aliases.get(name, name) command = script.commands.get(name, None) if command: return command # binary data store methods def load_data(self, name, default=None): path = os.path.join(self.config.base.save_path, f'{name}.dat') try: with open(path, 'r', newline=None) as fp: data = fp.read() except IOError: return default return eval(data) def save_data(self, name, value): os.makedirs(self.config.base.save_path, exist_ok=True) path = os.path.join(self.config.base.save_path, f'{name}.dat') data = pprint.pformat(value, width=1) with open(path, 'w') as fp: fp.write(data) # time methods def set_clock(self, value): day = self.get_day() time = parse_clock(value) self.start_time = self.loop.time() self.extra_elapsed_time = day * constants.MAX_TIME + time def get_elapsed_time(self): dt = self.loop.time() - self.start_time dt *= self.config.base.time_modifier * constants.NORMAL_TIME_SPEED return dt * 1000 + self.extra_elapsed_time def get_time(self): return int(self.get_elapsed_time() % constants.MAX_TIME) def get_day(self): return int(self.get_elapsed_time() / constants.MAX_TIME) def get_clock(self): return get_clock_string(self.get_time()) # stop/restart def stop(self, code=None): print('Stopping...') self.exit_code = code if self.world: self.world.stop() self.scripts.unload() self.loop.stop() # asyncio wrappers def get_interface(self): return self.config.base.network_interface def create_datagram_endpoint(self, *arg, port=0, **kw): host = self.get_interface() addr = (host, port) return self.loop.create_datagram_endpoint(*arg, local_addr=addr, **kw) def create_server(self, *arg, **kw): return self.loop.create_server(*arg, host=self.get_interface(), **kw) def connect_connection(self, *arg, **kw): host = self.get_interface() return self.loop.create_connection(*arg, local_addr=(host, 0), **kw)
class CubeWorldConnection(Protocol): """ Protocol used for players """ relay_client = None relay_packets = None has_joined = False entity_id = None entity_data = None login_id = None rank = None disconnected = False scripts = None chunk = None old_pos = None old_health = None old_level = None old_xp = None def __init__(self, server, addr): self.address = addr self.server = server self.relay_packets = [] # connection methods def got_relay_client(self, p): self.relay_client = p for data in self.relay_packets: self.relay_client.transport.write(data) self.relay_packets = None print 'Relaying Client Packets.' def connectionMade(self): self.transport.setTcpNoDelay(True) server = self.server self.client_packet_handler = PacketHandler(CS_PACKETS, self.on_client_packet) self.server_packet_handler = PacketHandler(SC_PACKETS, self.on_server_packet) server.connections.add(self) self.rights = AttributeSet() self.scripts = ScriptManager() server.scripts.call('on_new_connection', connection=self) point = TCP4ClientEndpoint(reactor, self.server.config.base.mitm_ip, self.server.config.base.mitm_port) d = point.connect(RelayFactory(self)) d.addCallback(self.got_relay_client) def serverDataReceived(self, data): self.server_packet_handler.feed(data) def dataReceived(self, data): self.client_packet_handler.feed(data) def disconnect(self, reason=None): self.transport.loseConnection() self.connectionLost(reason) def connectionLost(self, reason): if self.relay_client is not None: self.relay_client.transport.loseConnection() if self.disconnected: return self.disconnected = True if self.login_id is not None: database.update_online_seconds(self.server.db_con, self.login_id) self.server.connections.discard(self) if self.has_joined: del self.server.players[self] print '[INFO] Player %s #%s left the game.' % (self.name, self.entity_id) self.server.send_chat('<<< %s #%s left the game' % (self.name, self.entity_id)) if self.entity_data is not None: del self.server.entities[self.entity_id] if self.scripts is not None: self.scripts.unload() # packet methods def send_packet(self, packet): self.transport.write(write_packet(packet)) def relay_packet(self, packet): if self.relay_client is None: self.relay_packets.append(write_packet(packet)) else: self.relay_client.transport.write(write_packet(packet)) def on_server_packet(self, packet): if packet.packet_id == EntityUpdate.packet_id: if packet.entity_id == self.entity_id: self.on_entity_packet(packet) elif packet.packet_id == JoinPacket.packet_id: self.entity_id = packet.entity_id self.send_packet(packet) def on_client_packet(self, packet): if self.disconnected: return if packet is None: print 'Invalid packet received' self.disconnect() raise StopIteration() if packet.packet_id == EntityUpdate.packet_id: if self.on_entity_packet(packet) is True: self.relay_packet(packet) elif packet.packet_id == ClientChatMessage.packet_id: self.on_chat_packet(packet) elif packet.packet_id == InteractPacket.packet_id: self.on_interact_packet(packet) elif packet.packet_id == HitPacket.packet_id: self.on_hit_packet(packet) elif packet.packet_id == ShootPacket.packet_id: self.on_shoot_packet(packet) else: self.relay_packet(packet) def on_entity_packet(self, packet): if self.entity_id is None: return True if self.entity_data is None: self.entity_data = create_entity_data() self.server.entities[self.entity_id] = self.entity_data mask = packet.update_entity(self.entity_data) self.entity_data.mask |= mask if not self.has_joined and getattr(self.entity_data, 'name', None): self.on_join() return True result = True self.scripts.call('on_entity_update', mask=mask) # XXX clean this up if entity.is_pos_set(mask): if self.on_pos_update() is False: result = False if entity.is_mode_set(mask): self.scripts.call('on_mode_update') if entity.is_class_set(mask): self.scripts.call('on_class_update') if entity.is_name_set(mask): self.scripts.call('on_name_update') if entity.is_multiplier_set(mask): self.scripts.call('on_multiplier_update') if entity.is_level_set(mask): self.scripts.call('on_level_update') if entity.is_equipment_set(mask): self.scripts.call('on_equipment_update') if entity.is_skill_set(mask): self.scripts.call('on_skill_update') if entity.is_appearance_set(mask): self.scripts.call('on_appearance_update') if entity.is_charged_mp_set(mask): self.scripts.call('on_charged_mp_update') if entity.is_flags_set(mask): self.scripts.call('on_flags_update') if entity.is_consumable_set(mask): self.scripts.call('on_consumable_update') return result def on_pos_update(self): chunk = get_chunk(self.position) if self.chunk is None: self.chunk = chunk elif chunk != self.chunk: # Distance check if (abs(chunk[0]-self.chunk[0]) > 1) or (abs(chunk[1]-self.chunk[1]) > 1): self.disconnect('[ANTICHEAT] Traveled distance to large') print '[ANTICHEAT] Traveled distance of %s was to large' % self.name return False if abs(chunk[0]) < 2 or abs(chunk[1]) < 2: self.disconnect('[ANTICHEAT] Out of world border') self.teleport(550301073408, 550301073408, 1000000) print '[ANTICHEAT] %s was out of world border' % self.name return False self.chunk = chunk self.scripts.call('on_pos_update') return True def on_chat_packet(self, packet): message = filter_string(packet.value).strip() if not message: return message = self.on_chat(message) if not message: return chat_packet.entity_id = self.entity_id chat_packet.value = message self.server.broadcast_packet(chat_packet) print '[CHAT] %s: %s' % (self.name, message) def on_interact_packet(self, packet): interact_type = packet.interact_type item = packet.item_data if interact_type == INTERACT_DROP: pos = self.position.copy() if self.scripts.call('on_drop', item=item, pos=pos).result is False: return elif interact_type == INTERACT_PICKUP: pos = self.position.copy() if self.scripts.call('on_pickup', item=item, pos=pos).result is False: return self.relay_packet(packet) def on_hit_packet(self, packet): self.relay_packet(packet) try: target = self.server.entities[packet.target_id] except KeyError: return if self.scripts.call('on_hit', target=target, packet=packet).result is False: return #self.server.update_packet.player_hits.append(packet) if target.hp <= 0: return target.hp -= packet.damage if target.hp <= 0: self.scripts.call('on_kill', target=target) def on_shoot_packet(self, packet): self.relay_packet(packet) # handlers def on_join(self): if self.scripts.call('on_join').result is False: return False print '[INFO] Player %s joined the game at %s' % (self.name, self.position) self.server.send_chat('>>> %s #%s joined the game' % (self.name, self.entity_id)) self.server.players[(self.entity_id,)] = self self.has_joined = True return True def on_command(self, command, parameters): self.scripts.call('on_command', command=command, args=parameters) if ((not parameters) or (command == 'register') or (command == 'login')): print '[COMMAND] %s: /%s' % (self.name, command) else: print '[COMMAND] %s: /%s %s' % (self.name, command, ' '.join(parameters)) def on_chat(self, message): if message.startswith('/'): command, args = parse_command(message[1:]) self.on_command(command, args) return event = self.scripts.call('on_chat', message=message) if event.result is False: return return event.message # other methods def send_chat(self, value): packet = ServerChatMessage() packet.entity_id = 0 packet.value = value self.send_packet(packet) def give_item(self, item): action = PickupAction() action.entity_id = self.entity_id action.item_data = item self.server.update_packet.pickups.append(action) def send_lines(self, lines): current_time = 0 for line in lines: reactor.callLater(current_time, self.send_chat, line) current_time += 2 def heal(self, amount=None): if amount is not None and amount <= 0: return False packet = EntityUpdate() if amount is None or amount + self.entity_data.hp > 1000: self.entity_data.hp = 1000 else: self.entity_data.hp += amount packet.set_entity(self.entity_data, self.entity_id) self.relay_packet(packet) packet.set_entity(self.entity_data, 0) self.send_packet(packet) def kick(self): self.send_chat('You have been kicked') self.disconnect() self.server.send_chat('[INFO] %s has been kicked' % self.name) def teleport(self, to_x, to_y, to_z): packet = EntityUpdate() self.entity_data.pos.x = to_x self.entity_data.pos.y = to_y self.entity_data.pos.z = to_z self.chunk = get_chunk(self.entity_data.pos) self.old_pos = self.entity_data.pos packet.set_entity(self.entity_data, 0) self.send_packet(packet) packet.set_entity(self.entity_data, self.entity_id) self.relay_packet(packet) # convienience methods @property def position(self): if self.entity_data is None: return None return self.entity_data.pos @property def name(self): if self.entity_data is None: return None return self.entity_data.name
class CubeWorldConnection(Protocol): """ Protocol used for players """ has_joined = False entity_id = None entity_data = None disconnected = False scripts = None def __init__(self, server, addr): self.address = addr self.server = server # connection methods def connectionMade(self): server = self.server if len(server.connections) >= server.config.base.max_players: self.send_packet(server_full_packet) self.disconnect() return self.packet_handlers = { ClientVersion.packet_id: self.on_version_packet, EntityUpdate.packet_id: self.on_entity_packet, ClientChatMessage.packet_id: self.on_chat_packet, InteractPacket.packet_id: self.on_interact_packet, HitPacket.packet_id: self.on_hit_packet, ShootPacket.packet_id: self.on_shoot_packet } self.packet_handler = PacketHandler(CS_PACKETS, self.on_packet) server.connections.add(self) self.rights = AttributeSet() self.scripts = ScriptManager() server.scripts.call('on_new_connection', connection=self) def dataReceived(self, data): self.packet_handler.feed(data) def disconnect(self, reason=None): self.transport.loseConnection() self.connectionLost(reason) def connectionLost(self, reason): if self.disconnected: return self.disconnected = True self.server.connections.discard(self) if self.has_joined: del self.server.players[self] print 'Player %s left' % self.name if self.entity_data is not None: del self.server.entities[self.entity_id] if self.entity_id is not None: self.server.entity_ids.put_back(self.entity_id) if self.scripts is not None: self.scripts.unload() # packet methods def send_packet(self, packet): self.transport.write(write_packet(packet)) def on_packet(self, packet): if self.disconnected: return if packet is None: print 'Invalid packet received' self.disconnect() raise StopIteration() handler = self.packet_handlers.get(packet.packet_id, None) if handler is None: # print 'Unhandled client packet: %s' % packet.packet_id return handler(packet) def on_version_packet(self, packet): if packet.version != constants.CLIENT_VERSION: mismatch_packet.version = constants.CLIENT_VERSION self.send_packet(mismatch_packet) self.disconnect() return server = self.server self.entity_id = server.entity_ids.pop() join_packet.entity_id = self.entity_id self.send_packet(join_packet) seed_packet.seed = server.config.base.seed self.send_packet(seed_packet) def on_entity_packet(self, packet): if self.entity_data is None: self.entity_data = create_entity_data() self.server.entities[self.entity_id] = self.entity_data mask = packet.update_entity(self.entity_data) self.entity_data.mask |= mask if not self.has_joined and getattr(self.entity_data, 'name', None): self.on_join() return self.scripts.call('on_entity_update', mask=mask) # XXX clean this up if entity.is_pos_set(mask): self.scripts.call('on_pos_update') if entity.is_mode_set(mask): self.scripts.call('on_mode_update') if entity.is_class_set(mask): self.scripts.call('on_class_update') if entity.is_name_set(mask): self.scripts.call('on_name_update') if entity.is_multiplier_set(mask): self.scripts.call('on_multiplier_update') if entity.is_level_set(mask): self.scripts.call('on_level_update') if entity.is_equipment_set(mask): self.scripts.call('on_equipment_update') if entity.is_skill_set(mask): self.scripts.call('on_skill_update') if entity.is_appearance_set(mask): self.scripts.call('on_appearance_update') if entity.is_charged_mp_set(mask): self.scripts.call('on_charged_mp_update') if entity.is_flags_set(mask): self.scripts.call('on_flags_update') if entity.is_consumable_set(mask): self.scripts.call('on_consumable_update') def on_chat_packet(self, packet): message = filter_string(packet.value).strip() if not message: return message = self.on_chat(message) if not message: return chat_packet.entity_id = self.entity_id chat_packet.value = message self.server.broadcast_packet(chat_packet) print '%s: %s' % (self.name, message) def on_interact_packet(self, packet): interact_type = packet.interact_type item = packet.item_data if interact_type == INTERACT_DROP: pos = self.position.copy() pos.z -= constants.BLOCK_SCALE if self.scripts.call('on_drop', item=item, pos=pos).result is False: return self.server.drop_item(packet.item_data, pos) elif interact_type == INTERACT_PICKUP: chunk = (packet.chunk_x, packet.chunk_y) try: item = self.server.remove_item(chunk, packet.item_index) except IndexError: return self.give_item(item) def on_hit_packet(self, packet): try: target = self.server.entities[packet.target_id] except KeyError: return if self.scripts.call('on_hit', target=target, packet=packet).result is False: return self.server.update_packet.player_hits.append(packet) if target.hp <= 0: return target.hp -= packet.damage if target.hp <= 0: self.scripts.call('on_kill', target=target) def on_shoot_packet(self, packet): self.server.update_packet.shoot_actions.append(packet) # handlers def on_join(self): if self.scripts.call('on_join').result is False: return print 'Player %s joined' % self.name for player in self.server.players.values(): entity_packet.set_entity(player.entity_data, player.entity_id) self.send_packet(entity_packet) self.server.players[(self.entity_id, )] = self self.has_joined = True def on_command(self, command, parameters): self.scripts.call('on_command', command=command, args=parameters) def on_chat(self, message): if message.startswith('/'): command, args = parse_command(message[1:]) self.on_command(command, args) return event = self.scripts.call('on_chat', message=message) if event.result is False: return return event.message # other methods def send_chat(self, value): packet = ServerChatMessage() packet.entity_id = 0 packet.value = value self.send_packet(packet) def give_item(self, item): action = PickupAction() action.entity_id = self.entity_id action.item_data = item self.server.update_packet.pickups.append(action) def send_lines(self, lines): current_time = 0 for line in lines: reactor.callLater(current_time, self.send_chat, line) current_time += 2 def kick(self): self.send_chat('You have been kicked') self.disconnect() self.server.send_chat('%s has been kicked' % self.name) # convienience methods @property def position(self): if self.entity_data is None: return None return self.entity_data.pos @property def name(self): if self.entity_data is None: return None return self.entity_data.name