package speedytools.serverside.network;
import net.minecraft.util.ChatComponentText;
import net.minecraft.util.IChatComponent;
import net.minecraftforge.fml.common.network.simpleimpl.MessageContext;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import speedytools.common.network.*;
import speedytools.common.network.Packet250CloneToolAcknowledge.Acknowledgement;
import speedytools.common.utilities.ErrorLog;
import speedytools.common.utilities.ResultWithReason;
import speedytools.serverside.PlayerTrackerRegistry;
import speedytools.serverside.ServerSide;
import speedytools.serverside.actions.SpeedyToolServerActions;
import java.util.HashMap;
import java.util.Map;
/**
* User: The Grey Ghost
* Date: 8/03/14
* Used to receive commands from the client and send status messages back. See networkprotocols.txt
* Usage:
* (1) addPlayer when player joins, removePlayer when player leaves
* (2) changeServerStatus to let all interested clients know what the server is doing (busy or not)
* (3) handlePacket should be called to process incoming packets from the client
* (4) in response to an incoming ToolAction, will call SpeedyToolServerActions.performToolAction.
* performToolAction must:
* a) return true/false depending on whether the action is accepted or not
* b) update changeServerStatus
* c) when the action is finished, call actionCompleted and update changeServerStatus
* (5) in response to an incoming Undo, will call SpeedyToolServerActions.performUndoOfLastAction (if the client is undoing
* an action that has been completed and acknowledged), or .performUndoOfCurrentAction (if the client is undoing the
* action currently being performed, i.e. hasn't received a completed acknowledgement yet)
* The SpeedyToolServerActions must:
* a) return true/false depending on whether the undo is accepted or not
* b) update changeServerStatus
* c) when the undo is finished, call undoCompleted. actionCompleted should have been sent first.
* (6) tick() must be called at frequent intervals to check for timeouts - at least once per second
*/
public class SpeedyToolsNetworkServer
{
public SpeedyToolsNetworkServer(PacketHandlerRegistryServer i_packetHandlerRegistry, SpeedyToolServerActions i_speedyToolServerActions, PlayerTrackerRegistry playerTrackerRegistry)
{
packetHandlerRegistry = i_packetHandlerRegistry;
playerStatuses = new HashMap<EntityPlayerMP, ClientStatus>();
playerPacketSenders = new HashMap<EntityPlayerMP, PacketSenderServer>();
lastAcknowledgedAction = new HashMap<EntityPlayerMP, Integer>();
lastAcknowledgedActionPacket = new HashMap<EntityPlayerMP, Packet250CloneToolAcknowledge>();
lastAcknowledgedUndo = new HashMap<EntityPlayerMP, Integer>();
lastAcknowledgedUndoPacket = new HashMap<EntityPlayerMP, Packet250CloneToolAcknowledge>();
lastStatusPacketTimeNS = new HashMap<EntityPlayerMP, Long>();
// undoNotifications = new HashMap<EntityPlayerMP, ArrayList<TimeStampSequenceNumber>>();
speedyToolServerActions = i_speedyToolServerActions;
speedyToolServerActions.setCloneToolsNetworkServer(this);
packetHandlerCloneToolUse = this.new PacketHandlerCloneToolUse();
Packet250CloneToolUse.registerHandler(i_packetHandlerRegistry, packetHandlerCloneToolUse, Side.SERVER);
packetHandlerCloneToolStatus = this.new PacketHandlerCloneToolStatus();
Packet250CloneToolStatus.registerHandler(i_packetHandlerRegistry, packetHandlerCloneToolStatus, Side.SERVER);
packetHandlerSpeedyToolUse = this.new PacketHandlerSpeedyToolUse();
Packet250SpeedyToolUse.registerHandler(i_packetHandlerRegistry, packetHandlerSpeedyToolUse, Side.SERVER);
// no handler, but need to register the discriminator!
Packet250CloneToolAcknowledge.registerHandler(packetHandlerRegistry, null, Side.SERVER);
playerTracker = this.new PlayerTracker();
playerTrackerRegistry.registerHandler(playerTracker);
}
public void addPlayer(EntityPlayerMP newPlayer)
{
if (!playerStatuses.containsKey(newPlayer)) {
playerStatuses.put(newPlayer, ClientStatus.IDLE);
playerPacketSenders.put(newPlayer, new PacketSenderServer(packetHandlerRegistry, newPlayer));
lastAcknowledgedAction.put(newPlayer, Integer.MIN_VALUE);
lastAcknowledgedUndo.put(newPlayer, Integer.MIN_VALUE);
lastStatusPacketTimeNS.put(newPlayer, new Long(0));
}
}
public void removePlayer(EntityPlayerMP whichPlayer)
{
playerStatuses.remove(whichPlayer);
playerPacketSenders.remove(whichPlayer);
lastAcknowledgedAction.remove(whichPlayer);
lastAcknowledgedUndo.remove(whichPlayer);
lastAcknowledgedUndoPacket.remove(whichPlayer);
lastAcknowledgedActionPacket.remove(whichPlayer);
lastStatusPacketTimeNS.remove(whichPlayer);
}
/**
* Changes the server status and informs all clients who are interested in it.
* @param newServerStatus
* @param newServerPercentComplete
*/
public void changeServerStatus(ServerStatus newServerStatus, EntityPlayerMP newPlayerBeingServiced, byte newServerPercentComplete)
{
assert (newServerPercentComplete >= 0 && newServerPercentComplete <= 100);
if (newServerStatus == serverStatus && newPlayerBeingServiced == playerBeingServiced && newServerPercentComplete == serverPercentComplete) {
return;
}
serverStatus = newServerStatus;
serverPercentComplete = newServerPercentComplete;
playerBeingServiced = newPlayerBeingServiced;
for (Map.Entry<EntityPlayerMP,ClientStatus> playerStatus : playerStatuses.entrySet()) {
if (playerStatus.getValue() != ClientStatus.IDLE) {
sendUpdateToClient(playerStatus.getKey());
}
}
}
/** tell the client that the current action has been completed
*
* @param player
* @param actionSequenceNumber
*/
public void actionCompleted(EntityPlayerMP player, int actionSequenceNumber)
{
sendAcknowledgement(player, Acknowledgement.COMPLETED, actionSequenceNumber, Acknowledgement.NOUPDATE, 0);
}
/**tell the client that the current undo has been completed
*
* @param player
* @param undoSequenceNumber
*/
public void undoCompleted(EntityPlayerMP player, int undoSequenceNumber)
{
sendAcknowledgement(player, Acknowledgement.NOUPDATE, 0, Acknowledgement.COMPLETED, undoSequenceNumber);
}
/**
* Send the appropriate update status packet to this player
* @param player
*/
private void sendUpdateToClient(EntityPlayerMP player)
{
ServerStatus serverStatusForThisPlayer = serverStatus;
IChatComponent nameOfOtherPlayerBeingServiced = new ChatComponentText("");
if (player != playerBeingServiced) {
switch (serverStatus) {
case IDLE:
case PERFORMING_BACKUP: {
break;
}
case PERFORMING_YOUR_ACTION:
case UNDOING_YOUR_ACTION: {
serverStatusForThisPlayer = ServerStatus.BUSY_WITH_OTHER_PLAYER;
nameOfOtherPlayerBeingServiced = (playerBeingServiced == null) ? new ChatComponentText("someone") : playerBeingServiced.getDisplayName();
break;
}
default:
assert false: "Invalid serverStatus";
}
}
Packet250CloneToolStatus packet = Packet250CloneToolStatus.serverStatusChange(serverStatusForThisPlayer, serverPercentComplete, nameOfOtherPlayerBeingServiced);
PacketSenderServer packetSenderServer = playerPacketSenders.get(player);
if (packetSenderServer == null) {
ErrorLog.defaultLog().info("sendUpdateToClient tried to send packet to unregistered player:" + player);
} else if (packet.isPacketIsValid()) {
packetSenderServer.sendPacket(packet);
lastStatusPacketTimeNS.put(player, System.nanoTime());
}
}
/**
* sends a packet back to the client acknowledging their Action or undo, with success or failure
* @param player
* @param actionAcknowledgement
* @param actionSequenceNumber
* @param undoAcknowledgement
* @param undoSequenceNumber
* @param reason if the action or undo is rejected, a human-readable message can be provided. "" for none.
*/
private void sendAcknowledgementWithReason(EntityPlayerMP player,
Acknowledgement actionAcknowledgement, int actionSequenceNumber,
Acknowledgement undoAcknowledgement, int undoSequenceNumber,
String reason)
{
sendAcknowledgement_do(player, actionAcknowledgement, actionSequenceNumber, undoAcknowledgement, undoSequenceNumber, reason);
}
/**
* send a packet back to the client acknowledging their Action, with success or failure
* updates private variables to reflect the latest action and/or undo acknowledgments
*/
private void sendAcknowledgement(EntityPlayerMP player,
Acknowledgement actionAcknowledgement, int actionSequenceNumber,
Acknowledgement undoAcknowledgement, int undoSequenceNumber )
{
sendAcknowledgement_do(player, actionAcknowledgement, actionSequenceNumber, undoAcknowledgement, undoSequenceNumber, "");
}
/**
* send a packet back to the client acknowledging their Action, with success or failure
* updates private variables to reflect the latest action and/or undo acknowledgments
*/
private void sendAcknowledgement_do(EntityPlayerMP player,
Acknowledgement actionAcknowledgement, int actionSequenceNumber,
Acknowledgement undoAcknowledgement, int undoSequenceNumber,
String reason)
{
Packet250CloneToolAcknowledge packetAck;
packetAck = new Packet250CloneToolAcknowledge(actionAcknowledgement, actionSequenceNumber,
undoAcknowledgement, undoSequenceNumber, reason);
PacketSenderServer packetSenderServer = playerPacketSenders.get(player);
if (packetSenderServer == null) {
ErrorLog.defaultLog().info("sendUpdateToClient tried to send packet to unregistered player:" + player);
return;
}
if (!packetAck.isPacketIsValid()) {
return;
}
packetSenderServer.sendPacket(packetAck);
// verify that our packets don't contradict anything we have sent earlier
if (actionAcknowledgement != Acknowledgement.NOUPDATE) {
assert (lastAcknowledgedAction.get(player) < actionSequenceNumber ||
(lastAcknowledgedAction.get(player) == actionSequenceNumber
&& actionAcknowledgement == Acknowledgement.COMPLETED
&& lastAcknowledgedActionPacket.get(player).getActionAcknowledgement() == Acknowledgement.ACCEPTED) );
lastAcknowledgedAction.put(player, actionSequenceNumber);
lastAcknowledgedActionPacket.put(player, packetAck);
}
if (undoAcknowledgement != Acknowledgement.NOUPDATE) {
assert (lastAcknowledgedUndo.get(player) < undoSequenceNumber ||
(lastAcknowledgedUndo.get(player) == undoSequenceNumber
&& undoAcknowledgement == Acknowledgement.COMPLETED
&& lastAcknowledgedUndoPacket.get(player).getUndoAcknowledgement() == Acknowledgement.ACCEPTED) );
lastAcknowledgedUndo.put(player, undoSequenceNumber);
lastAcknowledgedUndoPacket.put(player, packetAck);
}
}
/**
* respond to an incoming action packet.
* @param player
* @param packet
*/
public void handlePacket(EntityPlayerMP player, Packet250CloneToolUse packet)
{
// System.out.println("SpeedyToolsNetworkServer.handlePacket:" + packet.getCommand());
switch (packet.getCommand()) {
case PREPARE_FOR_LATER_ACTION: {
speedyToolServerActions.prepareForToolAction(player);
break;
}
// check against previous actions before we implement this action
case PERFORM_TOOL_ACTION: {
int sequenceNumber = packet.getSequenceNumber();
if (sequenceNumber == lastAcknowledgedAction.get(player)) { // same as the action we've already acknowledged; send ack again
Packet250CloneToolAcknowledge packetAck = lastAcknowledgedActionPacket.get(player);
if (packetAck != null) {
PacketSenderServer packetSenderServer = playerPacketSenders.get(player);
if (packetSenderServer != null) {
packetSenderServer.sendPacket(packetAck);
}
}
break;
} else if (sequenceNumber < lastAcknowledgedAction.get(player)) { // old packet, ignore
break; // do nothing, just ignore it
} else {
ResultWithReason result = ResultWithReason.failure();
if (serverStatus == ServerStatus.IDLE) {
result = speedyToolServerActions.performComplexAction(player, sequenceNumber, packet.getToolID(), packet.getBlockWithMetadata(),
packet.getXpos(), packet.getYpos(), packet.getZpos(),
packet.getQuadOrientation(), packet.getSelectionInitialOrigin());
} else {
switch (serverStatus) {
case PERFORMING_BACKUP: {
result = ResultWithReason.failure("Must wait for world backup");
break;
}
case PERFORMING_YOUR_ACTION:
case UNDOING_YOUR_ACTION: {
if (player == playerBeingServiced) {
if (serverStatus == ServerStatus.PERFORMING_YOUR_ACTION) {
result = ResultWithReason.failure("Must wait for your earlier spell to finish");
} else {
result = ResultWithReason.failure("Must wait for your earlier spell to undo");
}
} else {
IChatComponent playerName = new ChatComponentText("someone");
if (playerBeingServiced != null) {
playerName = playerBeingServiced.getDisplayName();
}
result = ResultWithReason.failure("Must wait for " + playerName + " to finish");
}
break;
}
default:
assert false: "Invalid serverStatus";
}
}
sendAcknowledgementWithReason(player, (result.succeeded() ? Acknowledgement.ACCEPTED : Acknowledgement.REJECTED), sequenceNumber, Acknowledgement.NOUPDATE, 0, result.getReason());
}
break;
}
case PERFORM_TOOL_UNDO: {
int sequenceNumber = packet.getSequenceNumber();
if (sequenceNumber == lastAcknowledgedUndo.get(player)) { // if same as last undo sent, just resend again
Packet250CloneToolAcknowledge packetAck = lastAcknowledgedUndoPacket.get(player);
if (packetAck != null) {
PacketSenderServer packetSenderServer = playerPacketSenders.get(player);
if (packetSenderServer != null) {
packetSenderServer.sendPacket(packetAck);
}
}
break;
} else if (sequenceNumber < lastAcknowledgedUndo.get(player)) { // old packet
break; // do nothing, just ignore it
} else {
ResultWithReason result = ResultWithReason.failure();
if (packet.getActionToBeUndoneSequenceNumber() == null) { // undo last completed action
if (serverStatus == ServerStatus.IDLE) {
result = speedyToolServerActions.performUndoOfLastComplexAction(player, packet.getSequenceNumber());
} else {
switch (serverStatus) {
case PERFORMING_BACKUP: {
result = ResultWithReason.failure("Must wait for world backup");
break;
}
case PERFORMING_YOUR_ACTION:
case UNDOING_YOUR_ACTION: {
if (player == playerBeingServiced) {
if (serverStatus == ServerStatus.PERFORMING_YOUR_ACTION) {
result = ResultWithReason.failure("Must wait for your earlier spell to finish");
} else {
result = ResultWithReason.failure("Must wait for your earlier spell to undo");
}
} else {
IChatComponent playerName = new ChatComponentText("someone");
if (playerBeingServiced != null) {
playerName = playerBeingServiced.getDisplayName();
}
result = ResultWithReason.failure("Must wait for " + playerName + " to finish");
}
break;
}
default:
assert false : "Invalid serverStatus";
}
}
sendAcknowledgementWithReason(player, Acknowledgement.NOUPDATE, 0, (result.succeeded() ? Acknowledgement.ACCEPTED : Acknowledgement.REJECTED), sequenceNumber, result.getReason());
break;
} else if (packet.getActionToBeUndoneSequenceNumber() > lastAcknowledgedAction.get(player) ) { // undo for an action we haven't received yet
sendAcknowledgementWithReason(player, Acknowledgement.REJECTED, packet.getActionToBeUndoneSequenceNumber(),
Acknowledgement.COMPLETED, packet.getSequenceNumber(),
"");
break;
} else if (packet.getActionToBeUndoneSequenceNumber().equals(lastAcknowledgedAction.get(player))) { // undo for a specific action we have acknowledged as starting
result = speedyToolServerActions.performUndoOfCurrentComplexAction(player, packet.getSequenceNumber(), packet.getActionToBeUndoneSequenceNumber());
// sendAcknowledgementWithReason(player, Acknowledgement.NOUPDATE, 0, (result.succeeded() ? Acknowledgement.ACCEPTED : Acknowledgement.REJECTED), sequenceNumber, result.getReason());
sendAcknowledgementWithReason(player, Acknowledgement.COMPLETED, lastAcknowledgedAction.get(player),
(result.succeeded() ? Acknowledgement.ACCEPTED : Acknowledgement.REJECTED), sequenceNumber, result.getReason());
}
}
break;
}
default: {
assert false: "Invalid server side packet";
}
}
}
/**
* update the status of the appropriate client; replies with the server status if the client is interested
* @param player
* @param packet
*/
public void handlePacket(EntityPlayerMP player, Packet250CloneToolStatus packet)
{
ClientStatus newStatus = packet.getClientStatus();
if (!playerStatuses.containsKey(player)) {
ErrorLog.defaultLog().info("SpeedyToolsNetworkServer:: Packet received from player not in playerStatuses");
return;
}
playerStatuses.put(player, newStatus);
if (newStatus != ClientStatus.IDLE) {
sendUpdateToClient(player);
}
}
/**
* sends periodic status updates to the clients who have registered an interest.
*/
public void tick()
{
long thresholdTime = System.nanoTime() - STATUS_UPDATE_WAIT_TIME_MS * 1000 * 1000;
for (Map.Entry<EntityPlayerMP, ClientStatus> clientStatus : playerStatuses.entrySet()) {
ServerSide.getInGameStatusSimulator().setTestMode(clientStatus.getKey()); // for in-game testing purposes
if (ServerSide.getInGameStatusSimulator().isTestModeActivated()) {
serverStatus = ServerSide.getInGameStatusSimulator().getForcedStatus(serverStatus, Side.CLIENT);
playerBeingServiced = ServerSide.getInGameStatusSimulator().getForcedPlayerBeingServiced(playerBeingServiced, clientStatus.getKey(), Side.CLIENT);
serverPercentComplete = ServerSide.getInGameStatusSimulator().getForcedPercentComplete(serverPercentComplete, Side.CLIENT);
}
if (clientStatus.getValue() != ClientStatus.IDLE) {
if (lastStatusPacketTimeNS.get(clientStatus.getKey()) < thresholdTime ) {
sendUpdateToClient(clientStatus.getKey());
}
}
if (ServerSide.getInGameStatusSimulator().isTestModeActivated()) {
serverStatus = ServerSide.getInGameStatusSimulator().getForcedStatus(serverStatus, Side.SERVER);
playerBeingServiced = ServerSide.getInGameStatusSimulator().getForcedPlayerBeingServiced(playerBeingServiced, clientStatus.getKey(), Side.SERVER);
serverPercentComplete = ServerSide.getInGameStatusSimulator().getForcedPercentComplete(serverPercentComplete, Side.SERVER);
}
}
}
public class PacketHandlerCloneToolUse implements Packet250CloneToolUse.PacketHandlerMethod {
@Override
public boolean handlePacket(Packet250CloneToolUse packet250CloneToolUse, MessageContext ctx) {
if (!packet250CloneToolUse.isPacketIsValid()) return false;
if (!packet250CloneToolUse.validForSide(Side.SERVER)) return false;
SpeedyToolsNetworkServer.this.handlePacket(ctx.getServerHandler().playerEntity, packet250CloneToolUse);
return true;
}
}
public class PacketHandlerCloneToolStatus implements Packet250CloneToolStatus.PacketHandlerMethod {
@Override
public boolean handlePacket(Packet250CloneToolStatus packet250CloneToolStatus, MessageContext ctx) {
if (!packet250CloneToolStatus.isPacketIsValid()) return false;
if (!packet250CloneToolStatus.validForSide(Side.SERVER)) return false;
SpeedyToolsNetworkServer.this.handlePacket(ctx.getServerHandler().playerEntity, packet250CloneToolStatus);
return true;
}
}
public class PacketHandlerSpeedyToolUse implements Packet250SpeedyToolUse.PacketHandlerMethod{
@Override
public boolean handlePacket(Packet250SpeedyToolUse packet250SpeedyToolUse, MessageContext ctx) {
if (!packet250SpeedyToolUse.isPacketIsValid()) return false;
SpeedyToolsNetworkServer.this.speedyToolServerActions.performSimpleAction(ctx.getServerHandler().playerEntity,
packet250SpeedyToolUse.getButton(),
packet250SpeedyToolUse.getBlockToPlace(), packet250SpeedyToolUse.getSideToPlace(), packet250SpeedyToolUse.getCurrentlySelectedBlocks());
return true;
}
}
private PacketHandlerSpeedyToolUse packetHandlerSpeedyToolUse;
private class PlayerTracker implements PlayerTrackerRegistry.IPlayerTracker
{
public void onPlayerLogin(EntityPlayer player)
{
EntityPlayerMP entityPlayerMP = (EntityPlayerMP)player;
SpeedyToolsNetworkServer.this.addPlayer(entityPlayerMP);
}
public void onPlayerLogout(EntityPlayer player)
{
EntityPlayerMP entityPlayerMP = (EntityPlayerMP)player;
SpeedyToolsNetworkServer.this.removePlayer(entityPlayerMP);
}
public void onPlayerChangedDimension(EntityPlayer player) {}
public void onPlayerRespawn(EntityPlayer player) {}
}
private PlayerTracker playerTracker;
private Map<EntityPlayerMP, ClientStatus> playerStatuses;
private Map<EntityPlayerMP, PacketSenderServer> playerPacketSenders;
private Map<EntityPlayerMP, Integer> lastAcknowledgedAction;
private Map<EntityPlayerMP, Packet250CloneToolAcknowledge> lastAcknowledgedActionPacket;
private Map<EntityPlayerMP, Integer> lastAcknowledgedUndo;
private Map<EntityPlayerMP, Packet250CloneToolAcknowledge> lastAcknowledgedUndoPacket;
private Map<EntityPlayerMP, Long> lastStatusPacketTimeNS;
// private class TimeStampSequenceNumber {
// public long timestamp;
// public int sequenceNumber;
// }
// private Map<EntityPlayerMP, ArrayList<TimeStampSequenceNumber>> undoNotifications;
private static final int STATUS_UPDATE_WAIT_TIME_MS = 1000; // how often to send a status update
private ServerStatus serverStatus = ServerStatus.IDLE;
private byte serverPercentComplete = 0;
private SpeedyToolServerActions speedyToolServerActions;
private EntityPlayerMP playerBeingServiced;
private PacketHandlerCloneToolUse packetHandlerCloneToolUse;
private PacketHandlerCloneToolStatus packetHandlerCloneToolStatus;
PacketHandlerRegistryServer packetHandlerRegistry;
}