Example #1
0
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)
Example #2
0
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
Example #3
0
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
Example #4
0
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)
Example #5
0
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)
Example #6
0
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
Example #7
0
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
Example #8
0
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)
Example #9
0
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
Example #10
0
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