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
}
}