package com.asteria.net.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; import java.util.List; import java.util.Optional; import java.util.logging.Logger; import com.asteria.Server; import com.asteria.net.ISAACCipher; import com.asteria.net.NetworkConstants; import com.asteria.net.PlayerIO; import com.asteria.net.message.InputMessage; import com.asteria.net.message.InputMessageListener; import com.asteria.net.message.MessageBuilder; import com.asteria.utility.LoggerUtils; /** * The {@link ByteToMessageDecoder} implementation that decodes and queues the * game logic for all incoming {@link InputMessage}s. * * @author lare96 <http://github.com/lare96> */ public final class MessageDecoder extends ByteToMessageDecoder { /** * The logger that will print important information. */ private static Logger logger = LoggerUtils.getLogger(MessageDecoder.class); /** * The ISAAC that will decrypt incoming messages. */ private final ISAACCipher decryptor; /** * The state of the message being decoded. */ private State state = State.OPCODE; /** * The opcode of the message being decoded. */ private int opcode; /** * The size of the message being decoded. */ private int size; /** * Creates a new {@link MessageDecoder}. * * @param decryptor * the ISAAC decryptor that decodes data. */ public MessageDecoder(ISAACCipher decryptor) { this.decryptor = decryptor; } @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { switch (state) { case OPCODE: opcode(ctx, in).ifPresent(out::add); break; case SIZE: size(in); break; case PAYLOAD: payload(ctx, in).ifPresent(out::add); break; } } /** * Decode the message opcode and determine the message type. If the message * is variable sized set the next state to {@code SIZE}, otherwise set it to * {@code PAYLOAD}. * * @param ctx * the context for our channel, used to retrieve the session * instance. * @param msg * the message to decode the opcode from. * @return an optional containing a message with no payload, or an empty * optional. */ private Optional<InputMessage> opcode(ChannelHandlerContext ctx, ByteBuf msg) { if (msg.isReadable()) { opcode = msg.readUnsignedByte(); opcode = (opcode - decryptor.getKey()) & 0xFF; size = NetworkConstants.MESSAGE_SIZES[opcode]; if (size == 0) return message(ctx, Unpooled.EMPTY_BUFFER); state = size == NetworkConstants.VAR_SIZE || size == NetworkConstants.VAR_SIZE_SHORT ? State.SIZE : State.PAYLOAD; } return Optional.empty(); } /** * Decode the message size for variable sized messages, then set the state * to {@code PAYLOAD}. * * @param msg * the message to decode the size from. */ private void size(ByteBuf msg) { int bytes = size == NetworkConstants.VAR_SIZE ? Byte.BYTES : Short.BYTES; if (msg.isReadable(bytes)) { size = 0; for (int i = 0; i < bytes; i++) size |= msg.readUnsignedByte() << 8 * (bytes - 1 - i); state = State.PAYLOAD; } } /** * Decode the payload for this message, then queue it over to be received * upstream by the Netty channel handler. * * @param ctx * the context for our channel, used to retrieve the session * instance. * @param msg * the message to decode the payload from. * @return an optional containing the successfully decoded */ private Optional<InputMessage> payload(ChannelHandlerContext ctx, ByteBuf msg) { if (msg.isReadable(size)) return message(ctx, msg.readBytes(size)); return Optional.empty(); } /** * Determines if an {@link InputMessageListener} is available for the * current opcode, if it is it returns a new {@code InputMessage} wrapped in * an optional, if not it returns an empty optional. Before this method * returns, the state is reset to {@code OPCODE} and the opcode and size * values are reset to {@code -1}. * * * @param ctx * the context for our channel, used to retrieve the session * instance. * @param payload * the payload to pack in this message. * @return an optional containing the message if available, an empty * optional otherwise. */ private Optional<InputMessage> message(ChannelHandlerContext ctx, ByteBuf payload) { try { if (NetworkConstants.MESSAGES[opcode] != null) return Optional.of(new InputMessage(opcode, size, MessageBuilder.create(payload))); if (Server.DEBUG) { PlayerIO session = ctx.channel().attr(NetworkConstants.SESSION_KEY).get(); logger.info(session + " unhandled upstream message [opcode= " + opcode + ", size= " + size + "]"); } } finally { state = State.OPCODE; opcode = -1; size = -1; } return Optional.empty(); } /** * The enumerated type representing all of the possible states of this * decoder. * * @author lare96 <http://github.org/lare96> */ private enum State { OPCODE, SIZE, PAYLOAD } }