/*
 *    __     ______     ______     __  __     __   __     ______     ______  
 *   /\ \   /\  == \   /\  __ \   /\ \/ /    /\ "-.\ \   /\  ___\   /\__  _\
 *  _\_\ \  \ \  __<   \ \  __ \  \ \  _"-.  \ \ \-.  \  \ \  __\   \/_/\ \/  
 * /\_____\  \ \_\ \_\  \ \_\ \_\  \ \_\ \_\  \ \_\\"\_\  \ \_____\    \ \_\ 
 * \/_____/   \/_/ /_/   \/_/\/_/   \/_/\/_/   \/_/ \/_/   \/_____/     \/_/                                                                          
 *
 * the MIT License (MIT)
 *
 * Copyright (c) 2016-2020 "Whirvis" Trent Summerlin
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * the above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.whirvis.jraknet.server;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.whirvis.jraknet.InvalidChannelException;
import com.whirvis.jraknet.Packet;
import com.whirvis.jraknet.RakNet;
import com.whirvis.jraknet.RakNetException;
import com.whirvis.jraknet.RakNetPacket;
import com.whirvis.jraknet.ThreadedListener;
import com.whirvis.jraknet.client.RakNetClient;
import com.whirvis.jraknet.identifier.Identifier;
import com.whirvis.jraknet.peer.RakNetClientPeer;
import com.whirvis.jraknet.protocol.Reliability;
import com.whirvis.jraknet.protocol.connection.ConnectionBanned;
import com.whirvis.jraknet.protocol.connection.IncompatibleProtocolVersion;
import com.whirvis.jraknet.protocol.connection.OpenConnectionRequestOne;
import com.whirvis.jraknet.protocol.connection.OpenConnectionRequestTwo;
import com.whirvis.jraknet.protocol.connection.OpenConnectionResponseOne;
import com.whirvis.jraknet.protocol.connection.OpenConnectionResponseTwo;
import com.whirvis.jraknet.protocol.message.EncapsulatedPacket;
import com.whirvis.jraknet.protocol.status.UnconnectedPing;
import com.whirvis.jraknet.protocol.status.UnconnectedPong;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.FixedRecvByteBufAllocator;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramPacket;
import io.netty.channel.socket.nio.NioDatagramChannel;

/**
 * Used to create servers using the RakNet protocol.
 *
 * @author "Whirvis" Trent Summerlin
 * @since JRakNet v1.0.0
 */
public class RakNetServer implements RakNetServerListener {

	/**
	 * No globally unique ID for the
	 * {@link #validateSender(InetSocketAddress, long)} method.
	 */
	private static final int NO_GUID = -1;

	/**
	 * Has the maximum transfer unit automatically determined during startup.
	 */
	public static final int AUTOMATIC_MTU = -1;

	/**
	 * Allows for infinite connections to the server.
	 */
	public static final int INFINITE_CONNECTIONS = -1;

	private final InetSocketAddress bindingAddress;
	private final long guid;
	private final Logger logger;
	private final long pongId;
	private final long timestamp;
	private final int maximumTransferUnit;
	private int maxConnections;
	private boolean broadcastingEnabled;
	private Identifier identifier;
	private int eventThreadCount;
	private final ConcurrentLinkedQueue<RakNetServerListener> listeners;
	private final ConcurrentHashMap<InetSocketAddress, RakNetClientPeer> clients;
	private final ConcurrentLinkedQueue<InetAddress> banned;
	private Bootstrap bootstrap;
	private EventLoopGroup group;
	private RakNetServerHandler handler;
	private Channel channel;
	private InetSocketAddress bindAddress;
	private Thread peerThread;
	private volatile boolean running;

	/**
	 * Creates a RakNet server.
	 * 
	 * @param address
	 *            the address the server will bind to during startup. A
	 *            <code>null</code> IP address will have the server bind to the
	 *            wildcard address.
	 * @param maximumTransferUnit
	 *            the highest maximum transfer unit a client can use. The
	 *            maximum transfer unit is the maximum number of bytes that can
	 *            be sent in one packet. If a packet exceeds this size, it is
	 *            automatically split up so that it can still be sent over the
	 *            connection (this is handled automatically by
	 *            {@link com.whirvis.jraknet.peer.RakNetPeer RakNetPeer}). A
	 *            value of {@value #AUTOMATIC_MTU} will have the maximum
	 *            transfer unit be determined automatically via
	 *            {@link RakNet#getMaximumTransferUnit(InetAddress)} with the
	 *            parameter being the specified bind <code>address</code>.
	 * @param maxConnections
	 *            the maximum number of connections, A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @param identifier
	 *            the identifier that will be sent in response to server pings
	 *            if server broadcasting is enabled. A <code>null</code>
	 *            identifier means nothing will be sent in response to server
	 *            pings, even if server broadcasting is enabled.
	 * @throws NullPointerException
	 *             if the address is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the maximum connections is less than <code>0</code> and
	 *             not equal to {@value #INFINITE_CONNECTIONS} or the maximum
	 *             transfer unit size is less than
	 *             {@value RakNet#MINIMUM_MTU_SIZE} and is not equal to
	 *             {@value #AUTOMATIC_MTU}.
	 */
	public RakNetServer(InetSocketAddress address, int maximumTransferUnit, int maxConnections, Identifier identifier)
			throws NullPointerException, IllegalArgumentException {
		if (address == null) {
			throw new NullPointerException("Address cannot be null");
		} else if (maximumTransferUnit < RakNet.MINIMUM_MTU_SIZE && maximumTransferUnit != AUTOMATIC_MTU) {
			throw new IllegalArgumentException(
					"Maximum transfer unit must be no smaller than " + RakNet.MINIMUM_MTU_SIZE + " or equal to "
							+ AUTOMATIC_MTU + " for the maximum transfer unit to be determined automatically");
		} else if (maxConnections < 0 && maxConnections != INFINITE_CONNECTIONS) {
			throw new IllegalArgumentException("Maximum connections must be greater than or equal to 0 or "
					+ INFINITE_CONNECTIONS + " for infinite connections");
		}
		UUID uuid = UUID.randomUUID();
		this.bindingAddress = address;
		this.guid = uuid.getMostSignificantBits();
		this.logger = LogManager
				.getLogger(RakNetServer.class.getSimpleName() + "[" + Long.toHexString(guid).toUpperCase() + "]");
		this.pongId = uuid.getLeastSignificantBits();
		this.timestamp = System.currentTimeMillis();
		this.maxConnections = maxConnections;
		this.maximumTransferUnit = maximumTransferUnit == AUTOMATIC_MTU ? RakNet.getMaximumTransferUnit(address)
				: maximumTransferUnit;
		this.broadcastingEnabled = true;
		this.identifier = identifier;
		this.listeners = new ConcurrentLinkedQueue<RakNetServerListener>();
		this.clients = new ConcurrentHashMap<InetSocketAddress, RakNetClientPeer>();
		this.banned = new ConcurrentLinkedQueue<InetAddress>();
		if (this.getClass() != RakNetServer.class && RakNetServerListener.class.isAssignableFrom(this.getClass())) {
			this.addSelfListener();
		}
	}

	/**
	 * Creates a RakNet server.
	 * 
	 * @param address
	 *            the address the server will bind to during startup. A
	 *            <code>null</code> IP address will have the server bind to the
	 *            wildcard address.
	 * @param maxConnections
	 *            the maximum number of connections, A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @param identifier
	 *            the identifier that will be sent in response to server pings
	 *            if server broadcasting is enabled. A <code>null</code>
	 *            identifier means nothing will be sent in response to server
	 *            pings, even if server broadcasting is enabled.
	 * @throws NullPointerException
	 *             if the address is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the maximum connections is less than <code>0</code> and
	 *             not equal to {@value #INFINITE_CONNECTIONS}.
	 */
	public RakNetServer(InetSocketAddress address, int maxConnections, Identifier identifier)
			throws NullPointerException, IllegalArgumentException {
		this(address, AUTOMATIC_MTU, maxConnections, identifier);
	}

	/**
	 * Creates a RakNet server.
	 * 
	 * @param address
	 *            the address the server will bind to during startup. A
	 *            <code>null</code> IP address will have the server bind to the
	 *            wildcard address.
	 * @param maximumTransferUnit
	 *            the highest maximum transfer unit a client can use. The
	 *            maximum transfer unit is the maximum number of bytes that can
	 *            be sent in one packet. If a packet exceeds this size, it is
	 *            automatically split up so that it can still be sent over the
	 *            connection (this is handled automatically by
	 *            {@link com.whirvis.jraknet.peer.RakNetPeer RakNetPeer}). A
	 *            value of {@value #AUTOMATIC_MTU} will have the maximum
	 *            transfer unit be determined automatically via
	 *            {@link RakNet#getMaximumTransferUnit(InetAddress)} with the
	 *            parameter being the specified bind <code>address</code>.
	 * @param maxConnections
	 *            the maximum number of connections, A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @throws NullPointerException
	 *             if the address is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the maximum connections is less than <code>0</code> and
	 *             not equal to {@value #INFINITE_CONNECTIONS} or the maximum
	 *             transfer unit size is less than
	 *             {@value RakNet#MINIMUM_MTU_SIZE} and is not equal to
	 *             {@value #AUTOMATIC_MTU}.
	 */
	public RakNetServer(InetSocketAddress address, int maximumTransferUnit, int maxConnections)
			throws NullPointerException, IllegalArgumentException {
		this(address, maximumTransferUnit, maxConnections, null);
	}

	/**
	 * Creates a RakNet server.
	 * 
	 * @param address
	 *            the address the server will bind to during startup. A
	 *            <code>null</code> IP address will have the server bind to the
	 *            wildcard address.
	 * @param maxConnections
	 *            the maximum number of connections, A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @throws NullPointerException
	 *             if the address is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the maximum connections is less than <code>0</code> and
	 *             not equal to {@value #INFINITE_CONNECTIONS}.
	 */
	public RakNetServer(InetSocketAddress address, int maxConnections)
			throws NullPointerException, IllegalArgumentException {
		this(address, AUTOMATIC_MTU, maxConnections);
	}

	/**
	 * Creates a RakNet server.
	 * 
	 * @param address
	 *            the IP address the server will bind to during startup. A
	 *            <code>null</code> IP address will have the server bind to the
	 *            wildcard address.
	 * @param port
	 *            the port the server will bind to during startup.
	 * @param maximumTransferUnit
	 *            the highest maximum transfer unit a client can use. The
	 *            maximum transfer unit is the maximum number of bytes that can
	 *            be sent in one packet. If a packet exceeds this size, it is
	 *            automatically split up so that it can still be sent over the
	 *            connection (this is handled automatically by
	 *            {@link com.whirvis.jraknet.peer.RakNetPeer RakNetPeer}). A
	 *            value of {@value #AUTOMATIC_MTU} will have the maximum
	 *            transfer unit be determined automatically via
	 *            {@link RakNet#getMaximumTransferUnit(InetAddress)} with the
	 *            parameter being the specified bind <code>address</code>.
	 * @param maxConnections
	 *            the maximum number of connections, A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @param identifier
	 *            the identifier that will be sent in response to server pings
	 *            if server broadcasting is enabled. A <code>null</code>
	 *            identifier means nothing will be sent in response to server
	 *            pings, even if server broadcasting is enabled.
	 * @throws NullPointerException
	 *             if the address is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the maximum connections is less than <code>0</code> and
	 *             not equal to {@value #INFINITE_CONNECTIONS} or the maximum
	 *             transfer unit size is less than
	 *             {@value RakNet#MINIMUM_MTU_SIZE} and is not equal to
	 *             {@value #AUTOMATIC_MTU}.
	 */
	public RakNetServer(InetAddress address, int port, int maximumTransferUnit, int maxConnections,
			Identifier identifier) throws NullPointerException, IllegalArgumentException {
		this(new InetSocketAddress(address, port), maximumTransferUnit, maxConnections, identifier);
	}

	/**
	 * Creates a RakNet server.
	 * 
	 * @param address
	 *            the IP address the server will bind to during startup. A
	 *            <code>null</code> IP address will have the server bind to the
	 *            wildcard address.
	 * @param port
	 *            the port the server will bind to during startup.
	 * @param maxConnections
	 *            the maximum number of connections, A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @param identifier
	 *            the identifier that will be sent in response to server pings
	 *            if server broadcasting is enabled. A <code>null</code>
	 *            identifier means nothing will be sent in response to server
	 *            pings, even if server broadcasting is enabled.
	 * @throws NullPointerException
	 *             if the address is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the maximum connections is less than <code>0</code> and
	 *             not equal to {@value #INFINITE_CONNECTIONS}.
	 */
	public RakNetServer(InetAddress address, int port, int maxConnections, Identifier identifier)
			throws NullPointerException, IllegalArgumentException {
		this(address, port, AUTOMATIC_MTU, maxConnections, identifier);
	}

	/**
	 * Creates a RakNet server.
	 * 
	 * @param address
	 *            the IP address the server will bind to during startup. A
	 *            <code>null</code> IP address will have the server bind to the
	 *            wildcard address.
	 * @param port
	 *            the port the server will bind to during startup.
	 * @param maximumTransferUnit
	 *            the highest maximum transfer unit a client can use. The
	 *            maximum transfer unit is the maximum number of bytes that can
	 *            be sent in one packet. If a packet exceeds this size, it is
	 *            automatically split up so that it can still be sent over the
	 *            connection (this is handled automatically by
	 *            {@link com.whirvis.jraknet.peer.RakNetPeer RakNetPeer}). A
	 *            value of {@value #AUTOMATIC_MTU} will have the maximum
	 *            transfer unit be determined automatically via
	 *            {@link RakNet#getMaximumTransferUnit(InetAddress)} with the
	 *            parameter being the specified bind <code>address</code>.
	 * @param maxConnections
	 *            the maximum number of connections, A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @throws NullPointerException
	 *             if the address is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the maximum connections is less than <code>0</code> and
	 *             not equal to {@value #INFINITE_CONNECTIONS} or the maximum
	 *             transfer unit size is less than
	 *             {@value RakNet#MINIMUM_MTU_SIZE} and is not equal to
	 *             {@value #AUTOMATIC_MTU}.
	 */
	public RakNetServer(InetAddress address, int port, int maximumTransferUnit, int maxConnections)
			throws NullPointerException, IllegalArgumentException {
		this(address, port, maximumTransferUnit, maxConnections, null);
	}

	/**
	 * Creates a RakNet server.
	 * 
	 * @param address
	 *            the IP address the server will bind to during startup. A
	 *            <code>null</code> IP address will have the server bind to the
	 *            wildcard address.
	 * @param port
	 *            the port the server will bind to during startup.
	 * @param maxConnections
	 *            the maximum number of connections, A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @throws NullPointerException
	 *             if the address is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the maximum connections is less than <code>0</code> and
	 *             not equal to {@value #INFINITE_CONNECTIONS}.
	 */
	public RakNetServer(InetAddress address, int port, int maxConnections)
			throws NullPointerException, IllegalArgumentException {
		this(address, port, AUTOMATIC_MTU, maxConnections);
	}

	/**
	 * Creates a RakNet server.
	 * 
	 * @param host
	 *            the IP address the server will bind to during startup. A
	 *            <code>null</code> IP address will have the server bind to the
	 *            wildcard address.
	 * @param port
	 *            the port the server will bind to during startup.
	 * @param maximumTransferUnit
	 *            the highest maximum transfer unit a client can use. The
	 *            maximum transfer unit is the maximum number of bytes that can
	 *            be sent in one packet. If a packet exceeds this size, it is
	 *            automatically split up so that it can still be sent over the
	 *            connection (this is handled automatically by
	 *            {@link com.whirvis.jraknet.peer.RakNetPeer RakNetPeer}). A
	 *            value of {@value #AUTOMATIC_MTU} will have the maximum
	 *            transfer unit be determined automatically via
	 *            {@link RakNet#getMaximumTransferUnit(InetAddress)} with the
	 *            parameter being the specified bind <code>address</code>.
	 * @param maxConnections
	 *            the maximum number of connections, A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @param identifier
	 *            the identifier that will be sent in response to server pings
	 *            if server broadcasting is enabled. A <code>null</code>
	 *            identifier means nothing will be sent in response to server
	 *            pings, even if server broadcasting is enabled.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>bindAddress</code> could be
	 *             found, or if a <code>scope_id</code> was specified for a
	 *             global IPv6 address.
	 * @throws NullPointerException
	 *             if the address is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the maximum connections is less than <code>0</code> and
	 *             not equal to {@value #INFINITE_CONNECTIONS} or the maximum
	 *             transfer unit size is less than
	 *             {@value RakNet#MINIMUM_MTU_SIZE} and is not equal to
	 *             {@value #AUTOMATIC_MTU}.
	 */
	public RakNetServer(String host, int port, int maximumTransferUnit, int maxConnections, Identifier identifier)
			throws UnknownHostException, NullPointerException, IllegalArgumentException {
		this(InetAddress.getByName(host), port, maximumTransferUnit, maxConnections, identifier);
	}

	/**
	 * Creates a RakNet server.
	 * 
	 * @param host
	 *            the IP address the server will bind to during startup. A
	 *            <code>null</code> IP address will have the server bind to the
	 *            wildcard address.
	 * @param port
	 *            the port the server will bind to during startup.
	 * @param maxConnections
	 *            the maximum number of connections, A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @param identifier
	 *            the identifier that will be sent in response to server pings
	 *            if server broadcasting is enabled. A <code>null</code>
	 *            identifier means nothing will be sent in response to server
	 *            pings, even if server broadcasting is enabled.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>bindAddress</code> could be
	 *             found, or if a <code>scope_id</code> was specified for a
	 *             global IPv6 address.
	 * @throws NullPointerException
	 *             if the address is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the maximum connections is less than <code>0</code> and
	 *             not equal to {@value #INFINITE_CONNECTIONS}.
	 */
	public RakNetServer(String host, int port, int maxConnections, Identifier identifier)
			throws UnknownHostException, NullPointerException, IllegalArgumentException {
		this(host, port, AUTOMATIC_MTU, maxConnections, identifier);
	}

	/**
	 * Creates a RakNet server.
	 * 
	 * @param host
	 *            the IP address the server will bind to during startup. A
	 *            <code>null</code> IP address will have the server bind to the
	 *            wildcard address.
	 * @param port
	 *            the port the server will bind to during startup.
	 * @param maximumTransferUnit
	 *            the highest maximum transfer unit a client can use. The
	 *            maximum transfer unit is the maximum number of bytes that can
	 *            be sent in one packet. If a packet exceeds this size, it is
	 *            automatically split up so that it can still be sent over the
	 *            connection (this is handled automatically by
	 *            {@link com.whirvis.jraknet.peer.RakNetPeer RakNetPeer}). A
	 *            value of {@value #AUTOMATIC_MTU} will have the maximum
	 *            transfer unit be determined automatically via
	 *            {@link RakNet#getMaximumTransferUnit(InetAddress)} with the
	 *            parameter being the specified bind <code>address</code>.
	 * @param maxConnections
	 *            the maximum number of connections, A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>bindAddress</code> could be
	 *             found, or if a <code>scope_id</code> was specified for a
	 *             global IPv6 address.
	 * @throws NullPointerException
	 *             if the address is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the maximum connections is less than <code>0</code> and
	 *             not equal to {@value #INFINITE_CONNECTIONS} or the maximum
	 *             transfer unit size is less than
	 *             {@value RakNet#MINIMUM_MTU_SIZE} and is not equal to
	 *             {@value #AUTOMATIC_MTU}.
	 */
	public RakNetServer(String host, int port, int maximumTransferUnit, int maxConnections)
			throws UnknownHostException, NullPointerException, IllegalArgumentException {
		this(host, port, maximumTransferUnit, maxConnections, null);
	}

	/**
	 * Creates a RakNet server.
	 * 
	 * @param host
	 *            the IP address the server will bind to during startup. A
	 *            <code>null</code> IP address will have the server bind to the
	 *            wildcard address.
	 * @param port
	 *            the port the server will bind to during startup.
	 * @param maxConnections
	 *            the maximum number of connections, A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>bindAddress</code> could be
	 *             found, or if a <code>scope_id</code> was specified for a
	 *             global IPv6 address.
	 * @throws NullPointerException
	 *             if the address is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the maximum connections is less than <code>0</code> and
	 *             not equal to {@value #INFINITE_CONNECTIONS}.
	 */
	public RakNetServer(String host, int port, int maxConnections)
			throws UnknownHostException, NullPointerException, IllegalArgumentException {
		this(host, port, AUTOMATIC_MTU, maxConnections);
	}

	/**
	 * Creates a RakNet server.
	 * 
	 * @param port
	 *            the port the server will bind to during startup.
	 * @param maximumTransferUnit
	 *            the highest maximum transfer unit a client can use. The
	 *            maximum transfer unit is the maximum number of bytes that can
	 *            be sent in one packet. If a packet exceeds this size, it is
	 *            automatically split up so that it can still be sent over the
	 *            connection (this is handled automatically by
	 *            {@link com.whirvis.jraknet.peer.RakNetPeer RakNetPeer}). A
	 *            value of {@value #AUTOMATIC_MTU} will have the maximum
	 *            transfer unit be determined automatically via
	 *            {@link RakNet#getMaximumTransferUnit(InetAddress)} with the
	 *            parameter being the specified bind <code>address</code>.
	 * @param maxConnections
	 *            the maximum number of connections, A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @param identifier
	 *            the identifier that will be sent in response to server pings
	 *            if server broadcasting is enabled. A <code>null</code>
	 *            identifier means nothing will be sent in response to server
	 *            pings, even if server broadcasting is enabled.
	 * @throws NullPointerException
	 *             if the address is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the maximum connections is less than <code>0</code> and
	 *             not equal to {@value #INFINITE_CONNECTIONS} or the maximum
	 *             transfer unit size is less than
	 *             {@value RakNet#MINIMUM_MTU_SIZE} and is not equal to
	 *             {@value #AUTOMATIC_MTU}.
	 */
	public RakNetServer(int port, int maximumTransferUnit, int maxConnections, Identifier identifier)
			throws NullPointerException, IllegalArgumentException {
		this(new InetSocketAddress((InetAddress) null, port), maximumTransferUnit, maxConnections, identifier);
	}

	/**
	 * Creates a RakNet server.
	 * 
	 * @param port
	 *            the port the server will bind to during startup.
	 * @param maxConnections
	 *            the maximum number of connections, A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @param identifier
	 *            the identifier that will be sent in response to server pings
	 *            if server broadcasting is enabled. A <code>null</code>
	 *            identifier means nothing will be sent in response to server
	 *            pings, even if server broadcasting is enabled.
	 * @throws NullPointerException
	 *             if the address is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the maximum connections is less than <code>0</code> and
	 *             not equal to {@value #INFINITE_CONNECTIONS} or the maximum
	 *             transfer unit size is less than
	 *             {@value RakNet#MINIMUM_MTU_SIZE} and is not equal to
	 *             {@value #AUTOMATIC_MTU}.
	 */
	public RakNetServer(int port, int maxConnections, Identifier identifier)
			throws NullPointerException, IllegalArgumentException {
		this(port, AUTOMATIC_MTU, maxConnections, identifier);
	}

	/**
	 * Creates a RakNet server.
	 * 
	 * @param port
	 *            the port the server will bind to during startup.
	 * @param maximumTransferUnit
	 *            the highest maximum transfer unit a client can use. The
	 *            maximum transfer unit is the maximum number of bytes that can
	 *            be sent in one packet. If a packet exceeds this size, it is
	 *            automatically split up so that it can still be sent over the
	 *            connection (this is handled automatically by
	 *            {@link com.whirvis.jraknet.peer.RakNetPeer RakNetPeer}).
	 * @param maxConnections
	 *            the maximum number of connections, A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @throws NullPointerException
	 *             if the address is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the maximum connections is less than <code>0</code> and
	 *             not equal to {@value #INFINITE_CONNECTIONS} or the maximum
	 *             transfer unit size is less than
	 *             {@value RakNet#MINIMUM_MTU_SIZE} and is not equal to
	 *             {@value #AUTOMATIC_MTU}.
	 */
	public RakNetServer(int port, int maximumTransferUnit, int maxConnections)
			throws NullPointerException, IllegalArgumentException {
		this(port, maximumTransferUnit, maxConnections, null);
	}

	/**
	 * Creates a RakNet server.
	 * 
	 * @param port
	 *            the port the server will bind to during startup.
	 * @param maxConnections
	 *            the maximum number of connections, A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @throws NullPointerException
	 *             if the address is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the maximum connections is less than <code>0</code> and
	 *             not equal to {@value #INFINITE_CONNECTIONS}.
	 */
	public RakNetServer(int port, int maxConnections) throws NullPointerException, IllegalArgumentException {
		this(port, AUTOMATIC_MTU, maxConnections);
	}

	/**
	 * Forwards the port that the server is running on.
	 * 
	 * @return the result of the port forward attempt.
	 * @see RakNet#forwardPort(int)
	 */
	public final RakNet.UPnPResult forwardPort() {
		return RakNet.forwardPort(this.getPort());
	}

	/**
	 * Closes the port that the server is running on.
	 * 
	 * @return the result of the port close attempt.
	 * @see RakNet#closePort(int)
	 */
	public final RakNet.UPnPResult closePort() {
		return RakNet.closePort(this.getPort());
	}

	/**
	 * Returns the server's networking protocol version.
	 * 
	 * @return the server's networking protocol version.
	 */
	public final int getProtocolVersion() {
		return RakNet.SERVER_NETWORK_PROTOCOL;
	}

	/**
	 * Returns the server's globally unique ID.
	 * 
	 * @return the server's globally unique ID.
	 */
	public final long getGloballyUniqueId() {
		return this.guid;
	}

	/**
	 * Returns the server's timestamp.
	 * 
	 * @return the server's timestamp.
	 */
	public final long getTimestamp() {
		return System.currentTimeMillis() - timestamp;
	}

	/**
	 * Returns the address the server is bound to.
	 * 
	 * @return the address the server is bound to.
	 */
	public final InetSocketAddress getAddress() {
		return this.bindAddress;
	}

	/**
	 * Returns the IP address the server is bound to.
	 * 
	 * @return the IP address the server is bound to.
	 */
	public final InetAddress getInetAddress() {
		return bindAddress.getAddress();
	}

	/**
	 * Returns the port the server is bound to.
	 * 
	 * @return the port the server is bound to.
	 */
	public final int getPort() {
		return bindAddress.getPort();
	}

	/**
	 * Returns the server's maximum transfer unit.
	 * 
	 * @return the server's maximum transfer unit.
	 */
	public final int getMaximumTransferUnit() {
		return this.maximumTransferUnit;
	}

	/**
	 * Returns the maximum amount of connections allowed at once.
	 * 
	 * @return the maximum amount of connections allowed at once,
	 *         {@value #INFINITE_CONNECTIONS} if an infinite amount of
	 *         connections are allowed.
	 */
	public final int getMaxConnections() {
		return this.maxConnections;
	}

	/**
	 * Sets the maximum amount of connections allowed at once.
	 * 
	 * @param maxConnections
	 *            the maximum number of connections. A value of
	 *            {@value #INFINITE_CONNECTIONS} will allow for an infinite
	 *            number of connections.
	 * @throws IllegalArgumentException
	 *             if the <code>maxConnections</code> is less than
	 *             <code>0</code> and is not equal to
	 *             {@value #INFINITE_CONNECTIONS}.
	 */
	public final void setMaxConnections(int maxConnections) throws IllegalArgumentException {
		if (maxConnections < 0 && maxConnections != INFINITE_CONNECTIONS) {
			throw new IllegalArgumentException("Maximum connections must be greater than or equal to 0 or "
					+ INFINITE_CONNECTIONS + " for infinite connections");
		}
		boolean updated = this.maxConnections != maxConnections;
		this.maxConnections = maxConnections;
		if (updated == true) {
			logger.info("Set maximum connections to "
					+ (maxConnections == INFINITE_CONNECTIONS ? "infinite" : maxConnections));
		}
	}

	/**
	 * Enables/disables server broadcasting.
	 * 
	 * @param enabled
	 *            <code>true</code> to enable broadcasting, <code>false</code>
	 *            to disable broadcasting.
	 */
	public final void setBroadcastingEnabled(boolean enabled) {
		boolean wasBroadcasting = this.broadcastingEnabled;
		this.broadcastingEnabled = enabled;
		if (wasBroadcasting != enabled) {
			logger.info((enabled ? "Enabled" : "Disabled") + " broadcasting");
		}
	}

	/**
	 * Returns whether or not broadcasting is enabled.
	 * 
	 * @return <code>true</code> if broadcasting is enabled, <code>false</code>
	 *         otherwise.
	 */
	public final boolean isBroadcastingEnabled() {
		return this.broadcastingEnabled;
	}

	/**
	 * Returns the identifier sent back to clients who ping the server.
	 * 
	 * @return the identifier sent back to clients who ping the server.
	 */
	public final Identifier getIdentifier() {
		return this.identifier;
	}

	/**
	 * Sets the server's identifier used for discovery.
	 * 
	 * @param identifier
	 *            the new identifier.
	 */
	public final void setIdentifier(Identifier identifier) {
		boolean updated = !Objects.equals(this.identifier, identifier);
		this.identifier = identifier;
		if (updated == true) {
			if (identifier != null) {
				logger.info("Set identifier to \"" + identifier.build() + "\"");
			} else {
				logger.info("Removed identifier");
			}
		}
	}

	/**
	 * Adds a listener to the server.
	 * <p>
	 * Listeners are used to listen for events that occur relating to the server
	 * such as clients connecting to the server, receiving messages, and more.
	 * 
	 * @param listener
	 *            the listener to add.
	 * @return the server.
	 * @throws NullPointerException
	 *             if the listener is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the listener is another server that is not the server
	 *             itself.
	 */
	public final RakNetServer addListener(RakNetServerListener listener)
			throws NullPointerException, IllegalArgumentException {
		if (listener == null) {
			throw new NullPointerException("Listener cannot be null");
		} else if (listener instanceof RakNetClient && !this.equals(listener)) {
			throw new IllegalArgumentException("A server cannot be used as a listener except for itself");
		} else if (!listeners.contains(listener)) {
			listeners.add(listener);
			if (listener != this) {
				logger.info("Added listener of class " + listener.getClass().getName());
			} else {
				logger.info("Added self listener");
			}
		}
		return this;
	}

	/**
	 * Adds the server to its own set of listeners, used when extending the
	 * {@link RakNetServer} directly.
	 * 
	 * @return the server.
	 * @see #addListener(RakNetServerListener)
	 */
	public final RakNetServer addSelfListener() {
		return this.addListener(this);
	}

	/**
	 * Removes a listener from the server.
	 * 
	 * @param listener
	 *            the listener to remove.
	 * @return the server.
	 */
	public final RakNetServer removeListener(RakNetServerListener listener) {
		if (listeners.remove(listener)) {
			if (listener != this) {
				logger.info("Removed listener of class " + listener.getClass().getName());
			} else {
				logger.info("Removed self listener");
			}
		}
		return this;
	}

	/**
	 * Removes the server from its own set of listeners, used when extending the
	 * {@link RakNetServer} directly.
	 * 
	 * @return the server.
	 * @see #removeListener(RakNetServerListener)
	 */
	public final RakNetServer removeSelfListener() {
		this.removeListener(this);
		return this;
	}

	/**
	 * Calls the event.
	 * 
	 * @param event
	 *            the event to call.
	 * @throws NullPointerException
	 *             if the event is <code>null</code>.
	 */
	public final void callEvent(Consumer<? super RakNetServerListener> event) throws NullPointerException {
		if (event == null) {
			throw new NullPointerException("Event cannot be null");
		}
		logger.trace("Called event of class " + event.getClass().getName() + " for " + listeners.size() + " listeners");
		for (RakNetServerListener listener : listeners) {
			if (listener.getClass().isAnnotationPresent(ThreadedListener.class)) {
				ThreadedListener threadedListener = listener.getClass().getAnnotation(ThreadedListener.class);
				new Thread(RakNetServer.class.getSimpleName() + (threadedListener.name().length() > 0 ? "-" : "")
						+ threadedListener.name() + "-Thread-" + ++eventThreadCount) {

					@Override
					public void run() {
						event.accept(listener);
					}

				}.start();
			} else {
				event.accept(listener);
			}
		}
	}

	/**
	 * Returns the clients connected to the server.
	 * 
	 * @return the clients connected to the server.
	 */
	public final RakNetClientPeer[] getClients() {
		return clients.values().toArray(new RakNetClientPeer[clients.size()]);
	}

	/**
	 * Returns the amount of clients connected to the server.
	 * 
	 * @return the amount of clients connected to the server.
	 */
	public final int getClientCount() {
		return clients.size();
	}

	/**
	 * Returns whether or not a client with the specified address is currently
	 * connected to the server.
	 * 
	 * @param address
	 *            the address.
	 * @return <code>true</code> if a client with the address is connected to
	 *         the server, <code>false</code> otherwise.
	 */
	public final boolean hasClient(InetSocketAddress address) {
		if (address != null) {
			return clients.containsKey(address);
		}
		return false;
	}

	/**
	 * Returns whether or not a client with the specified address is currently
	 * connected to the server.
	 * 
	 * @param address
	 *            the IP address.
	 * @param port
	 *            the port.
	 * @return <code>true</code> if a client with the address is connected to
	 *         the server, <code>false</code> otherwise.
	 */
	public final boolean hasClient(InetAddress address, int port) {
		if (port >= 0x0000 && port <= 0xFFFF) {
			return this.hasClient(new InetSocketAddress(address, port));
		}
		return false;
	}

	/**
	 * Returns whether or not a client with the specified address is currently
	 * connected to the server.
	 * 
	 * @param host
	 *            the IP address.
	 * @param port
	 *            the port.
	 * @return <code>true</code> if a client with the address is connected to
	 *         the server, <code>false</code> otherwise.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>host</code> could be found, or
	 *             if a <code>scope_id</code> was specified for a global IPv6
	 *             address.
	 */
	public final boolean hasClient(String host, int port) throws UnknownHostException {
		return this.hasClient(InetAddress.getByName(host), port);
	}

	/**
	 * Returns whether or not a client with the specified IP address is
	 * currently connected to the server.
	 * 
	 * @param address
	 *            the IP address.
	 * @return <code>true</code> if a client with the IP address is connected to
	 *         the server, <code>false</code> otherwise.
	 */
	public final boolean hasClient(InetAddress address) {
		if (address != null) {
			for (InetSocketAddress clientAddress : clients.keySet()) {
				if (clientAddress.getAddress().equals(address)) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Returns whether or not a client with the specified IP address is
	 * currently connected to the server.
	 * 
	 * @param host
	 *            the IP address.
	 * @return <code>true</code> if a client with the IP address is connected to
	 *         the server, <code>false</code> otherwise.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>host</code> could be found, or
	 *             if a <code>scope_id</code> was specified for a global IPv6
	 *             address.
	 */
	public final boolean hasClient(String host) throws UnknownHostException {
		return this.hasClient(InetAddress.getByName(host));
	}

	/**
	 * Returns whether or not a client with the specified port is currently
	 * connected to the server.
	 * 
	 * @param port
	 *            the port.
	 * @return <code>true</code> if a client with the port is connected to the
	 *         server, <code>false</code> otherwise.
	 */
	public final boolean hasClient(int port) {
		if (port >= 0x0000 || port <= 0xFFFF) {
			for (InetSocketAddress clientAddress : clients.keySet()) {
				if (clientAddress.getPort() == port) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Returns whether or not the client with the specified globally unique ID
	 * is currently connected to the server.
	 * 
	 * @param guid
	 *            the globally unique ID.
	 * @return <code>true</code> if a client with the globally unique ID is
	 *         connected to the server, <code>false</code> otherwise.
	 */
	public final boolean hasClient(long guid) {
		for (RakNetClientPeer peer : clients.values()) {
			if (peer.getGloballyUniqueId() == guid) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns the client with the specified address.
	 * 
	 * @param address
	 *            the address.
	 * @return the client with the address, <code>null</code> if there is none.
	 */
	public final RakNetClientPeer getClient(InetSocketAddress address) {
		return clients.get(address);
	}

	/**
	 * Returns the client with the specified address.
	 * 
	 * @param address
	 *            the IP address.
	 * @param port
	 *            the port.
	 * @return the client with the address, <code>null</code> if there is none.
	 */
	public final RakNetClientPeer getClient(InetAddress address, int port) {
		if (address != null && port > 0x0000 && port < 0xFFFF) {
			return this.getClient(new InetSocketAddress(address, port));
		}
		return null;
	}

	/**
	 * Returns the client with the specified address.
	 * 
	 * @param host
	 *            the IP address.
	 * @param port
	 *            the port.
	 * @return the client with the address, <code>null</code> if there is none.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>host</code> could be found, or
	 *             if a <code>scope_id</code> was specified for a global IPv6
	 *             address.
	 */
	public final RakNetClientPeer getClient(String host, int port) throws UnknownHostException {
		if (host != null) {
			return this.getClient(InetAddress.getByName(host), port);
		}
		return null;
	}

	/**
	 * Returns all clients with the specified IP address.
	 * 
	 * @param host
	 *            the IP address.
	 * @return the clients with the IP address.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>host</code> could be found, or
	 *             if a <code>scope_id</code> was specified for a global IPv6
	 *             address.
	 */
	public final RakNetClientPeer[] getClient(String host) throws UnknownHostException {
		ArrayList<RakNetClientPeer> peers = new ArrayList<RakNetClientPeer>();
		if (host != null) {
			InetAddress inetAddress = InetAddress.getByName(host);
			for (RakNetClientPeer peer : clients.values()) {
				if (peer.getInetAddress().equals(inetAddress)) {
					peers.add(peer);
				}
			}
		}
		return peers.toArray(new RakNetClientPeer[peers.size()]);
	}

	/**
	 * Returns all clients with the specified port.
	 * 
	 * @param port
	 *            the port.
	 * @return the clients with the port.
	 */
	public final RakNetClientPeer[] getClient(int port) {
		if (port < 0x0000 || port > 0xFFFF) {
			return new RakNetClientPeer[0]; // Invalid port range
		}
		ArrayList<RakNetClientPeer> peers = new ArrayList<RakNetClientPeer>();
		for (RakNetClientPeer peer : clients.values()) {
			if (peer.getPort() == port) {
				peers.add(peer);
			}
		}
		return peers.toArray(new RakNetClientPeer[peers.size()]);
	}

	/**
	 * Returns the client with the specified globally unique ID.
	 * 
	 * @param guid
	 *            the globally unique ID of the client.
	 * @return the client with the globally unique ID, <code>null</code> if
	 *         there is none.
	 */
	public final RakNetClientPeer getClient(long guid) {
		for (RakNetClientPeer peer : clients.values()) {
			if (peer.getGloballyUniqueId() == guid) {
				return peer;
			}
		}
		return null;
	}

	/**
	 * Returns the globally unique ID of the specified peer.
	 * 
	 * @param peer
	 *            the peer.
	 * @return the globally unique ID of the specified peer, <code>-1</code> if
	 *         it does not exist.
	 * @throws NullPointerException
	 *             if the <code>peer</code> is <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the <code>peer</code> is not of the server.
	 */
	public final long getGuid(RakNetClientPeer peer) throws NullPointerException, IllegalArgumentException {
		if (peer == null) {
			throw new NullPointerException("Peer cannot be null");
		}
		RakNetClientPeer clientPeer = clients.get(peer.getAddress());
		if (clientPeer != null) {
			if (clientPeer != peer) {
				throw new NullPointerException("Peer must be of the server");
			}
			return clientPeer.getGloballyUniqueId();
		}
		return -1L;
	}

	/**
	 * Sends a message to the specified peer.
	 * 
	 * @param guid
	 *            the globally unique ID of the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the packet.
	 * @param channel
	 *            the channel to send the packet on.
	 * @param packet
	 *            the packet to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>reliability</code> or <code>packet</code> are
	 *             <code>null</code>.
	 * @throws InvalidChannelException
	 *             if the channel is higher than or equal to
	 *             {@value RakNet#CHANNEL_COUNT}.
	 */
	public final EncapsulatedPacket sendMessage(long guid, Reliability reliability, int channel, Packet packet)
			throws NullPointerException, IllegalArgumentException {
		if (reliability == null) {
			throw new NullPointerException("Reliability cannot be null");
		} else if (packet == null) {
			throw new NullPointerException("Packet cannot be null");
		} else if (!this.hasClient(guid)) {
			throw new IllegalArgumentException("No client with the specified GUID exists");
		}
		return this.getClient(guid).sendMessage(reliability, channel, packet);
	}

	/**
	 * Sends a message to the specified peer.
	 * 
	 * @param peer
	 *            the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the packet.
	 * @param channel
	 *            the channel to send the packet on.
	 * @param packet
	 *            the packet to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>peer</code>, <code>reliability</code> or
	 *             <code>packet</code> are <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the <code>peer</code> is not of the server.
	 * @throws InvalidChannelException
	 *             if the channel is higher than or equal to
	 *             {@value RakNet#CHANNEL_COUNT}.
	 */
	public final EncapsulatedPacket sendMessage(RakNetClientPeer peer, Reliability reliability, int channel,
			Packet packet) throws NullPointerException, IllegalArgumentException, InvalidChannelException {
		return this.sendMessage(this.getGuid(peer), reliability, channel, packet);
	}

	/**
	 * Sends messages to the specified peer.
	 * 
	 * @param guid
	 *            the globally unique ID of the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the packets.
	 * @param channel
	 *            the channel to send the packets on.
	 * @param packets
	 *            the packets to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>reliability</code> or <code>packets</code> are
	 *             <code>null</code>.
	 * @throws InvalidChannelException
	 *             if the channel is higher than or equal to
	 *             {@value RakNet#CHANNEL_COUNT}.
	 */
	public final EncapsulatedPacket[] sendMessage(long guid, Reliability reliability, int channel, Packet... packets)
			throws NullPointerException, InvalidChannelException {
		if (packets == null) {
			throw new NullPointerException("Packets cannot be null");
		}
		EncapsulatedPacket[] encapsulated = new EncapsulatedPacket[packets.length];
		for (int i = 0; i < encapsulated.length; i++) {
			encapsulated[i] = this.sendMessage(guid, reliability, channel, packets[i]);
		}
		return encapsulated;
	}

	/**
	 * Sends messages to the specified peer.
	 * 
	 * @param peer
	 *            the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the packets.
	 * @param channel
	 *            the channel to send the packets on.
	 * @param packets
	 *            the packets to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>peer</code>, <code>reliability</code> or
	 *             <code>packets</code> are <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the <code>peer</code> is not of the server.
	 * @throws InvalidChannelException
	 *             if the channel is higher than or equal to
	 *             {@value RakNet#CHANNEL_COUNT}.
	 */
	public final EncapsulatedPacket[] sendMessage(RakNetClientPeer peer, Reliability reliability, int channel,
			Packet... packets) throws NullPointerException, IllegalArgumentException, InvalidChannelException {
		return this.sendMessage(this.getGuid(peer), reliability, channel, packets);
	}

	/**
	 * Sends a message to the specified peer on the default channel.
	 * 
	 * @param guid
	 *            the globally unique ID of the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the packet.
	 * @param packet
	 *            the packet to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>reliability</code> or <code>packet</code> are
	 *             <code>null</code>.
	 */
	public final EncapsulatedPacket sendMessage(long guid, Reliability reliability, Packet packet)
			throws NullPointerException {
		return this.sendMessage(guid, reliability, RakNet.DEFAULT_CHANNEL, packet);
	}

	/**
	 * Sends a message to the specified peer on the default channel.
	 * 
	 * @param peer
	 *            the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the packet.
	 * @param packet
	 *            the packet to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>peer</code>, <code>reliability</code> or
	 *             <code>packet</code> are <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the <code>peer</code> is not of the server.
	 */
	public final EncapsulatedPacket sendMessage(RakNetClientPeer peer, Reliability reliability, Packet packet)
			throws NullPointerException, IllegalArgumentException {
		return this.sendMessage(this.getGuid(peer), reliability, packet);
	}

	/**
	 * Sends messages to the specified peer on the default channel.
	 * 
	 * @param guid
	 *            the globally unique ID of the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the packets.
	 * @param packets
	 *            the packets to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>reliability</code> or <code>packets</code> are
	 *             <code>null</code>.
	 */
	public final EncapsulatedPacket[] sendMessage(long guid, Reliability reliability, Packet... packets)
			throws NullPointerException {
		return this.sendMessage(guid, reliability, RakNet.DEFAULT_CHANNEL, packets);
	}

	/**
	 * Sends messages to the specified peer on the default channel.
	 * 
	 * @param peer
	 *            the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the packets.
	 * @param packets
	 *            the packets to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>peer</code>, <code>reliability</code> or
	 *             <code>packets</code> are <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the <code>peer</code> is not of the server.
	 */
	public final EncapsulatedPacket[] sendMessage(RakNetClientPeer peer, Reliability reliability, Packet... packets)
			throws NullPointerException, IllegalArgumentException {
		return this.sendMessage(this.getGuid(peer), reliability, packets);
	}

	/**
	 * Sends a message to the specified peer.
	 * 
	 * @param guid
	 *            the globally unique ID of the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the packet.
	 * @param channel
	 *            the channel to send the packet on.
	 * @param buf
	 *            the buffer to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>reliability</code> or <code>buf</code> are
	 *             <code>null</code>.
	 * @throws InvalidChannelException
	 *             if the channel is higher than or equal to
	 *             {@value RakNet#CHANNEL_COUNT}.
	 */
	public final EncapsulatedPacket sendMessage(long guid, Reliability reliability, int channel, ByteBuf buf)
			throws NullPointerException, InvalidChannelException {
		return this.sendMessage(guid, reliability, channel, new Packet(buf));
	}

	/**
	 * Sends a message to the specified peer.
	 * 
	 * @param peer
	 *            the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the packet.
	 * @param channel
	 *            the channel to send the packet on.
	 * @param buf
	 *            the buffer to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>peer</code>, <code>reliability</code> or
	 *             <code>buf</code> are <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the <code>peer</code> is not of the server.
	 * @throws InvalidChannelException
	 *             if the channel is higher than or equal to
	 *             {@value RakNet#CHANNEL_COUNT}.
	 */
	public final EncapsulatedPacket sendMessage(RakNetClientPeer peer, Reliability reliability, int channel,
			ByteBuf buf) throws NullPointerException, IllegalArgumentException, InvalidChannelException {
		return this.sendMessage(this.getGuid(peer), reliability, channel, buf);
	}

	/**
	 * Sends messages to the specified peer.
	 * 
	 * @param guid
	 *            the globally unique ID of the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the packets.
	 * @param channel
	 *            the channel to send the packets on.
	 * @param bufs
	 *            the buffers to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>reliability</code> or <code>bufs</code> are
	 *             <code>null</code>.
	 * @throws InvalidChannelException
	 *             if the channel is higher than or equal to
	 *             {@value RakNet#CHANNEL_COUNT}.
	 */
	public final EncapsulatedPacket[] sendMessage(long guid, Reliability reliability, int channel, ByteBuf... bufs)
			throws NullPointerException, InvalidChannelException {
		if (bufs == null) {
			throw new NullPointerException("Buffers cannot be null");
		}
		EncapsulatedPacket[] encapsulated = new EncapsulatedPacket[bufs.length];
		for (int i = 0; i < encapsulated.length; i++) {
			encapsulated[i] = this.sendMessage(guid, reliability, channel, bufs[i]);
		}
		return encapsulated;
	}

	/**
	 * Sends messages to the specified peer.
	 * 
	 * @param peer
	 *            the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the packets.
	 * @param channel
	 *            the channel to send the packets on.
	 * @param bufs
	 *            the buffers to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>peer</code>, <code>reliability</code> or
	 *             <code>bufs</code> are <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the <code>peer</code> is not of the server.
	 * @throws InvalidChannelException
	 *             if the channel is higher than or equal to
	 *             {@value RakNet#CHANNEL_COUNT}.
	 */
	public final EncapsulatedPacket[] sendMessage(RakNetClientPeer peer, Reliability reliability, int channel,
			ByteBuf... bufs) throws NullPointerException, IllegalArgumentException, InvalidChannelException {
		return this.sendMessage(this.getGuid(peer), reliability, bufs);
	}

	/**
	 * Sends messages to the specified peer on the default channel.
	 * 
	 * @param guid
	 *            the globally unique ID of the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the packet.
	 * @param buf
	 *            the buffer to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the inexistence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>reliability</code> or <code>buf</code> are
	 *             <code>null</code>.
	 */
	public final EncapsulatedPacket sendMessage(long guid, Reliability reliability, ByteBuf buf)
			throws NullPointerException {
		return this.sendMessage(guid, reliability, RakNet.DEFAULT_CHANNEL, buf);
	}

	/**
	 * Sends messages to the specified peer on the default channel.
	 * 
	 * @param peer
	 *            the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the packet.
	 * @param buf
	 *            the buffer to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>peer</code>, <code>reliability</code> or
	 *             <code>buf</code> are <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the <code>peer</code> is not of the server.
	 */
	public final EncapsulatedPacket sendMessage(RakNetClientPeer peer, Reliability reliability, ByteBuf buf)
			throws NullPointerException, IllegalArgumentException {
		return this.sendMessage(this.getGuid(peer), reliability, buf);
	}

	/**
	 * Sends messages to the specified peer on the default channel.
	 * 
	 * @param guid
	 *            the globally unique ID of the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the packet.
	 * @param bufs
	 *            the buffers to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>reliability</code> or <code>bufs</code> are
	 *             <code>null</code>.
	 */
	public final EncapsulatedPacket[] sendMessage(long guid, Reliability reliability, ByteBuf... bufs)
			throws NullPointerException {
		return this.sendMessage(guid, reliability, RakNet.DEFAULT_CHANNEL, bufs);
	}

	/**
	 * Sends messages to the specified peer on the default channel.
	 * 
	 * @param peer
	 *            the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the packet.
	 * @param bufs
	 *            the buffers to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>peer</code>, <code>reliability</code> or
	 *             <code>bufs</code> are <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the <code>peer</code> is not of the server.
	 */
	public final EncapsulatedPacket[] sendMessage(RakNetClientPeer peer, Reliability reliability, ByteBuf... bufs)
			throws NullPointerException, IllegalArgumentException {
		return this.sendMessage(this.getGuid(peer), reliability, bufs);
	}

	/**
	 * Sends a message identifier to the specified peer.
	 * 
	 * @param guid
	 *            the globally unique ID of the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the message identifier.
	 * @param channel
	 *            the channel to send the message identifier on.
	 * @param packetId
	 *            the message identifier to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>reliability</code> is <code>null</code>.
	 * @throws InvalidChannelException
	 *             if the channel is higher than or equal to
	 *             {@value RakNet#CHANNEL_COUNT}.
	 */
	public final EncapsulatedPacket sendMessage(long guid, Reliability reliability, int channel, int packetId)
			throws NullPointerException, InvalidChannelException {
		return this.sendMessage(guid, reliability, channel, new RakNetPacket(packetId));
	}

	/**
	 * Sends a message identifier to the specified peer.
	 * 
	 * @param peer
	 *            the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the message identifier.
	 * @param channel
	 *            the channel to send the message identifier on.
	 * @param packetId
	 *            the message identifier to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the<code>peer</code> or <code>reliability</code> are
	 *             <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the <code>peer</code> is not of the server.
	 * @throws InvalidChannelException
	 *             if the channel is higher than or equal to
	 *             {@value RakNet#CHANNEL_COUNT}.
	 */
	public final EncapsulatedPacket sendMessage(RakNetClientPeer peer, Reliability reliability, int channel,
			int packetId) throws NullPointerException, IllegalArgumentException, InvalidChannelException {
		return this.sendMessage(this.getGuid(peer), reliability, channel, packetId);
	}

	/**
	 * Sends message identifiers to the specified peer.
	 * 
	 * @param guid
	 *            the globally unique ID of the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the message identifiers.
	 * @param channel
	 *            the channel to send the message identifiers on.
	 * @param packetIds
	 *            the message identifiers to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>reliability</code> or <code>packetIds</code> are
	 *             <code>null</code>.
	 * @throws InvalidChannelException
	 *             if the channel is higher than or equal to
	 *             {@value RakNet#CHANNEL_COUNT}.
	 */
	public final EncapsulatedPacket[] sendMessage(long guid, Reliability reliability, int channel, int... packetIds)
			throws NullPointerException, InvalidChannelException {
		if (packetIds == null) {
			throw new NullPointerException("Packet IDs cannot be null");
		}
		EncapsulatedPacket[] encapsulated = new EncapsulatedPacket[packetIds.length];
		for (int i = 0; i < encapsulated.length; i++) {
			encapsulated[i] = this.sendMessage(guid, reliability, channel, packetIds[i]);
		}
		return encapsulated;
	}

	/**
	 * Sends message identifiers to the specified peer.
	 * 
	 * @param peer
	 *            the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the message identifiers.
	 * @param channel
	 *            the channel to send the message identifiers on.
	 * @param packetIds
	 *            the message identifiers to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>peer</code>, <code>reliability</code> or
	 *             <code>packetIds</code> are <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the <code>peer</code> is not of the server.
	 * @throws InvalidChannelException
	 *             if the channel is higher than or equal to
	 *             {@value RakNet#CHANNEL_COUNT}.
	 */
	public final EncapsulatedPacket[] sendMessage(RakNetClientPeer peer, Reliability reliability, int channel,
			int... packetIds) throws NullPointerException, IllegalArgumentException, InvalidChannelException {
		return this.sendMessage(this.getGuid(peer), reliability, channel, packetIds);
	}

	/**
	 * Sends a message identifier to the specified peer on the default channel.
	 * 
	 * @param guid
	 *            the globally unique ID of the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the message identifier.
	 * @param packetId
	 *            the message identifier to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>reliability</code> is <code>null</code>.
	 */
	public final EncapsulatedPacket sendMessage(long guid, Reliability reliability, int packetId)
			throws NullPointerException {
		return this.sendMessage(guid, reliability, new RakNetPacket(packetId));
	}

	/**
	 * Sends a message identifier to the specified peer on the default channel.
	 * 
	 * @param peer
	 *            the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the message identifier.
	 * @param packetId
	 *            the message identifier to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>peer</code>, <code>reliability</code> is
	 *             <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the <code>peer</code> is not of the server.
	 */
	public final EncapsulatedPacket sendMessage(RakNetClientPeer peer, Reliability reliability, int packetId)
			throws NullPointerException, IllegalArgumentException {
		return this.sendMessage(this.getGuid(peer), reliability, packetId);
	}

	/**
	 * Sends message identifiers to the specified peer on the default channel.
	 * 
	 * @param guid
	 *            the globally unique ID of the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the message identifiers.
	 * @param packetIds
	 *            the message identifiers to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>reliability</code> or <code>packetIds</code> are
	 *             <code>null</code>.
	 */
	public final EncapsulatedPacket[] sendMessage(long guid, Reliability reliability, int... packetIds)
			throws NullPointerException {
		return this.sendMessage(guid, reliability, RakNet.DEFAULT_CHANNEL, packetIds);
	}

	/**
	 * Sends message identifiers to the specified peer on the default channel.
	 * 
	 * @param peer
	 *            the peer to send the packet to.
	 * @param reliability
	 *            the reliability of the message identifiers.
	 * @param packetIds
	 *            the message identifiers to send.
	 * @return the generated encapsulated packet, <code>null</code> if no packet
	 *         was sent due to the non existence of the peer with the
	 *         <code>guid</code>. This is normally not important, however it can
	 *         be used for packet acknowledged and not acknowledged events if
	 *         the reliability is of the
	 *         {@link Reliability#UNRELIABLE_WITH_ACK_RECEIPT WITH_ACK_RECEIPT}
	 *         type.
	 * @throws NullPointerException
	 *             if the <code>peer</code>, <code>reliability</code> or
	 *             <code>packetIds</code> are <code>null</code>.
	 * @throws IllegalArgumentException
	 *             if the <code>peer</code> is not of the server.
	 */
	public final EncapsulatedPacket[] sendMessage(RakNetClientPeer peer, Reliability reliability, int... packetIds)
			throws NullPointerException, IllegalArgumentException {
		return this.sendMessage(this.getGuid(peer), reliability, packetIds);
	}

	/**
	 * Returns whether or not the specified client IP address is banned.
	 * 
	 * @param address
	 *            the IP address.
	 * @return <code>true</code> if the client address is banned,
	 *         <code>false</code> otherwise.
	 */
	public final boolean isClientBanned(InetAddress address) {
		return banned.contains(address);
	}

	/**
	 * Returns whether or not the specified client IP address is banned.
	 * 
	 * @param host
	 *            the IP address.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>host</code> could be found, or
	 *             if a <code>scope_id</code> was specified for a global IPv6
	 *             address.
	 * @return <code>true</code> if the client address is banned,
	 *         <code>false</code> otherwise.
	 */
	public final boolean isClientBanned(String host) throws UnknownHostException {
		return banned.contains(InetAddress.getByName(host));
	}

	/**
	 * Bans the specified client IP address.
	 * 
	 * @param address
	 *            the IP address to ban.
	 * @throws NullPointerException
	 *             if the <code>address</code> is <code>null</code>.
	 */
	public final void ban(InetAddress address) throws NullPointerException {
		if (address == null) {
			throw new NullPointerException("IP address cannot be null");
		} else if (!banned.contains(address)) {
			banned.add(address);
			logger.debug("Banned IP address " + address);
		}
	}

	/**
	 * Bans the specified client IP address.
	 * 
	 * @param host
	 *            the IP address to ban.
	 * @throws NullPointerException
	 *             if the <code>host</code> is <code>null</code>.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>host</code> could be found, or
	 *             if a <code>scope_id</code> was specified for a global IPv6
	 *             address.
	 */
	public final void ban(String host) throws NullPointerException, UnknownHostException {
		if (host == null) {
			throw new NullPointerException("IP address cannot be null");
		}
		this.ban(InetAddress.getByName(host));
	}

	/**
	 * Unbans the specified client IP address.
	 * 
	 * @param address
	 *            the IP address to unban.
	 */
	public final void unban(InetAddress address) {
		if (banned.remove(address)) {
			logger.debug("Unbanned IP address " + address);
		}
	}

	/**
	 * Unbans the specified client IP address.
	 * 
	 * @param host
	 *            the IP address to unban.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>host</code> could be found, or
	 *             if a <code>scope_id</code> was specified for a global IPv6
	 *             address.
	 */
	public final void unban(String host) throws UnknownHostException {
		if (host != null) {
			this.unban(InetAddress.getByName(host));
		}
	}

	/**
	 * Disconnects a client from the server.
	 * 
	 * @param address
	 *            the address of the client.
	 * @param reason
	 *            the reason for client disconnection. A <code>null</code>
	 *            reason will have <code>"Disconnected"</code> be used as the
	 *            reason instead.
	 * @return <code>true</code> if a client was disconnected,
	 *         <code>false</code> otherwise.
	 */
	public final boolean disconnect(InetSocketAddress address, String reason) {
		RakNetClientPeer peer = clients.remove(address);
		if (peer == null) {
			return false; // No client to disconnect
		}
		peer.disconnect();
		logger.debug("Disconnected client with address " + address + " for \""
				+ (reason == null ? "Disconnected" : reason) + "\"");
		this.callEvent(
				listener -> listener.onDisconnect(this, address, peer, reason == null ? "Disconnected" : reason));
		return true;
	}

	/**
	 * Disconnects a client from the server.
	 * 
	 * @param address
	 *            the address of the client.
	 * @param reason
	 *            the reason for client disconnection. A <code>null</code>
	 *            reason will have <code>"Disconnected"</code> be used as the
	 *            reason instead.
	 * @return <code>true</code> if a client was disconnected,
	 *         <code>false</code> otherwise.
	 */
	public final boolean disconnect(InetSocketAddress address, Throwable reason) {
		return this.disconnect(address, reason == null ? null : RakNet.getStackTrace(reason));
	}

	/**
	 * Disconnects a client from the server.
	 * 
	 * @param address
	 *            the address of the client.
	 * @return <code>true</code> if a client was disconnected,
	 *         <code>false</code> otherwise.
	 */
	public final boolean disconnect(InetSocketAddress address) {
		return this.disconnect(address, (String) /* Solves ambiguity */ null);
	}

	/**
	 * Disconnects a client from the server.
	 * 
	 * @param address
	 *            the IP address of the client.
	 * @param port
	 *            the port.
	 * @param reason
	 *            the reason for client disconnection. A <code>null</code>
	 *            reason will have <code>"Disconnected"</code> be used as the
	 *            reason instead.
	 * @return <code>true</code> if a client was disconnect, <code>false</code>
	 *         otherwise.
	 */
	public final boolean disconnect(InetAddress address, int port, String reason) {
		if (port < 0x0000 || port > 0xFFFF) {
			return false; // Invalid port range
		}
		return this.disconnect(new InetSocketAddress(address, port), reason);
	}

	/**
	 * Disconnects a client from the server.
	 * 
	 * @param address
	 *            the IP address of the client.
	 * @param port
	 *            the port.
	 * @param reason
	 *            the reason for client disconnection. A <code>null</code>
	 *            reason will have <code>"Disconnected"</code> be used as the
	 *            reason instead.
	 * @return <code>true</code> if a client was disconnect, <code>false</code>
	 *         otherwise.
	 */
	public final boolean disconnect(InetAddress address, int port, Throwable reason) {
		if (port < 0x0000 || port > 0xFFFF) {
			return false; // Invalid port range
		}
		return this.disconnect(new InetSocketAddress(address, port), reason);
	}

	/**
	 * Disconnects a client from the server.
	 * 
	 * @param address
	 *            the IP address of the client.
	 * @param port
	 *            the port.
	 * @return <code>true</code> if a client was disconnect, <code>false</code>
	 *         otherwise.
	 */
	public final boolean disconnect(InetAddress address, int port) {
		return this.disconnect(address, port, (String) /* Solves ambiguity */ null);
	}

	/**
	 * Disconnects a client from the server.
	 * 
	 * @param host
	 *            the IP address of the client.
	 * @param port
	 *            the port.
	 * @param reason
	 *            the reason for client disconnection. A <code>null</code>
	 *            reason will have <code>"Disconnected"</code> be used as the
	 *            reason instead.
	 * @return <code>true</code> if a client was disconnect, <code>false</code>
	 *         otherwise.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>host</code> could be found, or
	 *             if a <code>scope_id</code> was specified for a global IPv6
	 *             address.
	 */
	public final boolean disconnect(String host, int port, String reason) throws UnknownHostException {
		if (host == null) {
			return false;
		}
		return this.disconnect(InetAddress.getByName(host), port, reason);
	}

	/**
	 * Disconnects a client from the server.
	 * 
	 * @param host
	 *            the IP address of the client.
	 * @param port
	 *            the port.
	 * @param reason
	 *            the reason for client disconnection. A <code>null</code>
	 *            reason will have <code>"Disconnected"</code> be used as the
	 *            reason instead.
	 * @return <code>true</code> if a client was disconnect, <code>false</code>
	 *         otherwise.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>host</code> could be found, or
	 *             if a <code>scope_id</code> was specified for a global IPv6
	 *             address.
	 */
	public final boolean disconnect(String host, int port, Throwable reason) throws UnknownHostException {
		if (host == null) {
			return false;
		}
		return this.disconnect(InetAddress.getByName(host), port, reason);
	}

	/**
	 * Disconnects a client from the server.
	 * 
	 * @param host
	 *            the IP address of the client.
	 * @param port
	 *            the port.
	 * @return <code>true</code> if a client was disconnect, <code>false</code>
	 *         otherwise.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>host</code> could be found, or
	 *             if a <code>scope_id</code> was specified for a global IPv6
	 *             address.
	 */
	public final boolean disconnect(String host, int port) throws UnknownHostException {
		return this.disconnect(host, port, (String) /* Solves ambiguity */ null);
	}

	/**
	 * Disconnects all clients from the server with the address.
	 * 
	 * @param address
	 *            the IP address of the client.
	 * @param reason
	 *            the reason for client disconnection. A <code>null</code>
	 *            reason will have <code>"Disconnected"</code> be used as the
	 *            reason instead.
	 * @return <code>true</code> if a client was disconnect, <code>false</code>
	 *         otherwise.
	 */
	public final boolean disconnect(InetAddress address, String reason) {
		if (address == null) {
			return false;
		}
		boolean disconnected = false;
		for (InetSocketAddress peerAddress : clients.keySet()) {
			if (address.equals(peerAddress.getAddress())) {
				this.disconnect(peerAddress, reason);
				disconnected = true;
			}
		}
		return disconnected;
	}

	/**
	 * Disconnects all clients from the server with the IP address.
	 * 
	 * @param address
	 *            the IP address of the client.
	 * @return <code>true</code> if a client was disconnect, <code>false</code>
	 *         otherwise.
	 */
	public final boolean disconnect(InetAddress address) {
		return this.disconnect(address, null);
	}

	/**
	 * Disconnects all clients from the server with the IP address.
	 * 
	 * @param host
	 *            the IP address of the client.
	 * @param reason
	 *            the reason for client disconnection. A <code>null</code>
	 *            reason will have <code>"Disconnected"</code> be used as the
	 *            reason instead.
	 * @return <code>true</code> if a client was disconnect, <code>false</code>
	 *         otherwise.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>host</code> could be found, or
	 *             if a <code>scope_id</code> was specified for a global IPv6
	 *             address.
	 */
	public final boolean disconnect(String host, String reason) throws UnknownHostException {
		if (host == null) {
			return false;
		}
		return this.disconnect(InetAddress.getByName(host), reason);
	}

	/**
	 * Disconnects all clients from the server with the IP address.
	 * 
	 * @param host
	 *            the IP address of the client.
	 * @param reason
	 *            the reason for client disconnection. A <code>null</code>
	 *            reason will have <code>"Disconnected"</code> be used as the
	 *            reason instead.
	 * @return <code>true</code> if a client was disconnect, <code>false</code>
	 *         otherwise.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>host</code> could be found, or
	 *             if a <code>scope_id</code> was specified for a global IPv6
	 *             address.
	 */
	public final boolean disconnect(String host, Throwable reason) throws UnknownHostException {
		return this.disconnect(host, reason == null ? null : RakNet.getStackTrace(reason));
	}

	/**
	 * Disconnects all clients from the server with the IP address.
	 * 
	 * @param host
	 *            the IP address of the client.
	 * @return <code>true</code> if a client was disconnect, <code>false</code>
	 *         otherwise.
	 * @throws UnknownHostException
	 *             if no IP address for the <code>host</code> could be found, or
	 *             if a <code>scope_id</code> was specified for a global IPv6
	 *             address.
	 */
	public final boolean disconnect(String host) throws UnknownHostException {
		return this.disconnect(host, (String) /* Solves ambiguity */ null);
	}

	/**
	 * Disconnects all clients from the server with the port.
	 * 
	 * @param port
	 *            the port.
	 * @param reason
	 *            the reason for client disconnection. A <code>null</code>
	 *            reason will have <code>"Disconnected"</code> be used as the
	 *            reason instead.
	 * @return <code>true</code> if a client was disconnect, <code>false</code>
	 *         otherwise.
	 */
	public final boolean disconnect(int port, String reason) {
		if (port < 0x0000 || port > 0xFFFF) {
			return false; // Invalid port range
		}
		boolean disconnected = false;
		for (InetSocketAddress address : clients.keySet()) {
			if (address.getPort() == port) {
				this.disconnect(address, reason == null ? "Disconnected" : reason);
				disconnected = true;
			}
		}
		return disconnected;
	}

	/**
	 * Disconnects all clients from the server with the port.
	 * 
	 * @param port
	 *            the port.
	 * @param reason
	 *            the reason for client disconnection. A <code>null</code>
	 *            reason will have <code>"Disconnected"</code> be used as the
	 *            reason instead.
	 * @return <code>true</code> if a client was disconnect, <code>false</code>
	 *         otherwise.
	 */
	public final boolean disconnect(int port, Throwable reason) {
		return this.disconnect(port, reason == null ? null : RakNet.getStackTrace(reason));
	}

	/**
	 * Disconnects all clients from the server with the port.
	 * 
	 * @param port
	 *            the port.
	 * @return <code>true</code> if a client was disconnect, <code>false</code>
	 *         otherwise.
	 */
	public final boolean disconnect(int port) {
		return this.disconnect(port, (String) /* Solves ambiguity */ null);
	}

	/**
	 * Disconnects a client from the server.
	 * 
	 * @param peer
	 *            the peer of the client to disconnect.
	 * @param reason
	 *            the reason for client disconnection. A <code>null</code> value
	 *            will have <code>"Disconnected"</code> be used as the reason
	 *            instead.
	 * @return <code>true</code> if a client was disconnected,
	 *         <code>false</code> otherwise.
	 * @throws IllegalArgumentException
	 *             if the given peer is fabricated, meaning that the peer is not
	 *             one created by the server but rather one created externally.
	 */
	public final boolean disconnect(RakNetClientPeer peer, String reason) throws IllegalArgumentException {
		if (peer != null) {
			if (peer.getServer() != this) {
				throw new IllegalArgumentException("Peer must belong to the server");
			}
			return this.disconnect(peer.getAddress(), reason);
		}
		return false;
	}

	/**
	 * Disconnects a client from the server.
	 * 
	 * @param peer
	 *            the peer of the client to disconnect.
	 * @param reason
	 *            the reason for client disconnection. A <code>null</code> value
	 *            will have <code>"Disconnected"</code> be used as the reason
	 *            instead.
	 * @return <code>true</code> if a client was disconnected,
	 *         <code>false</code> otherwise.
	 * @throws IllegalArgumentException
	 *             if the given peer is fabricated, meaning that the peer is not
	 *             one created by the server but rather one created externally.
	 */
	public final boolean disconnect(RakNetClientPeer peer, Throwable reason) throws IllegalArgumentException {
		return this.disconnect(peer, reason == null ? null : RakNet.getStackTrace(reason));
	}

	/**
	 * Disconnects a client from the server.
	 * 
	 * @param peer
	 *            the peer of the client to disconnect.
	 * @return <code>true</code> if a client was disconnected,
	 *         <code>false</code> otherwise.
	 * @throws IllegalArgumentException
	 *             if the given peer is fabricated, meaning that the peer is not
	 *             one created by the server but rather one created externally.
	 */
	public final boolean disconnect(RakNetClientPeer peer) {
		return this.disconnect(peer, (String) /* Solves ambiguity */ null);
	}

	/**
	 * Returns whether or not the specified IP address is blocked.
	 * 
	 * @param address
	 *            the IP address to check.
	 * @return <code>true</code> if the IP address is blocked,
	 *         <code>false</code> otherwise.
	 */
	public final boolean isAddressBlocked(InetAddress address) {
		return handler.isAddressBlocked(address);
	}

	/**
	 * Blocks the specified IP address.
	 * <p>
	 * All currently connected clients with the IP address (regardless of port)
	 * will be disconnected with the same reason that the IP address was
	 * blocked.
	 * 
	 * @param address
	 *            the IP address to block.
	 * @param reason
	 *            the reason the address was blocked. A <code>null</code> reason
	 *            will have <code>"Address blocked"</code> be used as the reason
	 *            instead.
	 * @param time
	 *            how long the address will blocked in milliseconds.
	 * @throws NullPointerException
	 *             if <code>address</code> is <code>null</code>.
	 */
	public final void blockAddress(InetAddress address, String reason, long time) throws NullPointerException {
		handler.blockAddress(address, reason, time);
	}

	/**
	 * Blocks the specified IP address.
	 * <p>
	 * All currently connected clients with the IP address (regardless of port)
	 * will be disconnected with the same reason that the IP address was
	 * blocked.
	 * 
	 * @param address
	 *            the IP address to block.
	 * @param time
	 *            how long the address will blocked in milliseconds.
	 * @throws NullPointerException
	 *             if <code>address</code> is <code>null</code>.
	 */
	public final void blockAddress(InetAddress address, long time) throws NullPointerException {
		this.blockAddress(address, null, time);
	}

	/**
	 * Unblocks the specified IP address.
	 * 
	 * @param address
	 *            the IP address to unblock.
	 */
	public final void unblockAddress(InetAddress address) {
		handler.unblockAddress(address);
	}

	/**
	 * Called by the {@link RakNetServerHandler} when it catches a
	 * <code>Throwable</code> while handling a packet.
	 * 
	 * @param address
	 *            the address that caused the exception.
	 * @param cause
	 *            the <code>Throwable</code> caught by the handler.
	 * @throws NullPointerException
	 *             if the cause <code>address</code> or <code>cause</code> are
	 *             <code>null</code>.
	 */
	protected final void handleHandlerException(InetSocketAddress address, Throwable cause)
			throws NullPointerException {
		if (address == null) {
			throw new NullPointerException("Address cannot be null");
		} else if (cause == null) {
			throw new NullPointerException("Cause cannot be null");
		} else if (this.hasClient(address)) {
			this.disconnect(address, RakNet.getStackTrace(cause));
		}
		logger.warn("Handled exception " + cause.getClass().getName() + " caused by address " + address);
		this.callEvent(listener -> listener.onHandlerException(this, address, cause));
	}

	/**
	 * Handles a packet received by the {@link RakNetServerHandler}.
	 * 
	 * @param sender
	 *            the address of the sender.
	 * @param packet
	 *            the packet to handle.
	 * @throws NullPointerException
	 *             if the <code>sender</code> or <code>packet</code> are
	 *             <code>null</code>.
	 */
	protected final void handleMessage(InetSocketAddress sender, RakNetPacket packet) throws NullPointerException {
		if (sender == null) {
			throw new NullPointerException("Sender cannot be null");
		} else if (packet == null) {
			throw new NullPointerException("Packet cannot be null");
		} else if (clients.containsKey(sender)) {
			clients.get(sender).handleInternal(packet);
		} else if (packet.getId() == RakNetPacket.ID_UNCONNECTED_PING
				|| packet.getId() == RakNetPacket.ID_UNCONNECTED_PING_OPEN_CONNECTIONS) {
			UnconnectedPing ping = new UnconnectedPing(packet);
			ping.decode();
			if (!ping.failed()
					&& (packet.getId() == RakNetPacket.ID_UNCONNECTED_PING
							|| (clients.size() < maxConnections || maxConnections < 0))
					&& broadcastingEnabled == true && ping.magic == true) {
				ServerPing pingEvent = new ServerPing(sender, ping.connectionType, identifier);
				this.callEvent(listener -> listener.onPing(this, pingEvent));
				if (pingEvent.getIdentifier() != null) {
					UnconnectedPong pong = new UnconnectedPong();
					pong.timestamp = ping.timestamp;
					pong.pongId = this.pongId;
					pong.identifier = pingEvent.getIdentifier();
					pong.encode();
					if (!pong.failed()) {
						this.sendNettyMessage(pong, sender);
					} else {
						logger.error(pong.getClass().getSimpleName() + " packet failed to encode");
					}
				}
			}
		} else if (packet.getId() == RakNetPacket.ID_OPEN_CONNECTION_REQUEST_1) {
			OpenConnectionRequestOne connectionRequestOne = new OpenConnectionRequestOne(packet);
			connectionRequestOne.decode();
			if (clients.containsKey(sender)) {
				if (clients.get(sender).isLoggedIn()) {
					this.disconnect(sender, "Client reinstantiated connection");
				}
			}
			if (connectionRequestOne.magic == true) {
				RakNetPacket errorPacket = this.validateSender(sender, NO_GUID);
				if (errorPacket == null) {
					if (connectionRequestOne.networkProtocol != this.getProtocolVersion()) {
						IncompatibleProtocolVersion incompatibleProtocol = new IncompatibleProtocolVersion();
						incompatibleProtocol.networkProtocol = this.getProtocolVersion();
						incompatibleProtocol.serverGuid = this.guid;
						incompatibleProtocol.encode();
						this.sendNettyMessage(incompatibleProtocol, sender);
					} else {
						OpenConnectionResponseOne connectionResponseOne = new OpenConnectionResponseOne();
						connectionResponseOne.serverGuid = this.guid;
						connectionResponseOne.maximumTransferUnit = connectionRequestOne.maximumTransferUnit;
						connectionResponseOne.encode();
						this.sendNettyMessage(connectionResponseOne, sender);
					}
				} else {
					this.sendNettyMessage(errorPacket, sender);
				}
			}
		} else if (packet.getId() == RakNetPacket.ID_OPEN_CONNECTION_REQUEST_2) {
			OpenConnectionRequestTwo connectionRequestTwo = new OpenConnectionRequestTwo(packet);
			connectionRequestTwo.decode();
			if (!connectionRequestTwo.failed() && connectionRequestTwo.magic == true
					&& connectionRequestTwo.maximumTransferUnit >= RakNet.MINIMUM_MTU_SIZE) {
				RakNetPacket errorPacket = this.validateSender(sender, connectionRequestTwo.clientGuid);
				if (errorPacket == null) {
					OpenConnectionResponseTwo connectionResponseTwo = new OpenConnectionResponseTwo();
					connectionResponseTwo.serverGuid = this.guid;
					connectionResponseTwo.clientAddress = sender;
					connectionResponseTwo.maximumTransferUnit = Math.min(connectionRequestTwo.maximumTransferUnit,
							maximumTransferUnit);
					connectionResponseTwo.encode();
					if (!connectionResponseTwo.failed()) {
						this.callEvent(
								listener -> listener.onConnect(this, sender, connectionRequestTwo.connectionType));
						clients.put(sender,
								new RakNetClientPeer(this, connectionRequestTwo.connectionType,
										connectionRequestTwo.clientGuid, connectionResponseTwo.maximumTransferUnit,
										channel, sender));
						this.sendNettyMessage(connectionResponseTwo, sender);
					}
				} else {
					this.sendNettyMessage(errorPacket, sender);
				}
			}
		}
		logger.trace("Handled " + RakNetPacket.getName(packet) + " packet from " + sender);
	}

	/**
	 * Validates the sender of a packet.
	 * <p>
	 * This is called throughout initial client connection to make sure there
	 * are no issues.
	 * 
	 * @param sender
	 *            the address of the packet sender.
	 * @param guid
	 *            the globally unique ID of the sender, {@value #NO_GUID} if
	 *            there is none.
	 * @return the packet to respond with if there was an error,
	 *         <code>null</code> if there are no issues.
	 * @throws NullPointerException
	 *             if the <code>sender</code> is <code>null</code>.
	 */
	private final RakNetPacket validateSender(InetSocketAddress sender, long guid) throws NullPointerException {
		if (sender == null) {
			throw new NullPointerException("Sender cannot be null");
		} else if (this.hasClient(sender) || (this.hasClient(guid) && guid != NO_GUID)) {
			return new RakNetPacket(RakNetPacket.ID_ALREADY_CONNECTED);
		} else if (this.getClientCount() >= maxConnections && maxConnections >= 0) {
			return new RakNetPacket(RakNetPacket.ID_NO_FREE_INCOMING_CONNECTIONS);
		} else if (this.isClientBanned(sender.getAddress())) {
			ConnectionBanned connectionBanned = new ConnectionBanned();
			connectionBanned.serverGuid = guid;
			connectionBanned.encode();
			return connectionBanned;
		}
		return null;
	}

	/**
	 * Sends a Netty message over the channel raw.
	 * <p>
	 * This should be used sparingly, as if it is used incorrectly it could
	 * break client peers entirely. In order to send a message to a peer, use
	 * one of the
	 * {@link com.whirvis.jraknet.peer.RakNetPeer#sendMessage(com.whirvis.jraknet.protocol.Reliability, io.netty.buffer.ByteBuf)
	 * sendMessage()} methods.
	 * 
	 * @param buf
	 *            the buffer to send.
	 * @param address
	 *            the address to send the buffer to.
	 * @throws NullPointerException
	 *             if the <code>buf</code>, <code>address</code>, or IP address
	 *             of the <code>address</code> are <code>null</code>.
	 */
	public final void sendNettyMessage(ByteBuf buf, InetSocketAddress address) throws NullPointerException {
		if (buf == null) {
			throw new NullPointerException("Buffer cannot be null");
		} else if (address == null) {
			throw new NullPointerException("Address cannot be null");
		} else if (address.getAddress() == null) {
			throw new NullPointerException("IP address cannot be null");
		}
		channel.writeAndFlush(new DatagramPacket(buf, address));
		logger.trace("Sent netty message with size of " + buf.capacity() + " bytes (" + (buf.capacity() * 8)
				+ " bits) to " + address);
	}

	/**
	 * Sends a Netty message over the channel raw.
	 * <p>
	 * This should be used sparingly, as if it is used incorrectly it could
	 * break client peers entirely. In order to send a message to a peer, use
	 * one of the
	 * {@link com.whirvis.jraknet.peer.RakNetPeer#sendMessage(Reliability, Packet)
	 * sendMessage()} methods.
	 * 
	 * @param packet
	 *            the packet to send.
	 * @param address
	 *            the address to send the packet to.
	 * @throws NullPointerException
	 *             if the <code>packet</code>, <code>address</code>, or IP
	 *             address of the <code>address</code> are <code>null</code>.
	 */
	public final void sendNettyMessage(Packet packet, InetSocketAddress address) throws NullPointerException {
		if (packet == null) {
			throw new NullPointerException("Packet cannot be null");
		}
		this.sendNettyMessage(packet.buffer(), address);
	}

	/**
	 * Sends a Netty message over the channel raw.
	 * <p>
	 * This should be used sparingly, as if it is used incorrectly it could
	 * break client peers entirely. In order to send a message to a peer, use
	 * one of the
	 * {@link com.whirvis.jraknet.peer.RakNetPeer#sendMessage(com.whirvis.jraknet.protocol.Reliability, int)
	 * sendMessage()} methods.
	 * 
	 * @param packetId
	 *            the packet ID to send.
	 * @param address
	 *            the address to send the packet to.
	 * @throws NullPointerException
	 *             if the <code>address</code> or IP address of the
	 *             <code>address</code> are <code>null</code>.
	 */
	public final void sendNettyMessage(int packetId, InetSocketAddress address) throws NullPointerException {
		this.sendNettyMessage(new RakNetPacket(packetId), address);
	}

	/**
	 * Returns whether or not the server is running.
	 * 
	 * @return <code>true</code> if the server is running, <code>false</code>
	 *         otherwise.
	 */
	public final boolean isRunning() {
		return this.running;
	}

	/**
	 * Starts the server.
	 * 
	 * @throws IllegalStateException
	 *             if the server is already running.
	 * @throws RakNetException
	 *             if an error occurs during startup.
	 */
	public void start() throws IllegalStateException, RakNetException {
		if (running == true) {
			throw new IllegalStateException("Server is already running");
		} else if (listeners.isEmpty()) {
			logger.warn("Server has no listeners");
		}
		try {
			this.bootstrap = new Bootstrap();
			this.group = new NioEventLoopGroup();
			this.handler = new RakNetServerHandler(this);
			bootstrap.handler(handler);

			// Create bootstrap and bind channel
			bootstrap.channel(NioDatagramChannel.class).group(group);
			bootstrap.option(ChannelOption.SO_BROADCAST, true).option(ChannelOption.SO_REUSEADDR, false)
					.option(ChannelOption.SO_SNDBUF, maximumTransferUnit)
					.option(ChannelOption.SO_RCVBUF, maximumTransferUnit)
					.option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(maximumTransferUnit));
			this.channel = (bindingAddress != null ? bootstrap.bind(bindingAddress) : bootstrap.bind(0)).sync()
					.channel();
			this.bindAddress = (InetSocketAddress) channel.localAddress();
			this.running = true;
			logger.debug("Created and bound bootstrap");

			// Create and start peer update thread
			RakNetServer server = this;
			this.peerThread = new Thread(
					RakNetServer.class.getSimpleName() + "-Peer-Thread-" + Long.toHexString(guid).toUpperCase()) {

				@Override
				public void run() {
					HashMap<RakNetClientPeer, Throwable> disconnected = new HashMap<RakNetClientPeer, Throwable>();
					while (server.running == true && !this.isInterrupted()) {
						try {
							Thread.sleep(0, 1); // Lower CPU usage
						} catch (InterruptedException e) {
							this.interrupt(); // Interrupted during sleep
							continue;
						}
						for (RakNetClientPeer peer : clients.values()) {
							if (!peer.isDisconnected()) {
								try {
									peer.update();
									if (peer.getPacketsReceivedThisSecond() >= RakNet.getMaxPacketsPerSecond()) {
										server.blockAddress(peer.getInetAddress(), "Too many packets",
												RakNet.MAX_PACKETS_PER_SECOND_BLOCK);
									}
								} catch (Throwable throwable) {
									server.callEvent(listener -> listener.onPeerException(server, peer, throwable));
									disconnected.put(peer, throwable);
								}
							}
						}

						/*
						 * Disconnect peers.
						 * 
						 * This must be done here as simply removing a client
						 * from the clients map would be an incorrect way of
						 * disconnecting a client. This means that calling the
						 * disconnect() method is required. However, calling it
						 * while in the loop would cause a
						 * ConcurrentModifactionException. To get around this,
						 * the clients that need to be disconnected are properly
						 * disconnected after the loop is finished. This is done
						 * simply by having them and their disconnect reason be
						 * put in a disconnection map.
						 */
						if (disconnected.size() > 0) {
							for (RakNetClientPeer peer : disconnected.keySet()) {
								server.disconnect(peer, disconnected.get(peer));
							}
							disconnected.clear();
						}
					}
				}

			};
			peerThread.start();
			logger.debug("Created and started peer update thread");
			this.callEvent(listener -> listener.onStart(this));
		} catch (InterruptedException e) {
			this.running = false;
			throw new RakNetException(e);
		}
		logger.info("Started server");
	}

	/**
	 * Stops the server.
	 * <p>
	 * All currently connected clients will be disconnected with the same reason
	 * used for shutdown.
	 * 
	 * @param reason
	 *            the reason for shutdown. A <code>null</code> reason will have
	 *            <code>"Server shutdown"</code> be used as the reason instead.
	 * @throws IllegalStateException
	 *             if the server is not running.
	 */
	public void shutdown(String reason) throws IllegalStateException {
		if (running == false) {
			throw new IllegalStateException("Server is not running");
		}

		// Disconnect clients
		for (RakNetClientPeer client : clients.values()) {
			this.disconnect(client, reason == null ? "Server shutdown" : reason);
		}
		clients.clear();

		// Stop server
		this.running = false;
		peerThread.interrupt();
		logger.info("Shutdown server" + (reason != null ? " for \"" + reason + "\"" : ""));

		// Shutdown networking
		channel.close();
		group.shutdownGracefully(0L, 1000L, TimeUnit.MILLISECONDS);
		this.channel = null;
		this.handler = null;
		this.group = null;
		this.bootstrap = null;
		logger.debug("Shutdown networking");
		this.callEvent(listener -> listener.onShutdown(this));
	}

	/**
	 * Stops the server.
	 * <p>
	 * All currently connected clients will be disconnected with the same reason
	 * used for shutdown.
	 * 
	 * @param reason
	 *            the reason for shutdown. A <code>null</code> reason will have
	 *            <code>"Server shutdown"</code> be used as the reason instead.
	 * @throws IllegalStateException
	 *             if the server is not running.
	 */
	public final void shutdown(Throwable reason) throws IllegalStateException {
		this.shutdown(reason != null ? RakNet.getStackTrace(reason) : null);
	}

	/**
	 * Stops the server.
	 * <p>
	 * All currently connected clients will be disconnected with the same reason
	 * used for shutdown.
	 * 
	 * @throws IllegalStateException
	 *             if the server is not running.
	 */
	public final void shutdown() throws IllegalStateException {
		this.shutdown((String) /* Solves ambiguity */ null);
	}

	@Override
	public String toString() {
		return "RakNetServer [bindingAddress=" + bindingAddress + ", guid=" + guid + ", pongId=" + pongId
				+ ", timestamp=" + timestamp + ", maximumTransferUnit=" + maximumTransferUnit + ", maxConnections="
				+ maxConnections + ", broadcastingEnabled=" + broadcastingEnabled + ", identifier=" + identifier
				+ ", bindAddress=" + bindAddress + ", running=" + running + ", getProtocolVersion()="
				+ getProtocolVersion() + ", getTimestamp()=" + getTimestamp() + ", getAddress()=" + getAddress()
				+ ", getClientCount()=" + getClientCount() + "]";
	}

}