/*******************************************************************************
* Copyright (c) 2015
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*******************************************************************************/
package jsettlers.network.client;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.Timer;
import jsettlers.network.NetworkConstants;
import jsettlers.network.NetworkConstants.ENetworkKey;
import jsettlers.network.client.interfaces.IGameClock;
import jsettlers.network.client.interfaces.INetworkClient;
import jsettlers.network.client.interfaces.INetworkConnector;
import jsettlers.network.client.interfaces.ITaskScheduler;
import jsettlers.network.client.receiver.IPacketReceiver;
import jsettlers.network.client.task.TaskPacketListener;
import jsettlers.network.client.task.packets.TaskPacket;
import jsettlers.network.client.time.ISynchronizableClock;
import jsettlers.network.client.time.TimeSyncSenderTimerTask;
import jsettlers.network.client.time.TimeSynchronizationListener;
import jsettlers.network.common.packets.ArrayOfMatchInfosPacket;
import jsettlers.network.common.packets.BooleanMessagePacket;
import jsettlers.network.common.packets.ChatMessagePacket;
import jsettlers.network.common.packets.IdPacket;
import jsettlers.network.common.packets.MapInfoPacket;
import jsettlers.network.common.packets.MatchInfoPacket;
import jsettlers.network.common.packets.MatchInfoUpdatePacket;
import jsettlers.network.common.packets.MatchStartPacket;
import jsettlers.network.common.packets.OpenNewMatchPacket;
import jsettlers.network.common.packets.PlayerInfoPacket;
import jsettlers.network.infrastructure.channel.AsyncChannel;
import jsettlers.network.infrastructure.channel.GenericDeserializer;
import jsettlers.network.infrastructure.channel.IChannelClosedListener;
import jsettlers.network.infrastructure.channel.packet.EmptyPacket;
import jsettlers.network.infrastructure.channel.packet.Packet;
import jsettlers.network.infrastructure.channel.reject.RejectPacket;
import jsettlers.network.server.match.EPlayerState;
import jsettlers.network.synchronic.timer.NetworkTimer;
/**
* The {@link NetworkClient} class offers an interface to the servers methods. All methods of the {@link NetworkClient} class will never block. All
* calls to the server are done by an asynchronous Thread.
*
* @author Andreas Eberle
*
*/
public class NetworkClient implements ITaskScheduler, INetworkConnector, INetworkClient {
private final AsyncChannel channel;
private final Timer timer;
private final INetworkClientClock clock;
private EPlayerState state = EPlayerState.CHANNEL_CONNECTED;
private PlayerInfoPacket playerInfo;
private MatchInfoPacket matchInfo;
/**
*
* @param channel
* @param channelClosedListener
* The listener to be called when the channel is closed<br>
* or null, if no listener should be registered.
*/
public NetworkClient(AsyncChannel channel, IChannelClosedListener channelClosedListener) {
this(channel, channelClosedListener, new NetworkTimer());
}
public NetworkClient(String serverAddress, IChannelClosedListener channelClosedListener) throws UnknownHostException, IOException {
this(new AsyncChannel(serverAddress, NetworkConstants.Server.SERVER_PORT), channelClosedListener);
}
NetworkClient(AsyncChannel channel, final IChannelClosedListener channelClosedListener, INetworkClientClock gameClock) {
this.channel = channel;
channel.setChannelClosedListener(new IChannelClosedListener() {
@Override
public void channelClosed() {
close();
if (channelClosedListener != null)
channelClosedListener.channelClosed();
}
});
this.timer = new Timer("NetworkClientTimer");
this.clock = gameClock;
if (!channel.isStarted()) {
channel.start();
}
}
@Override
public void logIn(String id, String name, IPacketReceiver<ArrayOfMatchInfosPacket> matchesReceiver) throws IllegalStateException {
EPlayerState.assertState(state, EPlayerState.CHANNEL_CONNECTED);
playerInfo = new PlayerInfoPacket(id, name, false);
channel.registerListener(new IdentifiedUserListener(this));
channel.registerListener(generateDefaultListener(NetworkConstants.ENetworkKey.ARRAY_OF_MATCHES, ArrayOfMatchInfosPacket.class,
matchesReceiver));
channel.sendPacketAsync(NetworkConstants.ENetworkKey.IDENTIFY_USER, playerInfo);
}
/**
*
* @param matchName
* @param maxPlayers
* @param mapInfo
* @param matchStartedListener
* @param matchInfoUpdatedListener
* This listener will receive all further updates on the match.
* @param chatMessageReceiver
* @param taskScheduler
* @throws InvalidStateException
*/
@Override
public void openNewMatch(String matchName, int maxPlayers, MapInfoPacket mapInfo, long randomSeed,
IPacketReceiver<MatchStartPacket> matchStartedListener,
IPacketReceiver<MatchInfoUpdatePacket> matchInfoUpdatedListener, IPacketReceiver<ChatMessagePacket> chatMessageReceiver)
throws IllegalStateException {
EPlayerState.assertState(state, EPlayerState.LOGGED_IN);
registerMatchStartListeners(matchStartedListener, matchInfoUpdatedListener, chatMessageReceiver);
channel.sendPacketAsync(NetworkConstants.ENetworkKey.REQUEST_OPEN_NEW_MATCH, new OpenNewMatchPacket(matchName, maxPlayers, mapInfo,
randomSeed));
}
@Override
public void joinMatch(String matchId, IPacketReceiver<MatchStartPacket> matchStartedListener,
IPacketReceiver<MatchInfoUpdatePacket> matchInfoUpdatedListener, IPacketReceiver<ChatMessagePacket> chatMessageReceiver)
throws IllegalStateException {
EPlayerState.assertState(state, EPlayerState.LOGGED_IN);
registerMatchStartListeners(matchStartedListener, matchInfoUpdatedListener, chatMessageReceiver);
channel.sendPacketAsync(NetworkConstants.ENetworkKey.REQUEST_JOIN_MATCH, new IdPacket(matchId));
}
@Override
public void leaveMatch() {
channel.sendPacketAsync(NetworkConstants.ENetworkKey.REQUEST_LEAVE_MATCH, new EmptyPacket());
}
@Override
public void startMatch() throws IllegalStateException {
EPlayerState.assertState(state, EPlayerState.IN_MATCH);
channel.sendPacketAsync(NetworkConstants.ENetworkKey.REQUEST_START_MATCH, new EmptyPacket());
}
@Override
public void setReadyState(boolean ready) throws IllegalStateException {
EPlayerState.assertState(state, EPlayerState.IN_MATCH);
channel.sendPacketAsync(NetworkConstants.ENetworkKey.CHANGE_READY_STATE, new BooleanMessagePacket(ready));
}
@Override
public void setStartFinished(boolean startFinished) throws IllegalStateException {
EPlayerState.assertState(state, EPlayerState.IN_RUNNING_MATCH);
channel.sendPacketAsync(NetworkConstants.ENetworkKey.CHANGE_START_FINISHED, new BooleanMessagePacket(startFinished));
}
@Override
public void sendChatMessage(String message) throws IllegalStateException {
EPlayerState.assertState(state, EPlayerState.IN_MATCH, EPlayerState.IN_RUNNING_MATCH);
channel.sendPacketAsync(NetworkConstants.ENetworkKey.CHAT_MESSAGE, new ChatMessagePacket(playerInfo.getId(), message));
}
@Override
public void registerRejectReceiver(IPacketReceiver<RejectPacket> rejectListener) {
channel.registerListener(generateDefaultListener(NetworkConstants.ENetworkKey.REJECT_PACKET, RejectPacket.class, rejectListener));
}
private void registerMatchStartListeners(IPacketReceiver<MatchStartPacket> matchStartedListener,
IPacketReceiver<MatchInfoUpdatePacket> matchInfoUpdatedListener, IPacketReceiver<ChatMessagePacket> chatMessageReceiver) {
channel.registerListener(new MatchInfoUpdatedListener(this, matchInfoUpdatedListener));
channel.registerListener(new MatchStartedListener(this, matchStartedListener));
channel.registerListener(generateDefaultListener(ENetworkKey.CHAT_MESSAGE, ChatMessagePacket.class, chatMessageReceiver));
channel.registerListener(new TaskPacketListener(clock));
}
@Override
public void scheduleTask(TaskPacket task) {
channel.sendPacketAsync(NetworkConstants.ENetworkKey.SYNCHRONOUS_TASK, task);
}
private <T extends Packet> DefaultClientPacketListener<T> generateDefaultListener(ENetworkKey key, Class<T> classType,
IPacketReceiver<T> listener) {
return new DefaultClientPacketListener<T>(key, new GenericDeserializer<T>(classType), listener);
}
@Override
public EPlayerState getState() {
return state;
}
@Override
public void close() {
state = EPlayerState.DISCONNECTED;
timer.cancel();
channel.close();
clock.stopExecution();
}
void identifiedUserEvent() {
this.state = EPlayerState.LOGGED_IN;
channel.removeListener(NetworkConstants.ENetworkKey.IDENTIFY_USER);
}
private void playerJoinedEvent(MatchInfoPacket matchInfo) {
if (this.matchInfo == null) { // only if we joined.
this.state = EPlayerState.IN_MATCH;
this.matchInfo = matchInfo;
}
}
private void playerLeftEvent(MatchInfoUpdatePacket matchInfoUpdate) {
MatchInfoPacket updatedInfo = matchInfoUpdate.getMatchInfo();
assert updatedInfo != null && updatedInfo.getId().equals(updatedInfo.getId()) : "received match info for wrong match! " + updatedInfo;
if (playerInfo.getId().equals(matchInfoUpdate.getUpdatedPlayer().getId())) { // if this client left the game
state = EPlayerState.LOGGED_IN;
this.matchInfo = null;
channel.removeListener(NetworkConstants.ENetworkKey.MATCH_INFO_UPDATE);
channel.removeListener(NetworkConstants.ENetworkKey.CHAT_MESSAGE);
} else {
this.matchInfo = updatedInfo;
}
}
void matchStartedEvent() {
this.state = EPlayerState.IN_RUNNING_MATCH;
channel.removeListener(NetworkConstants.ENetworkKey.MATCH_STARTED);
startTimeSynchronization(clock);
channel.initPinging();
}
private void startTimeSynchronization(ISynchronizableClock clock) {
channel.registerListener(new TimeSynchronizationListener(channel, clock));
TimeSyncSenderTimerTask timeSyncSender = new TimeSyncSenderTimerTask(channel, clock);
timer.schedule(timeSyncSender, 0, NetworkConstants.Client.TIME_SYNC_SEND_INTERVALL);
}
void matchInfoUpdated(MatchInfoUpdatePacket matchInfoUpdate) {
switch (matchInfoUpdate.getUpdateReason()) {
case PLAYER_LEFT:
playerLeftEvent(matchInfoUpdate);
return; // this prevents that the matchInfo is set
case PLAYER_JOINED:
playerJoinedEvent(matchInfoUpdate.getMatchInfo());
break;
default:
break;
}
matchInfo = matchInfoUpdate.getMatchInfo();
}
/**
* @return the playerInfo
*/
@Override
public PlayerInfoPacket getPlayerInfo() {
return playerInfo;
}
/**
* @return the matchInfo
*/
@Override
public MatchInfoPacket getMatchInfo() {
return matchInfo;
}
@Override
public IGameClock getGameClock() {
return clock;
}
@Override
public int getRoundTripTimeInMs() {
return channel.getRoundTripTime().getRtt();
}
@Override
public void shutdown() {
close();
}
@Override
public ITaskScheduler getTaskScheduler() {
return this;
}
@Override
public boolean haveAllPlayersStartFinished() {
boolean allStartFinished = true;
for (PlayerInfoPacket currPlayer : matchInfo.getPlayers()) {
allStartFinished = allStartFinished && currPlayer.isStartFinished();
}
return allStartFinished;
}
@Override
public INetworkConnector getNetworkConnector() {
return this;
}
}