package com.asteria.net;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.socket.SocketChannel;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import com.asteria.game.World;
import com.asteria.game.character.player.IOState;
import com.asteria.game.character.player.Player;
import com.asteria.game.character.player.Rights;
import com.asteria.game.character.player.serialize.PlayerSerialization;
import com.asteria.net.codec.MessageDecoder;
import com.asteria.net.codec.MessageEncoder;
import com.asteria.net.login.LoginResponse;
import com.asteria.net.message.InputMessage;
import com.asteria.net.message.InputMessageListener;
import com.asteria.net.message.LoginDetailsMessage;
import com.asteria.net.message.Message;
import com.asteria.net.message.MessageBuilder;
import com.asteria.utility.TextUtils;
/**
* The session handler dedicated to a player that will handle input and output
* operations.
*
* @author lare96 <http://github.com/lare96>
* @author blakeman8192
*/
public final class PlayerIO {
/**
* The queue of messages that will be handled on the next sequence.
*/
private final Queue<InputMessage> messageQueue = new ConcurrentLinkedQueue<>();
/**
* The channel that will manage the connection for this player.
*/
private final Channel channel;
/**
* The player I/O operations will be executed for.
*/
private final Player player;
/**
* The host address this session is bound to.
*/
private final String host;
/**
* The current state of this I/O session.
*/
private IOState state = IOState.CONNECTED;
/**
* The current login response for this session.
*/
private LoginResponse response;
/**
* Creates a new {@link PlayerIO}.
*
* @param channel
* the socket channel that data will be written to.
*/
public PlayerIO(SocketChannel channel) {
this.host = channel.remoteAddress().getAddress().getHostAddress();
this.response = ConnectionHandler.evaluate(host);
this.channel = channel;
this.player = new Player(this);
}
@Override
public String toString() {
return state == IOState.LOGGED_IN ? player.toString() : "SESSION[host= " + host + ", state= " + state + "]";
}
/**
* Queues the {@code msg} for this session to be encoded and sent to the
* client.
*
* @param msg
* the message to queue.
*/
public void queue(MessageBuilder msg) {
try {
if (!channel.isOpen())
return;
channel.writeAndFlush(msg);
} catch (Exception ex) {
ex.printStackTrace();
channel.close();
}
}
/**
* Uses state-machine to handle upstream messages from Netty.
*
* @param msg
* the message to handle.
*/
public void handleIncomingMessage(Message msg) {
switch (state) {
// Handle the login details, send the final response to the client
// before queuing the session over to the main game thread to be logged
// in on the next sequence.
case LOGGING_IN:
if (msg instanceof LoginDetailsMessage)
finalizeDetails((LoginDetailsMessage) msg);
break;
// We are already logged in, handle incoming messages from the client by
// queuing them over to the main game thread to be processed on the next
// sequence.
case LOGGED_IN:
if (msg instanceof InputMessage) {
if (messageQueue.size() <= NetworkConstants.DECODE_LIMIT)
messageQueue.add((InputMessage) msg);
}
break;
default:
throw new IllegalStateException("Cannot receive upstream messages when " + state + ".");
}
}
/**
* Ensures that the login details are valid and completes the last part of
* the login protocol by sending the final login response code.
*
* @param msg
* the message containing the login details.
*/
private void finalizeDetails(LoginDetailsMessage msg) {
// Validate the username and password, change login response if needed
// for invalid credentials or the world being full.
boolean invalidCredentials = !msg.getUsername().matches("^[a-zA-Z0-9_ ]{1," + "12}$") || msg.getPassword().isEmpty() || msg
.getPassword().length() > 20;
response = invalidCredentials ? LoginResponse.INVALID_CREDENTIALS : World.getPlayers().spaceLeft() == 0 ? LoginResponse.WORLD_FULL
: response;
// If the login response is normal, deserialize the character file (or
// grab it from the Cache if it was recently serialized).
if (response == LoginResponse.NORMAL) {
player.setUsername(msg.getUsername());
player.setUsernameHash(TextUtils.nameToHash(msg.getUsername()));
player.setPassword(msg.getPassword());
if (World.getPlayer(player.getUsernameHash()).isPresent()) {
response = LoginResponse.ACCOUNT_ONLINE;
}
if (response == LoginResponse.NORMAL) {
response = new PlayerSerialization(player).deserialize(msg.getPassword());
}
player.setRights(ConnectionHandler.isLocal(host) ? Rights.DEVELOPER : player.getRights());
}
// Write the final response, send it off to the client.
ByteBuf resp = Unpooled.buffer(3);
resp.writeByte(response.getCode());
resp.writeByte(player.getRights().getProtocolValue());
resp.writeByte(0);
// If the response was invalid, close the channel right after the data
// is sent to the client.
ChannelFuture future = msg.getCtx().channel().writeAndFlush(resp);
if (response != LoginResponse.NORMAL) {
future.addListener(ChannelFutureListener.CLOSE);
return;
}
// Everything went well, so queue rearrange the pipeline for gameplay
// and queue the player for login.
msg.getCtx().pipeline().addAfter("post-login-handshake", "encoder", new MessageEncoder(msg.getEncryptor()));
msg.getCtx().pipeline().addAfter("encoder", "decoder", new MessageDecoder(msg.getDecryptor()));
msg.getCtx().pipeline().remove("post-login-handshake");
World.queueLogin(player);
}
/**
* Handles all of the queued messages from the {@link MessageDecoder} by
* polling the internal queue.
*/
public void handleQueuedMessages() {
InputMessage msg;
while ((msg = messageQueue.poll()) != null) {
try {
InputMessageListener listener = NetworkConstants.MESSAGES[msg.getOpcode()];
listener.handleMessage(player, msg.getOpcode(), msg.getSize(), msg.getPayload());
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* Gets the channel that will manage the connection for this player.
*
* @return the channel for this player.
*/
public Channel getChannel() {
return channel;
}
/**
* Gets the player I/O operations will be executed for.
*
* @return the player I/O operations.
*/
public Player getPlayer() {
return player;
}
/**
* Gets the host address this session is bound to.
*
* @return the host address.
*/
public String getHost() {
return host;
}
/**
* Gets the current state of this I/O session.
*
* @return the current state.
*/
public IOState getState() {
return state;
}
/**
* Sets the value for {@link PlayerIO#state}.
*
* @param state
* the new value to set.
*/
public void setState(IOState state) {
this.state = state;
}
/**
* Gets the current login response for this session.
*
* @return the current login response.
*/
public LoginResponse getResponse() {
return response;
}
/**
* Sets the value for {@link PlayerIO#response}.
*
* @param response
* the new value to set.
*/
public void setResponse(LoginResponse response) {
this.response = response;
}
}