001/*
002 * Trident - A Multithreaded Server Alternative
003 * Copyright 2014 The TridentSDK Team
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package net.tridentsdk.server.netty.packet;
019
020import io.netty.buffer.ByteBuf;
021import io.netty.channel.ChannelHandlerContext;
022import io.netty.handler.codec.MessageToByteEncoder;
023import net.tridentsdk.server.TridentServer;
024import net.tridentsdk.server.netty.ClientConnection;
025import net.tridentsdk.server.netty.Codec;
026
027import javax.annotation.concurrent.NotThreadSafe;
028import java.io.ByteArrayOutputStream;
029import java.io.IOException;
030import java.util.zip.Deflater;
031
032/**
033 * Used to compress (if needed) outgoing packets from the server
034 * <p>Note that this is not thread safe, if it is to be used in multiple threads, multiple instances should be
035 * created</p>
036 * <p>This is the second and final in the outbound packet pipeline</p>
037 *
038 * @author The TridentSDK Team
039 */
040@NotThreadSafe
041public class PacketEncoder extends MessageToByteEncoder<ByteBuf> {
042    private final Deflater deflater = new Deflater(Deflater.BEST_SPEED);
043    private final byte[] buffer = new byte[65536];
044    private final ByteArrayOutputStream compressed = new ByteArrayOutputStream();
045
046    private ClientConnection connection;
047
048    @Override
049    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
050        connection = ClientConnection.connection(ctx);
051    }
052
053    @Override
054    protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf msg, ByteBuf out) throws Exception {
055        int threshold = TridentServer.instance().compressionThreshold();
056        boolean underThreshold = msg.readableBytes() < threshold && threshold != -1;
057
058        if (underThreshold && connection.isCompressionEnabled()) {
059            sendDecompressed(msg, out);
060        } else if (!(underThreshold) && connection.isCompressionEnabled()) {
061            sendCompressed(msg, out);
062        } else {
063            Codec.writeVarInt32(out, msg.readableBytes());
064            out.writeBytes(msg);
065        }
066    }
067
068    /**
069     * Encodes the packet without checking for size to see if it should be compressed
070     * <p>Still sends a VarInt 0 to indicate that this packet has not been compressed</p>
071     * <p>This method of handling a packet is abnormal and is only used when compression is disabled</p>
072     * FIXME compression needs to be enabled in order to use this method
073     */
074    private void sendDecompressed(ByteBuf msg, ByteBuf out) {
075        Codec.writeVarInt32(out, msg.readableBytes() + Codec.sizeOf(0));
076        Codec.writeVarInt32(out, 0);
077        out.writeBytes(msg);
078    }
079
080    /**
081     * Checks a packets size and encodes (writes the size, compressed size, and data) and compressed the information if
082     * necessary
083     */
084    private void sendCompressed(ByteBuf msg, ByteBuf out) throws IOException {
085        int index = msg.readerIndex();
086        int length = msg.readableBytes();
087
088        byte[] decompressed = new byte[length];
089
090        msg.readBytes(decompressed);
091        deflater.setInput(decompressed);
092        deflater.finish();
093
094        while (!deflater.finished()) {
095            int bytes = deflater.deflate(buffer);
096            compressed.write(buffer, 0, bytes);
097        }
098
099        int afterCompress = compressed.toByteArray().length;
100
101        // Equals or more than original size
102        if (afterCompress == length || afterCompress > length) {
103            msg.readerIndex(index);
104            sendDecompressed(msg, out);
105            return;
106        }
107
108        deflater.reset();
109
110        Codec.writeVarInt32(out, afterCompress + Codec.sizeOf(length));
111        Codec.writeVarInt32(out, length);
112        out.writeBytes(compressed.toByteArray());
113
114        compressed.reset();
115    }
116}