public class Nyx {
    /**
     * The singleton instance of Nyx
     */
    @Getter private static Nyx instance = null;

    /**
     * <p>The server's main thread</p>
     */
    @Getter private final Thread mainThread;
    /**
     * <p>The directory where JAR module files are loaded</p>
     * <p>Can be specified from command line using the <code>-m</code> (<code>--module-path</code>) option.</p>
     */
    @Getter private final File modulePath;
    /**
     * <p>The parent directory of all simple loaded and saved data of this server instance.</p>
     * <p>Can be specified from command line using the {@code -d} ({@code --data-path}) option.</p>
     * <p>Developers who clone this project should set this path to an outside directory or one of the files listed in
     * {@code .gitignore}. The {@code /data/} entry was designated for this use.</p>
     */
    @Getter private final File dataPath;
    /**
     * <p>The {@link Logger log4j2 logger} for this server</p>
     */
    @Getter(lazy = true) private final static Logger log = LogManager.getLogger(Nyx.class);
    /**
     * <p>The loaded {@code nyx.xml} config file</p>
     */
    @Getter private final NyxConfig config;
    /**
     * <p>An internal list of module managers used to accept modules</p>
     */
    @Getter private final Map<String, ModuleManager<?>> moduleManagers = new HashMap<>();
    /**
     * <p>{@link Ticker} is the server's core pacemaker. Manages the main loop that keeps the server on.</p>
     */
    @Getter private final Ticker ticker;
    /**
     * <p>The {@link NetworkManager} for this server.</p>
     */
    @Getter private final NetworkManager network;
    /**
     * <p>The {@link PlayerManager} for this server.</p>
     */
    @Getter private final PlayerManager players;
    /**
     * <p>The {@link CommandManager} for this server.</p>
     */
    @Getter private final CommandManager commandManager;
    /**
     * <p>A thread in charge of reading command input from console ({@link System#in}).</p>
     */
    @Getter private final ConsoleReader console;
    /**
     * <p>A list of tasks to run <em>after</em> the last tick of the server is completed.</p>
     */
    @Getter private final List<Runnable> shutdownTasks = new ArrayList<>();

    @Builder
    private Nyx(File modulePath, File dataPath, long serverStartTime) {
        if(instance != null) {
            throw new IllegalStateException("An instance of Nyx already exists");
        }
        instance = this;
        mainThread = Thread.currentThread();

        this.modulePath = modulePath;
        this.dataPath = dataPath;
        getLog().info("Loading configuration...");
        config = loadConfig();

        network = new NetworkManager();
        players = new PlayerManager();
        ticker = new Ticker();
        commandManager = new CommandManager();
        console = new ConsoleReader();

        getLog().info("Loading modules...");
        loadBuiltinModules();
        loadJarModules();

        network.start();
        console.start();

        getLog().info(String.format("Server started (%s s)", (System.currentTimeMillis() - serverStartTime) / 1000d));
        ticker.startTicking();
    }

    @SneakyThrows({IOException.class})
    private NyxConfig loadConfig() {
        File configFile = new File(dataPath, "nyx.xml");
        if(!configFile.isFile()) {
            IOUtils.copy(getClass().getClassLoader().getResourceAsStream("nyx.xml"), new FileOutputStream(configFile));
        }

        return (NyxConfig) new ConfigParser(configFile).parse();
    }

    @SneakyThrows({IOException.class})
    private void loadBuiltinModules() {
        URL resource = getClass().getClassLoader().getResource("mods.list");
        assert resource != null;
        @Cleanup BufferedReader reader = new BufferedReader(new InputStreamReader(resource.openStream()));
        reader.lines().forEach(line -> {
            line = line.trim();
            String[] args = line.split(":", 3);
            if(line.charAt(0) == '#' || args.length != 3) {
                return;
            }
            String moduleType = args[0].trim();
            if(moduleManagers.containsKey(moduleType)) {
                try {
                    getLog().info(String.format("Loading builtin %s module: %s", moduleType, args[1]));
                    moduleManagers.get(moduleType).loadForClass(args[1].trim(), args[2].trim());
                } catch(Exception e) {
                    getLog().error("Cannot load builtin module", e);
                }
            } else {
                getLog().error("Unknown module type: " + moduleType);
            }
        });
    }

    private void loadJarModules() {
        File[] files = modulePath.listFiles((dir, name) -> name.toLowerCase(Locale.ENGLISH).endsWith(".jar"));
        assert files != null;
        for(File file : files) {
            Properties props;
            try {
                props = Util.loadJar(file);
            } catch(IOException e) {
                e.printStackTrace();
                continue;
            }
            String moduleType = props.getProperty("type");
            if(moduleManagers.containsKey(moduleType)) {
                try {
                    getLog().info(String.format("Loading %s module from jar: %s", moduleType, props.getProperty("name")));
                    moduleManagers.get(moduleType).loadForClass(props.getProperty("name"), props.getProperty("mainClass"));
                } catch(Exception e) {
                    getLog().error("Cannot load jar module", e);
                }
            } else {
                getLog().error("Jar module " + file.getName() + " declares an unknown module type: " + moduleType);
            }
        }
    }

    /**
     * <p>Adds a {@link Runnable} task that will be run <em>after</em> the last server tick is completed.</p>
     *
     * @param runnable the task to run
     */
    public void addShutdownTask(Runnable runnable) {
        shutdownTasks.add(runnable);
    }

    /**
     * <p>Causes the current server tick to be the last tick that runs, followed by running the {@link
     * #shutdownTasks}.</p>
     *
     * <p>This method will not block until the last server tick is completed. Actually, this method should only be
     * triggered from the main thread, i.e. from a server tick.</p>
     */
    public void shutdown() {
        ticker.stopTicking();
    }

    public void throwMainThread() {
        if(Thread.currentThread() != mainThread){
            throw new IllegalStateException("Only run in main thread");
        }
    }
} public abstract class Client {
    @Getter private Player assignedPlayer;
    @Getter private PingStats pingStats;
    @Getter private long lastAlive;

    protected Client() {
        refreshActive();
    }

    public abstract Protocol getSourceProtocol();

    public abstract SessionHandler getSessionHandler();

    public abstract void handlePacket(ByteBuffer buffer);

    public void close(CloseReason closeReason) {
        getSessionHandler().close();
        if(assignedPlayer != null) {
            assignedPlayer.detachClient(this);
        }
    }

    public void tick() {
        if(System.currentTimeMillis() - lastAlive > 10e+3){
            close(CloseReason.CLIENT_TIMEOUT);
        }
    }

    public InetSocketAddress getAddress() {
        return getSessionHandler().getAddress();
    }

    public void refreshActive() {
        lastAlive = System.currentTimeMillis();
    }

    public enum CloseReason {
        CLIENT_DISCONNECT,
        CLIENT_TIMEOUT,
        SERVER_DISCONNECT
    }
} public abstract class Protocol {
    protected Protocol() {
        init();
    }

    public abstract void init();

    public abstract Client acceptSession(SessionHandler session);

    public abstract String getName();
} @RequiredArgsConstructor
public abstract class SessionHandler {
    @Getter private final InetSocketAddress address;
    @Getter private final SharedServerSocket socket;

    public abstract void onReceive(PacketWithAddress pk);

    public abstract void send(ByteBuffer buffer);

    public abstract void close();

    public abstract Protocol getProtocol();

    public void tick() {

    }

    /**
     * <p>Returns whether this session an orphan.</p>
     *
     * <p>An orphan session is a session that has not been recognized by any sessions yet.</p>
     *
     * @return whether this session is an orphan.
     */
    public abstract boolean isOrphan();
} public class UdpSessionHandler extends SessionHandler {
    @Getter private UdpInitStack initStack = new UdpInitStack();
    @Getter private Protocol protocol;
    @Getter private Client client;

    public UdpSessionHandler(InetSocketAddress address, SharedUdpServerSocket socket) {
        super(address, socket);
    }

    @Override
    public void onReceive(PacketWithAddress pk) {
        if(protocol == null) {
            Nyx.getLog().debug(String.format("Handling packet from %s", pk.getAddress()));
            initStack.add(pk.getBuffer());
            Client client = NetworkManager.getInstance().adoptSession(this);
            if(client != null) {
                protocol = client.getSourceProtocol();
                this.client = client;
                initStack = null;
                Nyx.getLog().debug(String.format("Client %s is adopted by %s", getAddress(), protocol.getName()));
            }
        } else {
            Nyx.getLog().debug(String.format("Handling packet from %s", pk.getAddress()));
            client.handlePacket(pk.getBuffer());
        }
    }

    @Override
    public void send(ByteBuffer buffer) {
        ((SharedUdpServerSocket) getSocket()).getThread().pushSend(new PacketWithAddress(buffer, getAddress(), buffer.position() != 0));
    }

    @Override
    public void close() {
        ((SharedUdpServerSocket) getSocket()).onSessionClosed(this);
    }

    @Override
    public void tick() {
        super.tick();
        if(client != null) {
            client.tick();
        }
    }

    @Override
    public boolean isOrphan() {
        return client == null;
    }
} public abstract class DataPacket {
    @Getter @Setter private ByteBuffer buffer;

    public abstract byte getPacketId();

    public void write() {
        throw new UnsupportedOperationException("Packet is client->server only");
    }

    public DataPacket parse(){
        read();
        return this;
    }
    public void read() {
        throw new UnsupportedOperationException("Packet is server->client only");
    }

    public void writeByte(byte b) {
        buffer.put(b);
    }

    public void writeShort(short s) {
        buffer.order(BIG_ENDIAN).putShort(s);
    }

    public void writeTriad(int t) {
        buffer.put((byte) ((t >> 16) & 0xFF))
                .put((byte) ((t >> 8) & 0xFF))
                .put((byte) (t & 0xFF));
    }

    public void writeInt(int i) {
        buffer.order(BIG_ENDIAN).putInt(i);
    }

    public void writeLong(long l) {
        buffer.order(BIG_ENDIAN).putLong(l);
    }

    public void writeFloat(float f) {
        buffer.order(BIG_ENDIAN).putFloat(f);
    }

    public void writeDouble(double d) {
        buffer.order(BIG_ENDIAN).putDouble(d);
    }

    public void writeLShort(short s) {
        buffer.order(LITTLE_ENDIAN).putShort(s);
    }

    public void writeLTriad(int t) {
        buffer.put((byte) (t & 0xFF))
                .put((byte) ((t >> 8) & 0xFF))
                .put((byte) ((t >> 16) & 0xFF));
    }

    public void writeLInt(int i) {
        buffer.order(LITTLE_ENDIAN).putInt(i);
    }

    public void writeLLong(long l) {
        buffer.order(LITTLE_ENDIAN).putLong(l);
    }

    public void writeLFloat(float f) {
        buffer.order(LITTLE_ENDIAN).putFloat(f);
    }

    public void writeLDouble(double d) {
        buffer.order(LITTLE_ENDIAN).putDouble(d);
    }

    public void writeBytes(byte[] bytes) {
        buffer.put(bytes);
    }

    public void writeBytes(ByteBuffer bytes) {
        buffer.put(bytes);
    }

    public byte readByte() {
        return buffer.get();
    }

    public short readShort() {
        return buffer.order(BIG_ENDIAN).getShort();
    }

    public int readTriad() {
        return (buffer.get() << 16) | (buffer.get() << 8) | buffer.get();
    }

    public int readInt() {
        return buffer.order(BIG_ENDIAN).getInt();
    }

    public long readLong() {
        return buffer.order(BIG_ENDIAN).getLong();
    }

    public float readFloat() {
        return buffer.order(BIG_ENDIAN).getFloat();
    }

    public double readDouble() {
        return buffer.order(BIG_ENDIAN).getDouble();
    }

    public short readLShort() {
        return buffer.order(LITTLE_ENDIAN).getShort();
    }

    public int readLTriad() {
        return buffer.get() | (buffer.get() << 8) | (buffer.get() << 16);
    }

    public int readLInt() {
        return buffer.order(LITTLE_ENDIAN).getInt();
    }

    public long readLLong() {
        return buffer.order(LITTLE_ENDIAN).getLong();
    }

    public float readLFloat() {
        return buffer.order(LITTLE_ENDIAN).getFloat();
    }

    public double readLDouble() {
        return buffer.order(LITTLE_ENDIAN).getDouble();
    }

    public byte[] readBytes(int length) {
        byte[] bytes = new byte[length];
        buffer.get(bytes);
        return bytes;
    }

    @Override
    @SneakyThrows(CloneNotSupportedException.class)
    public DataPacket clone() {
        return (DataPacket) super.clone();
    }
} public class McpeDataPacketManager {
    @Getter private static McpeDataPacketManager instance = new McpeDataPacketManager();

    @Getter private Map<Byte, DataPacket> packets = new HashMap<>();

    public McpeDataPacketManager(){
        registerPackets();
    }

    private void registerPackets() {
        registerPacket(new LoginPacket());
    }

    public void registerPacket(@NonNull DataPacket instance) {
        packets.put(instance.getPacketId(), instance);
    }

    public DataPacket get(ByteBuffer buffer) {
        return get(buffer.get(), buffer);
    }

    public DataPacket get(byte pid, ByteBuffer rest){
        if(packets.containsKey(pid)) {
            return packets.get(pid).clone().setBuffer(rest).parse();
        }
        return null;
    }
} @RequiredArgsConstructor
public class RakNetInterface {
    @Getter private final McpeClient client;
    private final List<RawDataPacket> sendQueue = new ArrayList<>();
    private int nextSeqNumber = 0;
    private final Map<Integer, SentDataPacketGroup> nackRecovery = new HashMap<>();
    private final List<SentDataPacketGroup> nackReplyQueue = new LinkedList<>();
    private final List<Integer> ackQueue = new ArrayList<>();
    private final List<Integer> nackQueue = new ArrayList<>(); // TODO implement NACK
    private Map<Long, Ping> pingMap = new HashMap<>();
    private long lastPing = 0;

    public void handlePacket(ReceivedPacket packet) {
//        Nyx.getInstance().getLog().debug(String.format("Received %s from %s", packet.getClass().getSimpleName(), client.getAddress()));

        if(packet instanceof ReceivedDataPacketGroup) {
            ReceivedDataPacketGroup group = (ReceivedDataPacketGroup) packet;
            group.packets.forEach(this::handleDataPacket);
            ackQueue.add(group.seqNumber);
            return;
        }
        if(packet instanceof ReceivedAck) {
            ((ReceivedAck) packet).getSeqNumbers().forEach(nackRecovery::remove);
            return;
        }
        if(packet instanceof ReceivedNack) {
            for(Integer seqNumber : ((ReceivedNack) packet).getSeqNumbers()) {
                SentDataPacketGroup resend = nackRecovery.remove(seqNumber);
                if(resend == null) {
                    Nyx.getLog().debug(String.format("Client %s sent NACK %s for unknown seqNumber %d. Buffer: %s",
                            client.getAddress(), packet, seqNumber, Hex.encodeHexString(packet.getBuffer().array())));
                    continue;
                }
                resend.seqNumber = getNextSeqNumber();
                queueNackReply(resend);
            }
            return;
        }

        if(client.getLoginStep() == LOGIN_STEP_WAIT_REQUEST_1) {
            if(!(packet instanceof OpenConnectionRequest1)) {
                client.unexpectedPacket(packet);
                return;
            }
            OpenConnectionRequest1 request = (OpenConnectionRequest1) packet;
            OpenConnectionReply1 reply = new OpenConnectionReply1();
            reply.serverId = client.getProtocol().getServerId();
            reply.mtuSize = (short) request.getMtuSize();
            sendRakNetPacket(reply);
            client.setLoginStep(LOGIN_STEP_WAIT_REQUEST_2);
        } else if(client.getLoginStep() == LOGIN_STEP_WAIT_REQUEST_2) {
            if(!(packet instanceof OpenConnectionRequest2)) {
                client.unexpectedPacket(packet);
                return;
            }
            OpenConnectionRequest2 request = (OpenConnectionRequest2) packet;
            OpenConnectionReply2 reply = new OpenConnectionReply2();
            reply.serverId = client.getProtocol().getServerId();
            reply.address = client.getSession().getAddress();
            reply.mtuSize = request.getMtuSize();
            sendRakNetPacket(reply);
            client.setLoginStep(LOGIN_STEP_WAIT_CONNECT);
        } else {
            client.unexpectedPacket(packet);
        }
    }

    private void handleDataPacket(RawDataPacket packet) {
        ByteBuffer buffer = ByteBuffer.wrap(packet.buffer).order(ByteOrder.BIG_ENDIAN);
        byte pid = buffer.get();

        if(pid == DATA_PING) {
            long pingId = buffer.getLong();
            Pong pong = new Pong();
            pong.pingId = pingId;
            RawDataPacket pk = new RawDataPacket();
            pk.reliability = 0;
            pk.buffer = pong.write().array();
            queueDataPacket(pk);
            return;
        }
        if(pid == DATA_PONG) {
            long pingId = buffer.getLong();
            Ping ping = pingMap.remove(pingId);
            if(ping == null) {
                Nyx.getLog().error(String.format("Client from %s sent PONG with unknown ping ID %d",
                        client.getAddress(), pingId));
                return;
            }
            client.getPingStats().addDatum(ping.sendTimeNano, true);
            return;
        }
        if(pid == DATA_CLIENT_DISCONNECT) {
            client.close(Client.CloseReason.CLIENT_DISCONNECT);
        }

        if(client.getLoginStep() == LOGIN_STEP_WAIT_CONNECT) {
            if(pid != DATA_CLIENT_CONENCT) {
                client.unexpectedPacket(packet);
                return;
            }
            ClientConnect request = new ClientConnect(buffer);
            ServerHandshake response = new ServerHandshake();
            response.address = client.getSession().getAddress();
            response.ping = request.pingTime;
            response.pong = request.pingTime + 1000;
            RawDataPacket pk = new RawDataPacket();
            pk.reliability = 0;
            pk.buffer = response.write().array();
            queueDataPacket(pk);
            flushDataPacketQueue();
            client.setLoginStep(LOGIN_STEP_WAIT_HANDSHAKE);
        } else if(client.getLoginStep() == LOGIN_STEP_WAIT_HANDSHAKE) {
            if(pid != DATA_CLIENT_HANDSHAKE) {
                client.unexpectedPacket(packet);
                return;
            }
            ClientHandshake handshake = new ClientHandshake(buffer);
            // TODO log referrer?
            // TODO check port?
            client.setLoginStep(LOGIN_STEP_IN_GAME);
            Nyx.getLog().debug(String.format("Client %s is now logging in!", client.getAddress()));
        } else if(client.getLoginStep() != LOGIN_STEP_IN_GAME) {
            client.unexpectedPacket(packet);
        } else {
            if(pid != RakNetConsts.DATA_MCPE) {
                client.unexpectedPacket(packet);
                return;
            }

            client.handleDataPacket(buffer);
        }
    }

    public void sendRakNetPacket(SentPacket packet) {
        client.getSession().send(packet.write());
    }

    public void queueDataPacket(RawDataPacket packet) {
        Nyx.getLog().debug(String.format("Sending data packet 0x%x to %s: %s", packet.buffer[0], client.getAddress(), Hex.encodeHexString(packet.buffer)));
        sendQueue.add(packet);
    }

    public void flushDataPacketQueue() {
        SentDataPacketGroup group = new SentDataPacketGroup((byte) 0x84); // FIXME why?
        group.seqNumber = getNextSeqNumber();
        group.lastSendNanos = System.nanoTime();
        group.getPackets().addAll(sendQueue);
        sendRakNetPacket(group);
        nackRecovery.put(group.seqNumber, group);
        sendQueue.clear();
    }

    public void sendDataPacketImmediate(RawDataPacket packet) {
        SentDataPacketGroup group = new SentDataPacketGroup((byte) 0); // FIXME why?
        group.seqNumber = getNextSeqNumber();
        group.lastSendNanos = System.nanoTime();
        group.getPackets().add(packet);
        sendRakNetPacket(group);
        nackRecovery.put(group.seqNumber, group);
    }

    public void queueNackReply(SentDataPacketGroup group) {
        nackReplyQueue.add(group);
    }

    public void flushNackReplyQueue() {
        Iterator<SentDataPacketGroup> iterator = nackReplyQueue.iterator();
        for(int i = 0; i < 16 && iterator.hasNext(); i++) {
            sendRakNetPacket(iterator.next());
            iterator.remove();
        }
        // TODO manage reliability
    }

    public void flushAckQueue() {
        if(ackQueue.isEmpty()) {
            return;
        }
        SentAckNack ack = new SentAckNack(SentAckNack.ACK);
        ack.getSeqNumbers().addAll(ackQueue);
        sendRakNetPacket(ack);
        ackQueue.clear();
    }

    public void flushNackQueue() {
        if(nackQueue.isEmpty()) {
            return;
        }
        SentAckNack nack = new SentAckNack(SentAckNack.NACK);
        nack.getSeqNumbers().addAll(nackQueue);
        sendRakNetPacket(nack);
        nackQueue.clear();
    }

    public void tick() {
        flushDataPacketQueue();
        flushNackReplyQueue();
        flushAckQueue();
        flushNackQueue();
        pingMap = pingMap.entrySet().stream().filter(this::filterPingMapEntry).collect(Collectors.toMap(Entry::getKey, Entry::getValue));
        if(lastPing - System.currentTimeMillis() > 2000) {
            firePing();
        }
    }

    private boolean filterPingMapEntry(Entry<Long, Ping> entry) {
        if(System.nanoTime() - entry.getValue().sendTimeNano > 10e+9) {
            client.getPingStats().addDatum(entry.getValue().sendTimeNano, false);
            return false;
        }
        return true;
    }

    public void firePing() {
        long pingId = new Random().nextLong();
        Ping ping = new Ping();
        ping.pingId = pingId;
        RawDataPacket pk = new RawDataPacket();
        pk.reliability = 0;
        pk.buffer = ping.write().array();
        queueDataPacket(pk);
        pingMap.put(pingId, ping);
        flushDataPacketQueue(); // FIXME should do this?
    }

    public int getNextSeqNumber() {
        return nextSeqNumber++;
    }
} public class RawDataPacket {
    public final static byte RELIABILITY_UNRELIABLE = (byte) 0x00;
    public final static byte RELIABILITY_UNRELIABLE_SEQUENCED = (byte) 0x01;
    public final static byte RELIABILITY_RELIABLE = (byte) 0x02;
    public final static byte RELIABILITY_RELIABLE_ORDERED = (byte) 0x03;
    public final static byte RELIABILITY_RELIABLE_SEQUENCED = (byte) 0x04;
    public final static byte RELIABILITY_UNRELIABLE_WITH_ACK_RECEIPT = (byte) 0x05;
    public final static byte RELIABILITY_UNRELIABLE_SEQUENCED_WITH_ACK_RECEIPT = (byte) 0x06;
    public final static byte RELIABILITY_RELIABLE_WITH_ACK_RECEIPT = (byte) 0x07;
    public final static byte RELIABILITY_RELIABLE_ORDERED_WITH_ACK_RECEIPT = (byte) 0x08;
    public final static byte RELIABILITY_RELIABLE_SEQUENCED_WITH_ACK_RECEIPT = (byte) 0x09;

    public byte[] buffer;
    public byte reliability;
    public boolean hasSplit;
    public int orderIndex;
    public int messageIndex;
    public byte orderChannel;
    public int splitCount;
    public short splitId;
    public int splitIndex;

    public static RawDataPacket read(ReceivedPacket packet) {
        RawDataPacket instance = new RawDataPacket();
        byte flags = packet.getByte();
        instance.reliability = (byte) ((flags & 0b11100000) >>> 5);
        instance.hasSplit = (flags & 0b00010000) > 0;
        instance.buffer = new byte[(int) Math.ceil(packet.getShort() / 8d)];

        switch(instance.reliability) {
            case 1:
                instance.orderIndex = packet.getLTriad();
                instance.orderChannel = packet.getByte();
                break;
            case 2:
                instance.messageIndex = packet.getLTriad();
                break;
            case 3:
            case 4:
                instance.messageIndex = packet.getLTriad();
                instance.orderIndex = packet.getLTriad();
                instance.orderChannel = packet.getByte();
                break;
            case 6:
            case 7:
                instance.messageIndex = packet.getLTriad();
                break;
        }

        if(instance.hasSplit) {
            instance.splitCount = packet.getInt();
            instance.splitId = packet.getShort();
            instance.splitIndex = packet.getInt();
        }

        packet.getBuffer().get(instance.buffer);

        return instance;
    }

    public byte[] write() {
        ByteBuffer bb = ByteBuffer.allocate(binaryLength());
        bb.put((byte) ((reliability << 5) | (hasSplit ? 0b00010000 : 0))).putShort((short) (buffer.length << 3));
        switch(reliability) {
            case 1:
                putLTriad(bb, orderIndex);
                bb.put(orderChannel);
                break;
            case 2:
                putLTriad(bb, messageIndex);
                break;
            case 3:
            case 4:
                putLTriad(bb, messageIndex);
                putLTriad(bb, orderIndex);
                bb.put(orderChannel);
                break;
            case 6:
            case 7:
                putLTriad(bb, messageIndex);
                break;
        }
        if(hasSplit) {
            bb.putInt(splitCount).putShort(splitId).putInt(splitIndex);
        }
        bb.put(buffer);

        return bb.array();
    }

    public int binaryLength() {
        int relLen = 0;
        switch(reliability) {
            case 1:
            case 2:
            case 6:
            case 7:
                relLen = 3;
                break;
            case 3:
            case 4:
                relLen = 6;
                break;
        }
        return 3 + buffer.length + (hasSplit ? 10 : 0) + relLen;
    }

    public static ByteBuffer putLTriad(ByteBuffer buffer, int triad) {
        buffer.put((byte) (triad & 0xFF));
        buffer.put((byte) ((triad >> 8) & 0xFF));
        buffer.put((byte) ((triad >> 16) & 0xFF));
        return buffer;
    }
} public class ClientDisconnect extends SentPacket {
    @Override
    public byte getId() {
        return RakNetConsts.DATA_CLIENT_DISCONNECT;
    }

    @Override
    protected int length() {
        return 0;
    }

    @Override
    protected void encode() {
    }
} public class RakNetPacketManager {
    @Getter private static RakNetPacketManager instance;

    static {
        instance = new RakNetPacketManager();
        instance.registerPackets();
    }

    private Map<Byte, ReceivedPacket> packetTypes = new HashMap<>();

    @SneakyThrows({IllegalAccessException.class, InstantiationException.class})
    private void registerPackets() {
        registerPacket(OpenConnectionRequest1.class);
        registerPacket(OpenConnectionRequest2.class);
        for(byte pid = RakNetConsts.DATA_PACKET_MIN; pid <= RakNetConsts.DATA_PACKET_MAX; pid++) {
            registerPacket(pid, new ReceivedDataPacketGroup(pid));
        }
        registerPacket(ReceivedAck.class);
        registerPacket(ReceivedNack.class);
    }

    public void registerPacket(Class<? extends ReceivedPacket> clazz) throws IllegalAccessException, InstantiationException {
        ReceivedPacket pk = clazz.newInstance();
        registerPacket(pk.getId(), pk);
    }

    public void registerPacket(byte pid, ReceivedPacket packet) {
        packetTypes.put(pid, packet);
    }

    public ReceivedPacket parsePacket(ByteBuffer buffer) {
        byte pid = buffer.get();
        if(packetTypes.containsKey(pid)) {
            ReceivedPacket packet = packetTypes.get(pid).getClone();
            packet.parse(buffer);
            return packet;
        }
        return null;
    }
}
import org.projectnyx.network.mcpe.mcpe.McpeDataPacketManager;
import org.projectnyx.network.mcpe.raknet.RakNetInterface;
import org.projectnyx.network.mcpe.raknet.RawDataPacket;
import org.projectnyx.network.mcpe.raknet.data.ClientDisconnect;
import org.projectnyx.network.mcpe.raknet.raknet.RakNetPacketManager;
import java.nio.ByteBuffer;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.apache.commons.codec.binary.Hex;
import org.projectnyx.Nyx;
import org.projectnyx.network.Client;
import org.projectnyx.network.Protocol;
import org.projectnyx.network.ext.SessionHandler;
import org.projectnyx.network.ext.udp.UdpSessionHandler;
import org.projectnyx.network.mcpe.mcpe.DataPacket;
/*
 * Nyx - Server software for Minecraft: PE and more
 * Copyright © boredphoton 2016
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.projectnyx.network.mcpe;




@RequiredArgsConstructor
public class McpeClient extends Client {
    public final static int LOGIN_STEP_WAIT_REQUEST_1 = 0;
    public final static int LOGIN_STEP_WAIT_REQUEST_2 = 1;
    public final static int LOGIN_STEP_WAIT_CONNECT = 2;
    public final static int LOGIN_STEP_WAIT_HANDSHAKE = 3;
    public final static int LOGIN_STEP_IN_GAME = 4;

    @Getter @NonNull private final McpeProtocol protocol;
    @Getter private final UdpSessionHandler session;
    @Getter private RakNetInterface rakNetInterface = new RakNetInterface(this);
    @Getter @Setter private int loginStep = LOGIN_STEP_WAIT_REQUEST_1;

    @Override
    public Protocol getSourceProtocol() {
        return protocol;
    }

    @Override
    public SessionHandler getSessionHandler() {
        return session;
    }

    @Override
    public void handlePacket(ByteBuffer buffer) {
        ReceivedPacket packet = RakNetPacketManager.getInstance().parsePacket(buffer);
        if(packet == null) {
            buffer.position(0);
            Nyx.getLog().error(String.format("Cannot handle MCPE packet from client %s - %s",
                    session.getAddress(), Hex.encodeHexString(buffer.array())));
            return;
        }

        rakNetInterface.handlePacket(packet);
    }

    public void handleDataPacket(ByteBuffer buffer) {
        byte pid = buffer.get();
        DataPacket packet = McpeDataPacketManager.getInstance().get(pid, buffer);