// --------------------------------------------------------------------------------------------------
//  Copyright (c) 2016 Microsoft Corporation
//  
//  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 com.microsoft.Malmo.Client;

import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import java.util.Map.Entry;
import java.util.logging.Level;

import javax.xml.bind.JAXBException;
import javax.xml.stream.XMLStreamException;

import com.microsoft.Malmo.Schemas.*;
import net.minecraft.block.Block;
import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.EntityPlayerSP;
import net.minecraft.client.gui.GuiDisconnected;
import net.minecraft.client.gui.GuiIngameMenu;
import net.minecraft.client.gui.GuiMainMenu;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.multiplayer.ThreadLanServerPing;
import net.minecraft.client.multiplayer.WorldClient;
import net.minecraft.client.network.NetHandlerPlayClient;
import net.minecraft.client.settings.GameSettings;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.init.Blocks;
import net.minecraft.init.Items;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.crafting.FurnaceRecipes;
import net.minecraft.launchwrapper.Launch;
import net.minecraft.network.NetworkManager;
import net.minecraft.server.integrated.IntegratedServer;
import net.minecraft.stats.StatisticsManagerServer;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.text.TextComponentString;
import net.minecraft.world.GameType;
import net.minecraft.world.World;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.IChunkProvider;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.config.Configuration;
import net.minecraftforge.fml.client.event.ConfigChangedEvent.OnConfigChangedEvent;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.Loader;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent.ClientTickEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent.Phase;
import net.minecraftforge.fml.common.gameevent.TickEvent.ServerTickEvent;

import org.xml.sax.SAXException;

import com.google.gson.JsonObject;
import com.microsoft.Malmo.IState;
import com.microsoft.Malmo.MalmoMod;
import com.microsoft.Malmo.MalmoMod.IMalmoMessageListener;
import com.microsoft.Malmo.MalmoMod.MalmoMessageType;
import com.microsoft.Malmo.StateEpisode;
import com.microsoft.Malmo.StateMachine;
import com.microsoft.Malmo.Client.MalmoModClient.InputType;
import com.microsoft.Malmo.MissionHandlerInterfaces.IVideoProducer;
import com.microsoft.Malmo.MissionHandlerInterfaces.IWantToQuit;
import com.microsoft.Malmo.MissionHandlers.MissionBehaviour;
import com.microsoft.Malmo.MissionHandlers.MultidimensionalReward;
import com.microsoft.Malmo.Schemas.ServerSection.HumanInteraction;
import com.microsoft.Malmo.Utils.AddressHelper;
import com.microsoft.Malmo.Utils.AuthenticationHelper;
import com.microsoft.Malmo.Utils.SchemaHelper;
import com.microsoft.Malmo.Utils.ScreenHelper;
import com.microsoft.Malmo.Utils.SeedHelper;
import com.microsoft.Malmo.Utils.ScoreHelper;
import com.microsoft.Malmo.Utils.TextureHelper;
import com.microsoft.Malmo.Utils.ScreenHelper.TextCategory;
import com.microsoft.Malmo.Utils.TCPInputPoller;
import com.microsoft.Malmo.Utils.TCPInputPoller.CommandAndIPAddress;
import com.microsoft.Malmo.Utils.TimeHelper.SyncTickEvent;
import com.microsoft.Malmo.Utils.TCPSocketChannel;
import com.microsoft.Malmo.Utils.TCPUtils;
import com.microsoft.Malmo.Utils.TimeHelper;
import com.mojang.authlib.properties.Property;

/**
 * Class designed to track and control the state of the mod, especially regarding mission launching/running.<br>
 * States are defined by the MissionState enum, and control is handled by
 * MissionStateEpisode subclasses. The ability to set the state directly is
 * restricted, but hooks such as onPlayerReadyForMission etc are exposed to
 * allow subclasses to react to certain state changes.<br>
 * The ProjectMalmo mod app class inherits from this and uses these hooks to run missions.
 */
public class ClientStateMachine extends StateMachine implements IMalmoMessageListener
{
    private static final int WAIT_MAX_TICKS = 2000; // Over 1 minute and a half in client ticks.
    private static final int VIDEO_MAX_WAIT = 90 * 1000; // Max wait for video in ms.
    private static final String MISSING_MCP_PORT_ERROR = "no_mcp";
    private static final String INFO_MCP_PORT = "info_mcp";
    private static final String INFO_RESERVE_STATUS = "info_reservation";

    private MissionInit currentMissionInit = null; // The MissionInit object for the mission currently being loaded/run.
    private MissionBehaviour missionBehaviour = new MissionBehaviour();
    private String missionQuitCode = ""; // The reason why this mission ended.
    private MultidimensionalReward finalReward = new MultidimensionalReward(true); // The reward at the end of the mission, sent separately to ensure timely delivery.
    private MissionDiagnostics missionEndedData = new MissionDiagnostics();
    private ScreenHelper screenHelper = new ScreenHelper();
    protected MalmoModClient inputController;

    // Env service:
    protected MalmoEnvServer envServer;

    // Socket stuff:
    protected TCPInputPoller missionPoller;
    protected TCPInputPoller controlInputPoller;
    protected int integratedServerPort;
    String reservationID = "";   // empty if we are not reserved, otherwise "RESERVED" + the experiment ID we are reserved for.
    long reservationExpirationTime = 0;
    private TCPSocketChannel missionControlSocket;

    private void reserveClient(String id)
    {
        synchronized(this.reservationID)
        {
            ClientStateMachine.this.getScreenHelper().clearFragment(INFO_RESERVE_STATUS);

            // id is in the form <long>:<expID>, where long is the length of time to keep the reservation for,
            // and expID is the experimentationID used to ensure the client is reserved for the correct experiment.
            int separator = id.indexOf(":");
            if (separator == -1)
            {
                System.out.println("Error - malformed reservation request - client will not be reserved.");
                this.reservationID = "";
            }
            else
            {
                long duration = Long.valueOf(id.substring(0, separator));
                String expID = id.substring(separator + 1);
                this.reservationExpirationTime = System.currentTimeMillis() + duration;
                // We don't just use the id, in case users have supplied a blank string as their experiment ID.
                this.reservationID = "RESERVED" + expID;
                ClientStateMachine.this.getScreenHelper().addFragment("Reserved: " + expID, TextCategory.TXT_INFO, (int)duration);//INFO_RESERVE_STATUS);
            }
        }
    }

    private boolean isReserved()
    {
        synchronized(this.reservationID)
        {
            System.out.println("==== RES: " + this.reservationID + " - " + (this.reservationExpirationTime - System.currentTimeMillis()));
            return !this.reservationID.isEmpty() && this.reservationExpirationTime > System.currentTimeMillis();
        }
    }

    private boolean isAvailable(String id)
    {
        synchronized(this.reservationID)
        {
            return (this.reservationID.isEmpty() || this.reservationID.equals("RESERVED" + id) || System.currentTimeMillis() >= this.reservationExpirationTime);
        }
    }

    private void cancelReservation()
    {
        synchronized(this.reservationID)
        {
            this.reservationID = "";
            ClientStateMachine.this.getScreenHelper().clearFragment(INFO_RESERVE_STATUS);
        }            
    }

    protected TCPSocketChannel getMissionControlSocket() { return this.missionControlSocket; }
    
    protected void createMissionControlSocket()
    {
        TCPUtils.LogSection ls = new TCPUtils.LogSection("Creating MissionControlSocket");
        // Set up a TCP connection to the agent:
        ClientAgentConnection cac = currentMissionInit().getClientAgentConnection();
        if (this.missionControlSocket == null ||
            this.missionControlSocket.getPort() != cac.getAgentMissionControlPort() ||
            this.missionControlSocket.getAddress() == null ||
            !this.missionControlSocket.isValid() ||
            !this.missionControlSocket.isOpen() ||
            !this.missionControlSocket.getAddress().equals(cac.getAgentIPAddress()))
        {
            if (this.missionControlSocket != null)
                this.missionControlSocket.close();
            this.missionControlSocket = new TCPSocketChannel(cac.getAgentIPAddress(), cac.getAgentMissionControlPort(), "mcp");
        }
        ls.close();
    }

    public ClientStateMachine(ClientState initialState, MalmoModClient inputController)
    {
        super(initialState);
        this.inputController = inputController;

        // Register ourself on the event busses, so we can harness the client tick:
        MinecraftForge.EVENT_BUS.register(this);
        MalmoMod.MalmoMessageHandler.registerForMessage(this, MalmoMessageType.SERVER_TEXT);
    }

    @Override
    public void clearErrorDetails()
    {
        super.clearErrorDetails();
        this.missionQuitCode = "";
    }

    @SubscribeEvent
    public void onClientTick(TickEvent.ClientTickEvent ev)
    {
        // Use the client tick to ensure we regularly update our state (from the client thread)
        updateState();
    }

    public ScreenHelper getScreenHelper()
    {
        return screenHelper;
    }

    @Override
    public void onMessage(MalmoMessageType messageType, Map<String, String> data)
    {
        if (messageType == MalmoMessageType.SERVER_TEXT)
        {
            String chat = data.get("chat");
            if (chat != null)
                Minecraft.getMinecraft().ingameGUI.getChatGUI().printChatMessageWithOptionalDeletion(new TextComponentString(chat), 1);
            else
            {
                String text = data.get("text");
                ScreenHelper.TextCategory category = ScreenHelper.TextCategory.valueOf(data.get("category"));
                String strtime = data.get("displayTime");
                Integer time = (strtime != null) ? Integer.valueOf(strtime) : null;
                this.getScreenHelper().addFragment(text, category, time);
            }
        }
    }

    @Override
    protected String getName()
    {
        return "CLIENT";
    }

    @Override
    protected void onPreStateChange(IState toState)
    {
        this.getScreenHelper().addFragment("CLIENT: " + toState, ScreenHelper.TextCategory.TXT_CLIENT_STATE, "");
    }

    /**
     * Create the episode object for the requested state.
     * 
     * @param state the state the mod is entering
     * @return a MissionStateEpisode that localises all the logic required to run this state
     */
    @Override
    protected StateEpisode getStateEpisodeForState(IState state)
    {
        if (!(state instanceof ClientState))
            return null;

        ClientState cs = (ClientState) state;
        switch (cs) {
            case WAITING_FOR_MOD_READY:
                return new InitialiseClientModEpisode(this);
            case DORMANT:
                return new DormantEpisode(this);
            case CREATING_HANDLERS:
                return new CreateHandlersEpisode(this);
            case EVALUATING_WORLD_REQUIREMENTS:
                return new EvaluateWorldRequirementsEpisode(this);
            case PAUSING_OLD_SERVER:
                return new PauseOldServerEpisode(this);
            case CLOSING_OLD_SERVER:
                return new CloseOldServerEpisode(this);
            case CREATING_NEW_WORLD:
                return new CreateWorldEpisode(this);
            case WAITING_FOR_SERVER_READY:
                return new WaitingForServerEpisode(this);
            case RUNNING:
                return new MissionRunningEpisode(this);
            case IDLING:
                return new MissionIdlingEpisode(this);
            case MISSION_ENDED:
                return new MissionEndedEpisode(this, MissionResult.ENDED, false, false, true);
            case ERROR_DUFF_HANDLERS:
                return new MissionEndedEpisode(this, MissionResult.MOD_FAILED_TO_INSTANTIATE_HANDLERS, true, true, true);
            case ERROR_INTEGRATED_SERVER_UNREACHABLE:
                return new MissionEndedEpisode(this, MissionResult.MOD_SERVER_UNREACHABLE, true, true, true);
            case ERROR_NO_WORLD:
                return new MissionEndedEpisode(this, MissionResult.MOD_HAS_NO_WORLD_LOADED, true, true, true);
            case ERROR_CANNOT_CREATE_WORLD:
                return new MissionEndedEpisode(this, MissionResult.MOD_FAILED_TO_CREATE_WORLD, true, true, true);
            case ERROR_CANNOT_START_AGENT: // run-ons deliberate
            case ERROR_LOST_AGENT:
            case ERROR_LOST_VIDEO:
                return new MissionEndedEpisode(this, MissionResult.MOD_HAS_NO_AGENT_AVAILABLE, true, true, false);
            case ERROR_LOST_NETWORK_CONNECTION: // run-on deliberate
            case ERROR_CANNOT_CONNECT_TO_SERVER:
                return new MissionEndedEpisode(this, MissionResult.MOD_CONNECTION_FAILED, true, false, true); // No point trying to inform the server - we can't reach it anyway!
            case ERROR_TIMED_OUT_WAITING_FOR_EPISODE_START: // run-ons deliberate
            case ERROR_TIMED_OUT_WAITING_FOR_EPISODE_PAUSE:
            case ERROR_TIMED_OUT_WAITING_FOR_EPISODE_CLOSE:
            case ERROR_TIMED_OUT_WAITING_FOR_MISSION_END:
            case ERROR_TIMED_OUT_WAITING_FOR_WORLD_CREATE:
                return new MissionEndedEpisode(this, MissionResult.MOD_CONNECTION_FAILED, true, true, true);
            case MISSION_ABORTED:
                return new MissionEndedEpisode(this, MissionResult.MOD_SERVER_ABORTED_MISSION, true, false, true);  // Don't inform the server - it already knows (we're acting on its notification)
            case WAITING_FOR_SERVER_MISSION_END:
                return new WaitingForServerMissionEndEpisode(this);
            default:
                break;
        }
        return null;
    }

    protected MissionInit currentMissionInit()
    {
        return this.currentMissionInit;
    }

    protected MissionBehaviour currentMissionBehaviour()
    {
        return this.missionBehaviour;
    }

    protected class MissionInitResult
    {
        public MissionInit missionInit = null;
        public boolean wasMissionInit = false;
        public String error = null;
    }

    protected MissionInitResult decodeMissionInit(String command)
    {
        MissionInitResult result = new MissionInitResult();
        if (command == null)
        {
            result.error = "Null command passed.";
            return result;
        }

        String rootNodeName = SchemaHelper.getRootNodeName(command);
        if (rootNodeName != null && rootNodeName.equals("MissionInit"))
        {
            result.wasMissionInit = true;
            // Attempt to decode the MissionInit XML string.
            try
            {
                result.missionInit = (MissionInit) SchemaHelper.deserialiseObject(command, "MissionInit.xsd", MissionInit.class);
            }
            catch (JAXBException e)
            {
                System.out.println("JAXB exception: " + e);
                if (e.getMessage() != null)
                    result.error = e.getMessage();
                else if (e.getLinkedException() != null && e.getLinkedException().getMessage() != null)
                    result.error = e.getLinkedException().getMessage();
                else
                    result.error = "Unspecified problem parsing MissionInit - check your Mission xml.";
            }
            catch (SAXException e)
            {
                System.out.println("SAX exception: " + e);
                result.error = e.getMessage();
            }
            catch (XMLStreamException e)
            {
                System.out.println("XMLStreamException: " + e);
                result.error = e.getMessage();
            }
        }
        return result;
    }

    protected boolean areMissionsEqual(Mission m1, Mission m2)
    {
        return true;
        // FIX NEEDED - the following code fails because m1 may have been
        // modified since loading - eg the MazeDecorator writes directly to the XML,
        // and the use of some of the getters in the XSD-generated code can cause extra
        // (empty) nodes to be added to the resulting XML.
        // We need a more robust way of comparing two mission objects.
        // For now, simply return true, since a false positive is less dangerous
        // than a false negative.
        /*
        try {
            String s1 = SchemaHelper.serialiseObject(m1, Mission.class);
            String s2 = SchemaHelper.serialiseObject(m2, Mission.class);
            return s1.compareTo(s2) == 0;
        } catch( JAXBException e ) {
            System.out.println("JAXB exception: " + e);
            return false;
        }*/
    }

    /**
     * Set up the mission poller.<br>
     * This is called during the initialisation episode, but also needs to be
     * available for other episodes in case the configuration changes, resulting
     * in changes to the ports.
     * 
     * @throws UnknownHostException
     */
    protected void initialiseComms() throws UnknownHostException
    {
        // Start polling for missions:
        if (this.missionPoller != null)
        {
            this.missionPoller.stopServer();
        }

        this.missionPoller = new TCPInputPoller(AddressHelper.getMissionControlPortOverride(), AddressHelper.MIN_MISSION_CONTROL_PORT, AddressHelper.MAX_FREE_PORT, true, "mcp")
        {
            @Override
            public void onError(String error, DataOutputStream dos)
            {
                System.out.println("SENDING ERROR: " + error);
                try
                {
                    dos.writeInt(error.length());
                    dos.writeBytes(error);
                    dos.flush();
                }
                catch (IOException e)
                {
                }
            }

            private void reply(String reply, DataOutputStream dos)
            {
                System.out.println("REPLYING WITH: " + reply);
                try
                {
                    dos.writeInt(reply.length());
                    dos.writeBytes(reply);
                    dos.flush();
                }
                catch (IOException e)
                {
                    System.out.println("Failed to reply to message!");
                }
            }

            @Override
            public boolean onCommand(String command, String ipFrom, DataOutputStream dos)
            {
                System.out.println("Received from " + ipFrom + ":" + command);
                boolean keepProcessing = false;

                // Possible commands:
                // 1: MALMO_REQUEST_CLIENT:<malmo version>:<reservation_length(ms)><experiment_id>
                // 2: MALMO_CANCEL_REQUEST
                // 3: MALMO_FIND_SERVER<experiment_id>
                // 4: MALMO_KILL_CLIENT
                // 5: MissionInit

                String reservePrefixGeneral = "MALMO_REQUEST_CLIENT:";
                String reservePrefix = reservePrefixGeneral + Loader.instance().activeModContainer().getVersion() + ":";
                String findServerPrefix = "MALMO_FIND_SERVER";
                String cancelRequestCommand = "MALMO_CANCEL_REQUEST";
                String killClientCommand = "MALMO_KILL_CLIENT";
                
                if (command.startsWith(reservePrefix))
                {
                    // Reservation request.
                    // We either reply with MALMOOK, if we are free, or MALMOBUSY if not.
                    IState currentState = getStableState();
                    if (currentState != null && currentState.equals(ClientState.DORMANT) && !isReserved())
                    {
                        reserveClient(command.substring(reservePrefix.length()));
                        reply("MALMOOK", dos);
                    }
                    else
                    {
                        // We're busy - we can't be reserved.
                        reply("MALMOBUSY", dos);
                    }
                }
                else if (command.startsWith(reservePrefixGeneral))
                {
                    // Reservation request, but it didn't match the request we expect, above.
                    // This happens if the agent sending the request is running a different version of Malmo -
                    // a version mismatch error.
                    reply("MALMOERRORVERSIONMISMATCH in reservation string (Got " + command + ", expected " + reservePrefix + " - check your path for old versions of MalmoPython/MalmoJava/Malmo.lib etc)", dos);
                }
                else if (command.equals(cancelRequestCommand))
                {
                    // If we've been reserved, cancel the reservation.
                    if (isReserved())
                    {
                        cancelReservation();
                        reply("MALMOOK", dos);
                    }
                    else
                    {
                        // We weren't reserved in the first place - something is odd.
                        reply("MALMOERRORAttempt to cancel a reservation that was never made.", dos);
                    }
                }
                else if (command.startsWith(findServerPrefix))
                {
                    // Request to find the server for the given experiment ID.
                    String expID = command.substring(findServerPrefix.length());
                    if (currentMissionInit() != null && currentMissionInit().getExperimentUID().equals(expID))
                    {
                        // Our Experiment IDs match, so we are running the same experiment.
                        // Return the port and server IP address to the caller:
                        MinecraftServerConnection msc = currentMissionInit().getMinecraftServerConnection();
                        if (msc == null)
                            reply("MALMONOSERVERYET", dos); // Mission might be starting up.
                        else
                            reply("MALMOS" + msc.getAddress().trim() + ":" + msc.getPort(), dos);
                    }
                    else
                    {
                        // We don't have a MissionInit ourselves, or we're running a different experiment,
                        // so we can't help.
                        reply("MALMONOSERVER", dos);
                    }
                }
                else if (command.equals(killClientCommand))
                {
                    // Kill switch provided in case AI takes over the world...
                    // Or, more likely, in case this Minecraft instance has become unreliable (eg if it's been running for several days)
                    // and needs to be replaced with a fresh instance.
                    // If we are currently running a mission, we gracefully decline, to prevent users from wiping out
                    // other users' experiments.
                    // We also decline unless we were launched in "replaceable" mode - a command-line switch that indicates we were
                    // launched by a script which is still running, and can therefore replace us when we terminate.
                    IState currentState = getStableState();
                    if (currentState != null && currentState.equals(ClientState.DORMANT) && !isReserved())
                    {
                        Configuration config = MalmoMod.instance.getModSessionConfigFile();
                        if (config.getBoolean("replaceable", "runtype", false, "Will be replaced if killed"))
                        {
                            reply("MALMOOK", dos);

                            missionPoller.stopServer();
                            exitJava();
                        }
                        else
                        {
                            reply("MALMOERRORNOTKILLABLE", dos);
                        }
                    }
                    else
                    {
                        // We're too busy and important to be killed.
                        reply("MALMOBUSY", dos);
                    }
                }
                else
                {
                    // See if we've been sent a MissionInit message:

                    MissionInitResult missionInitResult = decodeMissionInit(command);

                    if (missionInitResult.wasMissionInit && missionInitResult.missionInit == null)
                    {
                        // Got sent a duff MissionInit xml - pass back the JAXB/SAXB errors.
                        reply("MALMOERROR" + missionInitResult.error, dos);
                    }
                    else if (missionInitResult.wasMissionInit && missionInitResult.missionInit != null)
                    {
                        MissionInit missionInit = missionInitResult.missionInit;
                        // We've been sent a MissionInit message.
                        // First, check the version number:
                        String platformVersion = missionInit.getPlatformVersion();
                        String ourVersion = Loader.instance().activeModContainer().getVersion();
                        if (platformVersion == null || !platformVersion.equals(ourVersion))
                        {
                            reply("MALMOERRORVERSIONMISMATCH (Got " + platformVersion + ", expected " + ourVersion + " - check your path for old versions of MalmoPython/MalmoJava/Malmo.lib etc)", dos);
                        }
                        else
                        {
                            // MissionInit passed to us - this is a request to launch this mission. Can we?
                            IState currentState = getStableState();
                            if (currentState != null && currentState.equals(ClientState.DORMANT) && isAvailable(missionInit.getExperimentUID()))
                            {
                                reply("MALMOOK", dos);
                                keepProcessing = true; // State machine will now process this MissionInit and start the mission.
                            }
                            else
                            {
                                // We're busy - we can't run this mission.
                                reply("MALMOBUSY", dos);
                            }
                        }
                    }
                }

                return keepProcessing;
            }
        };

        int mcPort = 0;
        if (MalmoEnvServer.isEnv()) {
            // Start up new "Env" service instead of Malmo AgentHost api.
            System.out.println("***** Start MalmoEnvServer on port " + AddressHelper.getMissionControlPortOverride());
            this.envServer = new MalmoEnvServer(
                Loader.instance().activeModContainer().getVersion(), 
                AddressHelper.getMissionControlPortOverride(), 
                this.missionPoller,
                this.inputController);
            Thread thread = new Thread("MalmoEnvServer") {
                public void run() {
                    try {
                        envServer.serve();
                    } catch (IOException ioe) {
                        System.out.println("MalmoEnvServer exist on " + ioe);
                    }
                }
            };
            thread.start();
        } else {
            // "Legacy" AgentHost api.
            this.missionPoller.start();
            mcPort = ClientStateMachine.this.missionPoller.getPortBlocking();
        }

        // Tell the address helper what the actual port is:
        AddressHelper.setMissionControlPort(mcPort);
        if (AddressHelper.getMissionControlPort() == -1)
        {
            // Failed to create a mission control port - nothing will work!
            System.out.println("**** NO MISSION CONTROL SOCKET CREATED - WAS THE PORT IN USE? (Check Mod GUI options) ****");
            ClientStateMachine.this.getScreenHelper().addFragment("ERROR: Could not open a Mission Control Port - check the Mod GUI options.", TextCategory.TXT_CLIENT_WARNING, MISSING_MCP_PORT_ERROR);
        }
        else
        {
            // Clear the error string, if there was one:
            ClientStateMachine.this.getScreenHelper().clearFragment(MISSING_MCP_PORT_ERROR);
        }
        // Display the port number:
        ClientStateMachine.this.getScreenHelper().clearFragment(INFO_MCP_PORT);
        if (AddressHelper.getMissionControlPort() != -1)
            ClientStateMachine.this.getScreenHelper().addFragment("MCP: " + AddressHelper.getMissionControlPort(), TextCategory.TXT_INFO, INFO_MCP_PORT);
    }

    public static void exitJava() {
        // Give non-hard exit 10 seconds to complete and force a hard exit.
        Thread deadMansHandle = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 10; i > 0; i--) {
                    try {
                        Thread.sleep(1000);
                        System.out.println("Waiting to exit " + i + "...");
                    } catch (InterruptedException e) {
                        System.out.println("Interrupted " + i + "...");
                    }
                }

                // Kill it with fire!!!
                System.out.println("Attempting hard exit");
                FMLCommonHandler.instance().exitJava(0, true);
            }
        });

        deadMansHandle.setDaemon(true);
        deadMansHandle.start();

        // Have to use FMLCommonHandler; direct calls to System.exit() are trapped and denied by the FML code.
        FMLCommonHandler.instance().exitJava(0, false);
    }

    // ---------------------------------------------------------------------------------------------------------
    // Episode helpers - each extends a MissionStateEpisode to encapsulate a certain state
    // ---------------------------------------------------------------------------------------------------------

    public abstract class ErrorAwareEpisode extends StateEpisode implements IMalmoMessageListener
    {
        protected Boolean errorFlag = false;
        protected Map<String, String> errorData = null;

        public ErrorAwareEpisode(ClientStateMachine machine)
        {
            super(machine);
            MalmoMod.MalmoMessageHandler.registerForMessage(this, MalmoMessageType.SERVER_ABORT);
        }

        protected boolean pingAgent(boolean abortIfFailed)
        {
            if (AddressHelper.getMissionControlPort() == 0) {
                // MalmoEnvServer has no server to client ping.
                return true;
            }

            boolean sentOkay = ClientStateMachine.this.getMissionControlSocket().sendTCPString("<?xml version=\"1.0\" encoding=\"UTF-8\"?><ping/>", 1);
            if (!sentOkay)
            {
                // It's not available - bail.
                ClientStateMachine.this.getScreenHelper().addFragment("ERROR: Lost contact with agent - aborting mission", TextCategory.TXT_CLIENT_WARNING, 10000);
                if (abortIfFailed)
                    episodeHasCompletedWithErrors(ClientState.ERROR_LOST_AGENT, "Lost contact with the agent");
            }
            return sentOkay;
        }

        @Override
        public void onMessage(MalmoMessageType messageType, Map<String, String> data)
        {
            if (messageType == MalmoMod.MalmoMessageType.SERVER_ABORT)
            {
                synchronized (this.errorFlag)
                {
                    this.errorFlag = true;
                    this.errorData = data;
                    // Save the error message, if there is one:
                    if (data != null)
                    {
                        String message = data.get("message");
                        String user = data.get("username");
                        String error = data.get("error");
                        String report = "";
                        if (user != null)
                            report += "From " + user + ": ";
                        if (error != null)
                            report += error;
                        if (message != null)
                            report += " (" + message + ")";
                        ClientStateMachine.this.saveErrorDetails(report);
                    }
                    onAbort(data);
                }
            }
        }

        @Override
        public void cleanup()
        {
            super.cleanup();
            MalmoMod.MalmoMessageHandler.deregisterForMessage(this, MalmoMessageType.SERVER_ABORT);
        }

        protected boolean inAbortState()
        {
            synchronized (this.errorFlag)
            {
                return this.errorFlag;
            }
        }

        protected Map<String, String> getErrorData()
        {
            synchronized (this.errorFlag)
            {
                return this.errorData;
            }
        }

        protected void onAbort(Map<String, String> errorData)
        {
            // Default does nothing, but can be overridden.
        }
    }

    /**
     * Helper base class that responds to the config change and updates our AddressHelper.<br>
     * This will also reset the mission poller. Depending on the state, more
     * work may be needed (eg to recreate the command handler, etc) - it's up to
     * the individual state episodes to do whatever else needs doing.
     */
    abstract public class ConfigAwareStateEpisode extends ErrorAwareEpisode
    {
        ConfigAwareStateEpisode(ClientStateMachine machine)
        {
            super(machine);
        }

        @Override
        public void onConfigChanged(OnConfigChangedEvent ev)
        {
            if (ev.getConfigID().equals(MalmoMod.SOCKET_CONFIGS))
            {
                AddressHelper.update(MalmoMod.instance.getModSessionConfigFile());
                try
                {
                    ClientStateMachine.this.initialiseComms();
                }
                catch (UnknownHostException e)
                {
                    // TODO What to do here?
                    e.printStackTrace();
                }
                ScreenHelper.update(MalmoMod.instance.getModPermanentConfigFile());
                TCPUtils.update(MalmoMod.instance.getModPermanentConfigFile());
            }
        }
    }

    /** Initial episode - perform client setup */
    public class InitialiseClientModEpisode extends ConfigAwareStateEpisode
    {
        InitialiseClientModEpisode(ClientStateMachine machine)
        {
            super(machine);
        }

        @Override
        protected void execute() throws Exception
        {
            ClientStateMachine.this.initialiseComms();

            // This is necessary in order to allow user to exit the Minecraft window without halting the experiment:
            GameSettings settings = Minecraft.getMinecraft().gameSettings;
            settings.pauseOnLostFocus = false;
            // And hook the screen helper into the ingame gui (which is responsible for overlaying chat, titles etc) -
            // this has to be done after Minecraft.init(), so we do it here.
            ScreenHelper.hookIntoInGameGui();
        }

        @Override
        public void onRenderTick(TickEvent.RenderTickEvent ev)
        {
            // We wait until we start to get render ticks, at which point we assume Minecraft has finished starting up.
            episodeHasCompleted(ClientState.DORMANT);
        }
    }

    // ---------------------------------------------------------------------------------------------------------
    /** Dormant state - receptive to new missions */
    public class DormantEpisode extends ConfigAwareStateEpisode
    {
        private ClientStateMachine csMachine;

        protected DormantEpisode(ClientStateMachine machine)
        {
            super(machine);
            this.csMachine = machine;
        }

        @Override
        protected void execute()
        {
            TextureHelper.init();

            // Clear our current MissionInit state:
            csMachine.currentMissionInit = null;
            // Clear our current error state:
            clearErrorDetails();
            // And clear out any stale commands left over from recent missions:
            if (ClientStateMachine.this.controlInputPoller != null)
                ClientStateMachine.this.controlInputPoller.clearCommands();
            // Finally, do some Java housekeeping:
            System.gc();

            if (envServer != null) {
                envServer.setRunning(false);
            }
        }

        @Override
        public void onClientTick(TickEvent.ClientTickEvent ev) throws Exception
        {

            Minecraft.getMinecraft().mcProfiler.startSection("malmoHandleMissionCommands");
            checkForMissionCommand();
            Minecraft.getMinecraft().mcProfiler.endSection();
        }

        private void checkForMissionCommand() throws Exception
        {
            if (ClientStateMachine.this.missionPoller == null)
                return;

            CommandAndIPAddress comip = missionPoller.getCommandAndIPAddress();
            if (comip == null)
                return;
            String missionMessage = comip.command;
            if (missionMessage == null || missionMessage.length() == 0)
                return;
            Minecraft.getMinecraft().mcProfiler.endSection();
            Minecraft.getMinecraft().mcProfiler.startSection("malmoDecodeMissionInit");

            MissionInitResult missionInitResult = decodeMissionInit(missionMessage);
            Minecraft.getMinecraft().mcProfiler.endSection();

            MissionInit missionInit = missionInitResult.missionInit;
            if (missionInit != null)
            {
                missionInit.getClientAgentConnection().setAgentIPAddress(comip.ipAddress);
                System.out.println("Mission received: " + missionInit.getMission().getAbout().getSummary());
                csMachine.currentMissionInit = missionInit;
                TimeHelper.SyncManager.numTicks = 0;
                ScoreHelper.logMissionInit(missionInit);

                ClientStateMachine.this.createMissionControlSocket();
                // Move on to next state:
                episodeHasCompleted(ClientState.CREATING_HANDLERS);
            }
            else
            {
                throw new Exception("Failed to get valid MissionInit object from SchemaHelper.");
            }
        }
    }

    // ---------------------------------------------------------------------------------------------------------
    /**
     * Now the MissionInit XML has been decoded, the client needs to create the
     * Mission Handlers.
     */
    public class CreateHandlersEpisode extends ConfigAwareStateEpisode
    {
        protected CreateHandlersEpisode(ClientStateMachine machine)
        {
            super(machine);
        }

        @Override
        protected void execute() throws Exception
        {
            // First, clear our reservation state, if we were reserved:
            ClientStateMachine.this.cancelReservation();

            // Now try creating the handlers:
            try
            {
                if(envServer != null){
                    SeedHelper.advanceNextSeed(envServer.getSeed());
                }
                ClientStateMachine.this.missionBehaviour = MissionBehaviour.createAgentHandlersFromMissionInit(currentMissionInit());
                if (envServer != null) {
                    ClientStateMachine.this.missionBehaviour.addQuitProducer(envServer);
                }
            }
            catch (Exception e)
            {
                // TODO
                System.err.println("ERROR: Exception caught making agent handlers" + e.toString());
                e.printStackTrace();
            }
            // Set up our command input poller. This is only checked during the MissionRunning episode, but
            // it needs to be started now, so we can report the port it's using back to the agent.
            TCPUtils.LogSection ls = new TCPUtils.LogSection("Initialise Command Input Poller");
            ClientAgentConnection cac = currentMissionInit().getClientAgentConnection();
            int requestedPort = cac.getClientCommandsPort();
            // If the requested port is 0, we dynamically allocate our own port, and feed that back to the agent.
            // If the requested port is non-zero, we have to use it.
            if (requestedPort != 0 && ClientStateMachine.this.controlInputPoller != null && ClientStateMachine.this.controlInputPoller.getPort() != requestedPort)
            {
                // A specific port has been requested, and it's not the one we are currently using,
                // so we need to recreate our poller.
                System.out.println("Requested command port is not the same as the input poller port; the port was not free. Stopping server.");
                ClientStateMachine.this.controlInputPoller.stopServer();
                ClientStateMachine.this.controlInputPoller = null;
            }
            if (ClientStateMachine.this.controlInputPoller == null)
            {
                if (requestedPort == 0)
                    ClientStateMachine.this.controlInputPoller = new TCPInputPoller(AddressHelper.MIN_FREE_PORT, AddressHelper.MAX_FREE_PORT, true, "com");
                else
                    ClientStateMachine.this.controlInputPoller = new TCPInputPoller(requestedPort, "com");
                System.out.println("Starting command server.");
                ClientStateMachine.this.controlInputPoller.start();
            }
            // Make sure the cac is up-to-date:
            cac.setClientCommandsPort(ClientStateMachine.this.controlInputPoller.getPortBlocking());
            ls.close();

            // Check to see whether anything has caused us to abort - if so, go to the abort state.
            if (inAbortState())
                episodeHasCompleted(ClientState.MISSION_ABORTED);

            // Set the agent's name as the current username:
            List<AgentSection> agents = currentMissionInit().getMission().getAgentSection();
            String agentName = agents.get(currentMissionInit().getClientRole()).getName();
            AuthenticationHelper.setPlayerName(Minecraft.getMinecraft().getSession(), agentName);
            // Handlers and poller created successfully; proceed to next stage of loading.
            // We will either need to connect to an existing server, or to start
            // a new integrated server ourselves, depending on our role.
            // For now, assume that the mod with role 0 is responsible for the server.
            if (currentMissionInit().getClientRole() == 0)
            {
                // We are responsible for the server - investigate what needs to happen next:
                episodeHasCompleted(ClientState.EVALUATING_WORLD_REQUIREMENTS);
            }
            else
            {
                // We may need to connect to a server.
                episodeHasCompleted(ClientState.WAITING_FOR_SERVER_READY);
            }
        }
    }

    // ---------------------------------------------------------------------------------------------------------
    /**
     * Attempt to connect to a server. Wait until connection is established.
     */
    public class WaitingForServerEpisode extends ConfigAwareStateEpisode
    {
        String agentName;
        int ticksUntilNextPing = 0;
        int totalTicks = 0;
        boolean waitingForChunk = false;
        boolean waitingForPlayer = true;

        protected WaitingForServerEpisode(ClientStateMachine machine)
        {
            super(machine);
            MalmoMod.MalmoMessageHandler.registerForMessage(this, MalmoMessageType.SERVER_ALLPLAYERSJOINED);
        }

        private boolean isChunkReady()
        {
            // First, find the starting position we ought to have:
            List<AgentSection> agents = currentMissionInit().getMission().getAgentSection();
            if (agents == null || agents.size() <= currentMissionInit().getClientRole())
                return true;    // This should never happen.
            AgentSection as = agents.get(currentMissionInit().getClientRole());
            if (as.getAgentStart() != null && as.getAgentStart().getPlacement() != null)
            {
                PosAndDirection pos = as.getAgentStart().getPlacement();
                int x = MathHelper.floor(pos.getX().doubleValue()) >> 4;
                int z = MathHelper.floor(pos.getZ().doubleValue()) >> 4;
                // Now get the chunk we should be starting in:
                IChunkProvider chunkprov = Minecraft.getMinecraft().world.getChunkProvider();
                EntityPlayerSP player = Minecraft.getMinecraft().player;
                if (player.addedToChunk)
                {
                    // Our player is already added to a chunk - is it the right one?
                    Chunk actualChunk = chunkprov.provideChunk(player.chunkCoordX, player.chunkCoordZ);
                    Chunk requestedChunk = chunkprov.provideChunk(x,  z);
                    if (actualChunk == requestedChunk && actualChunk != null && !actualChunk.isEmpty())
                    {
                        // We're in the right chunk, and it's not an empty chunk.
                        // We're ready to proceed, but first set our client positions to where we ought to be.
                        // The server should be doing this too, but there's no harm (probably) in doing it ourselves.
                        player.posX = pos.getX().doubleValue();
                        player.posY = pos.getY().doubleValue();
                        player.posZ = pos.getZ().doubleValue();
                        return true;
                    }
                }
                return false;   // Our starting position has been specified, but it's not yet ready.
            }
            return true;    // No starting position specified, so doesn't matter where we start.
        }

        @Override
        protected void onClientTick(ClientTickEvent ev)
        {
            // Check to see whether anything has caused us to abort - if so, go to the abort state.
            if (inAbortState())
                episodeHasCompleted(ClientState.MISSION_ABORTED);

            if (this.waitingForPlayer)
            {
                if (Minecraft.getMinecraft().player != null)
                {
                    this.waitingForPlayer = false;
                    handleLan();
                }
                else
                    return;
            }

            totalTicks++;

            if (ticksUntilNextPing == 0)
            {
                // Tell the server what our agent name is.
                // We do this repeatedly, because the server might not yet be listening.
                if (Minecraft.getMinecraft().player != null && !this.waitingForChunk)
                {
                    HashMap<String, String> map = new HashMap<String, String>();
                    map.put("agentname", agentName);
                    map.put("username", Minecraft.getMinecraft().player.getName());
                    currentMissionBehaviour().appendExtraServerInformation(map);
                    System.out.println("***Telling server we are ready - " + agentName);
                    MalmoMod.network.sendToServer(new MalmoMod.MalmoMessage(MalmoMessageType.CLIENT_AGENTREADY, 0, map));
                }

                // We also ping our agent, just to check it is still available:
                pingAgent(true);    // Will abort to an error state if client unavailable.

                ticksUntilNextPing = 10; // Try again in ten ticks.
            }
            else
            {
                ticksUntilNextPing--;
            }

            if (this.waitingForChunk)
            {
                // The server is ready, we're just waiting for our chunk to appear.
                if (isChunkReady())
                    proceed();
            }

            List<AgentSection> agents = currentMissionInit().getMission().getAgentSection();
            boolean completedWithErrors = false;
            
            if (agents.size() > 1 && currentMissionInit().getClientRole() != 0)
            {
                // We are waiting to join an out-of-process server. Need to pay attention to what happens -
                // if we can't join, for any reason, we should abort the mission.
                GuiScreen screen = Minecraft.getMinecraft().currentScreen;
                if (screen != null && screen instanceof GuiDisconnected) {
                    // Disconnected screen appears when something has gone wrong.
                    // Would be nice to grab the reason from the screen, but it's a private member.
                    // (Can always use reflection, but it's so inelegant.)
                    String msg = "Unable to connect to Minecraft server in multi-agent mission.";
                    TCPUtils.Log(Level.SEVERE, msg);
                    episodeHasCompletedWithErrors(ClientState.ERROR_CANNOT_CONNECT_TO_SERVER, msg);
                    completedWithErrors = true;
                }
            }

            if (!completedWithErrors && totalTicks > WAIT_MAX_TICKS)
            {
                String msg = "Too long waiting for server episode to start.";
                TCPUtils.Log(Level.SEVERE, msg);
                episodeHasCompletedWithErrors(ClientState.ERROR_TIMED_OUT_WAITING_FOR_EPISODE_START, msg);
            }

            if(envServer != null) {
                if(envServer.doIWantToQuit(currentMissionInit())){
                    episodeHasCompleted(ClientState.MISSION_ABORTED); //This could be a source of a race condition.
                }
            }
        }

        @Override
        protected void execute() throws Exception
        {
            totalTicks = 0;

            Minecraft.getMinecraft().displayGuiScreen(null); // Clear any menu screen that might confuse things.
            // Get our name from the Mission:
            List<AgentSection> agents = currentMissionInit().getMission().getAgentSection();
            //if (agents == null || agents.size() <= currentMissionInit().getClientRole())
            //    throw new Exception("No agent section for us!"); // TODO
            this.agentName = agents.get(currentMissionInit().getClientRole()).getName();

            if (agents.size() > 1 && currentMissionInit().getClientRole() != 0)
            {
                // Multi-agent mission, we should be joining a server.
                // (Unless we are already on the correct server.)
                String address = currentMissionInit().getMinecraftServerConnection().getAddress().trim();
                int port = currentMissionInit().getMinecraftServerConnection().getPort();
                String targetIP = address + ":" + port;
                System.out.println("We should be joining " + targetIP);
                EntityPlayerSP player = Minecraft.getMinecraft().player;
                boolean namesMatch = (player == null) || Minecraft.getMinecraft().player.getName().equals(this.agentName);
                if (!namesMatch)
                {
                    // The name of our agent no longer matches the agent in our game profile -
                    // safest way to update is to log out and back in again.
                    // This hangs so just warn instead about the miss-match and proceed.
                    TCPUtils.Log(Level.WARNING,"Agent name does not match agent in game.");
                    // Minecraft.getMinecraft().world.sendQuittingDisconnectingPacket();
                    // Minecraft.getMinecraft().loadWorld((WorldClient)null);
                }
                if (Minecraft.getMinecraft().getCurrentServerData() == null || !Minecraft.getMinecraft().getCurrentServerData().serverIP.equals(targetIP))
                {
                    net.minecraftforge.fml.client.FMLClientHandler.instance().connectToServerAtStartup(address, port);

                    // TODO - should we wait for a connected notification?
                    TimeHelper.SyncManager.setServerRunning();
                    TimeHelper.SyncManager.setPistolFired(true);
                }
                this.waitingForPlayer = false;
            }
        }
        
        protected void handleLan()
        {
            // Get our name from the Mission:
            final List<AgentSection> agents = currentMissionInit().getMission().getAgentSection();
            final HumanInteraction hc = currentMissionInit().getMission().getServerSection().getHumanInteraction();
            //if (agents == null || agents.size() <= currentMissionInit().getClientRole())
            //    throw new Exception("No agent section for us!"); // TODO
            this.agentName = agents.get(currentMissionInit().getClientRole()).getName();

            if ((hc != null || agents.size() > 1) && currentMissionInit().getClientRole() == 0) // Multi-agent mission - make sure the server is open to the LAN:
            {
                final MinecraftServerConnection msc = new MinecraftServerConnection();
                final String address = currentMissionInit().getClientAgentConnection().getClientIPAddress();
                // Do we need to open to LAN?
                if (Minecraft.getMinecraft().isSingleplayer() && !Minecraft.getMinecraft().getIntegratedServer().getPublic())
                {
                    String portStr = "";
                    if (hc == null)
                        portStr = Minecraft.getMinecraft().getIntegratedServer().shareToLAN(GameType.SURVIVAL, true); // Set to true to stop spam kicks.
                    else{
                        try
                        {
                            // 1.11.2 - start our own server on a SPECIFIC port.
                            final IntegratedServer serv = Minecraft.getMinecraft().getIntegratedServer();
                            final Integer i = Integer.parseInt(hc.getPort());
                            serv.getNetworkSystem().addLanEndpoint((InetAddress)null, i);
                            serv.isPublic = true;
                            serv.lanServerPing = new ThreadLanServerPing(serv.getMOTD(), i + "");
                            serv.lanServerPing.start();
                            serv.getPlayerList().setGameType(GameType.SURVIVAL);
                            serv.getPlayerList().setCommandsAllowedForAll(true);
                            serv.mc.player.setPermissionLevel(true ? 4 : 0);
                            
                            serv.getPlayerList().maxPlayers = hc.getMaxPlayers() + 1; //TODO: for multi-agent add more.
                            portStr = i + "";

                        } catch (final IOException var6)
                        {
                            System.out.println("[ERROR] Could not make MineRL agent server public on port" + hc.getPort() + ".");
                            synchronized(this.errorFlag){
                                this.errorFlag = true;
                            }
                        }
                    }
                    ClientStateMachine.this.integratedServerPort = Integer.valueOf(portStr);
                }

                TCPUtils.Log(Level.INFO,"Integrated server port: " + ClientStateMachine.this.integratedServerPort);
                msc.setPort(ClientStateMachine.this.integratedServerPort);
                msc.setAddress(address);

                if (envServer != null) {
                    envServer.notifyIntegrationServerStarted(ClientStateMachine.this.integratedServerPort);
                }
                currentMissionInit().setMinecraftServerConnection(msc);
            }
        }

        @Override
        public void onMessage(MalmoMessageType messageType, Map<String, String> data)
        {
            super.onMessage(messageType, data);

            if (messageType != MalmoMessageType.SERVER_ALLPLAYERSJOINED)
                return;

            List<Object> handlers = new ArrayList<Object>();
            for (Entry<String, String> entry : data.entrySet())
            {
                if (entry.getKey().equals("startPosition"))
                {
                    try
                    {
                        String[] parts = entry.getValue().split(":");
                        Float x = Float.valueOf(parts[0]);
                        Float y = Float.valueOf(parts[1]);
                        Float z = Float.valueOf(parts[2]);
                        // Find the starting position we ought to have:
                        List<AgentSection> agents = currentMissionInit().getMission().getAgentSection();
                        if (agents != null && agents.size() > currentMissionInit().getClientRole())
                        {
                            // And write this new position into it:
                            AgentSection as = agents.get(currentMissionInit().getClientRole());
                            AgentStart startSection = as.getAgentStart();
                            if (startSection != null)
                            {
                                PosAndDirection pos = startSection.getPlacement();
                                if (pos == null)
                                    pos = new PosAndDirection();
                                pos.setX(new BigDecimal(x));
                                pos.setY(new BigDecimal(y));
                                pos.setZ(new BigDecimal(z));
                                startSection.setPlacement(pos);
                                as.setAgentStart(startSection);
                            }
                        }
                    }
                    catch (Exception e)
                    {
                        System.out.println("Couldn't interpret position data");
                    }
                }
                else
                {
                    String extraHandler = entry.getValue();
                    if (extraHandler != null && extraHandler.length() > 0)
                    {
                        try
                        {
                            Class<?> handlerClass = Class.forName(entry.getKey());
                            Object handler = SchemaHelper.deserialiseObject(extraHandler, "MissionInit.xsd", handlerClass);
                            handlers.add(handler);
                        }
                        catch (Exception e)
                        {
                            System.out.println("Error trying to create extra handlers: " + e);
                            // Do something... like episodeHasCompletedWithErrors(nextState, error)?
                        }
                    }
                }
            }
            if (!handlers.isEmpty())
                currentMissionBehaviour().addExtraHandlers(handlers);
            this.waitingForChunk = true;
        }
        
        private void proceed()
        {
            // The server is ready, so send our MissionInit back to the agent and go!
            // We launch the agent by sending it the MissionInit message we were sent
            // (but with the Launcher's IP address included)
            String xml = null;
            boolean sentOkay = false;
            String errorReport = "";
            try
            {
                xml = SchemaHelper.serialiseObject(currentMissionInit(), MissionInit.class);
                if (AddressHelper.getMissionControlPort() == 0) {
                    if (envServer != null) {
                        // TODO MalmoEnvServer <- Running
                    }
                    sentOkay = true;
                } else {
                    sentOkay = ClientStateMachine.this.getMissionControlSocket().sendTCPString(xml, 1);
                }
            }
            catch (JAXBException e)
            {
                errorReport = e.getMessage();
            }
            if (sentOkay)
                episodeHasCompleted(ClientState.RUNNING);
            else
            {
                ClientStateMachine.this.getScreenHelper().addFragment("ERROR: Could not contact agent to start mission - mission will abort.", TextCategory.TXT_CLIENT_WARNING, 10000);
                if (!errorReport.isEmpty())
                {
                    ClientStateMachine.this.getScreenHelper().addFragment("ERROR DETAILS: " + errorReport, TextCategory.TXT_CLIENT_WARNING, 10000);
                    errorReport = ": " + errorReport;
                }
                episodeHasCompletedWithErrors(ClientState.ERROR_CANNOT_START_AGENT, "Failed to send MissionInit back to agent" + errorReport);
            }
        }

        @Override
        public void cleanup()
        {
            super.cleanup();
            MalmoMod.MalmoMessageHandler.deregisterForMessage(this, MalmoMessageType.SERVER_ALLPLAYERSJOINED);
        }
    }

    /**
     * Wait for the server to decide the mission has ended.<br>
     * We're not allowed to return to dormant until the server decides everyone can.
     */
    public class WaitingForServerMissionEndEpisode extends ConfigAwareStateEpisode
    {
        protected WaitingForServerMissionEndEpisode(ClientStateMachine machine)
        {
            super(machine);
            MalmoMod.MalmoMessageHandler.registerForMessage(this, MalmoMessageType.SERVER_MISSIONOVER);
        }

        @Override
        protected void execute() throws Exception
        {
            // Get our name from the Mission:
            List<AgentSection> agents = currentMissionInit().getMission().getAgentSection();
            if (agents == null || agents.size() <= currentMissionInit().getClientRole())
                throw new Exception("No agent section for us!"); // TODO
            String agentName = agents.get(currentMissionInit().getClientRole()).getName();

            // Now send a message to the server saying that we are ready:
            HashMap<String, String> map = new HashMap<String, String>();
            map.put("agentname", agentName);
            MalmoMod.network.sendToServer(new MalmoMod.MalmoMessage(MalmoMessageType.CLIENT_AGENTSTOPPED, 0, map));
        }

        @Override
        public void onMessage(MalmoMessageType messageType, Map<String, String> data)
        {
            super.onMessage(messageType, data);
            if (messageType == MalmoMessageType.SERVER_MISSIONOVER)
                episodeHasCompleted(ClientState.DORMANT);
        }

        @Override
        public void cleanup()
        {
            super.cleanup();
            MalmoMod.MalmoMessageHandler.deregisterForMessage(this, MalmoMessageType.SERVER_MISSIONOVER);
        }

        @Override
        protected void onAbort(Map<String, String> errorData)
        {
            episodeHasCompleted(ClientState.MISSION_ABORTED);
        }
    }

    // ---------------------------------------------------------------------------------------------------------
    /**
     * Depending on the basemap provided, either begin to perform a full world
     * load, or reset the current world
     */
    public class EvaluateWorldRequirementsEpisode extends ConfigAwareStateEpisode
    {
        EvaluateWorldRequirementsEpisode(ClientStateMachine machine)
        {
            super(machine);
        }

        @Override
        protected void execute()
        {
            // We are responsible for creating the server, if required.
            // This means we need access to the server's MissionHandlers:
            MissionBehaviour serverHandlers = null;
            try
            {
                serverHandlers = MissionBehaviour.createServerHandlersFromMissionInit(currentMissionInit());
            }
            catch (Exception e)
            {
                episodeHasCompletedWithErrors(ClientState.ERROR_DUFF_HANDLERS, "Could not create server mission handlers: " + e.getMessage());
            }

            World world = null;
            if (Minecraft.getMinecraft().getIntegratedServer() != null)
                world = Minecraft.getMinecraft().getIntegratedServer().getEntityWorld();

            boolean needsNewWorld = serverHandlers != null && serverHandlers.worldGenerator != null && serverHandlers.worldGenerator.shouldCreateWorld(currentMissionInit(), world);
            boolean worldCurrentlyExists = world != null;
            if (worldCurrentlyExists)
            {
                // If a world already exists, we need to check that our requested agent name matches the name
                // of the player. If not, the safest thing to do is start a new server.
                // Get our name from the Mission:
                List<AgentSection> agents = currentMissionInit().getMission().getAgentSection();
                String agentName = agents.get(currentMissionInit().getClientRole()).getName();
                if (Minecraft.getMinecraft().player != null)
                {
                    if (!Minecraft.getMinecraft().player.getName().equals(agentName))
                        needsNewWorld = true;
                }
            }
            if (needsNewWorld && worldCurrentlyExists)
            {
                // We want a new world, and there is currently a world running,
                // so we need to kill the current world.
                episodeHasCompleted(ClientState.PAUSING_OLD_SERVER);
            }
            else if (needsNewWorld && !worldCurrentlyExists)
            {
                // We want a new world, and there is currently nothing running,
                // so jump to world creation:
                episodeHasCompleted(ClientState.CREATING_NEW_WORLD);
            }
            else if (!needsNewWorld && worldCurrentlyExists)
            {
                // We don't want a new world, and we can use the current one -
                // but we own the server, so we need to pass it the new mission init:
                Minecraft.getMinecraft().getIntegratedServer().addScheduledTask(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        try
                        {
                            MalmoMod.instance.sendMissionInitDirectToServer(currentMissionInit);
                        }
                        catch (Exception e)
                        {
                            episodeHasCompletedWithErrors(ClientState.ERROR_INTEGRATED_SERVER_UNREACHABLE, "Could not send MissionInit to our integrated server: " + e.getMessage());
                        }
                    }
                });
                // Skip all the map loading stuff and go straight to waiting for the server:
                episodeHasCompleted(ClientState.WAITING_FOR_SERVER_READY);
            }
            else if (!needsNewWorld && !worldCurrentlyExists)
            {
                // Mission has requested no new world, but there is no current world to play in - this is an error:
                episodeHasCompletedWithErrors(ClientState.ERROR_NO_WORLD, "We have no world to play in - check that your ServerHandlers section contains a world generator");
            }
        }
    }

    // ---------------------------------------------------------------------------------------------------------
    /**
     * Pause the old server. It's vital that we do this, otherwise it will
     * respond to the quit disconnect package straight away and kill the server
     * thread, which means there will be no server to respond to the loadWorld
     * code. (This was the cause of the infamous "Holder Lookups" hang.)
     */
    public class PauseOldServerEpisode extends ConfigAwareStateEpisode
    {
        int serverTickCount = 0;
        int clientTickCount = 0;
        int totalTicks = 0;

        PauseOldServerEpisode(ClientStateMachine machine)
        {
            super(machine);
        }

        @Override
        protected void execute()
        {
            serverTickCount = 0;
            clientTickCount = 0;
            totalTicks = 0;

            if (Minecraft.getMinecraft().getIntegratedServer() != null && Minecraft.getMinecraft().world != null)
            {
                // If the integrated server has been opened to the LAN, we won't be able to pause it.
                // To get around this, we need to make it think it's not open, by modifying its isPublic flag.
                if (Minecraft.getMinecraft().getIntegratedServer().getPublic())
                {
                    if (!killPublicFlag(Minecraft.getMinecraft().getIntegratedServer()))
                    {
                        // Can't pause, don't want to risk the hang - so bail.
                        episodeHasCompletedWithErrors(ClientState.ERROR_CANNOT_CREATE_WORLD, "Can not pause the old server since it's open to LAN; no way to safely create new world.");
                    }
                }

                Minecraft.getMinecraft().displayGuiScreen(new GuiIngameMenu());
            }
        }

        private boolean killPublicFlag(IntegratedServer server)
        {
            // Are we in a dev environment?
            boolean devEnv = (Boolean) Launch.blackboard.get("fml.deobfuscatedEnvironment");
            // We need to know, because the member name will either be obfuscated or not.
            String isPublicMemberName = devEnv ? "isPublic" : "field_71346_p";
            // NOTE: obfuscated name may need updating if Forge changes.
            Field isPublic;
            try
            {
                isPublic = IntegratedServer.class.getDeclaredField(isPublicMemberName);
                isPublic.setAccessible(true);
                isPublic.set(server, false);
                return true;
            }
            catch (SecurityException e)
            {
                e.printStackTrace();
            }
            catch (IllegalAccessException e)
            {
                e.printStackTrace();
            }
            catch (IllegalArgumentException e)
            {
                e.printStackTrace();
            }
            catch (NoSuchFieldException e)
            {
                e.printStackTrace();
            }
            return false;
        }

        @Override
        public void onClientTick(TickEvent.ClientTickEvent ev)
        {
            // Check to see whether anything has caused us to abort - if so, go to the abort state.
            if (inAbortState())
                episodeHasCompleted(ClientState.MISSION_ABORTED);

            // We need to make sure that both the client and server have paused,
            // otherwise we are still susceptible to the "Holder Lookups" hang.
            
            // Since the server sets its pause state in response to the client's pause state,
            // and it only performs this check once, at the top of its tick method,
            // to be sure that the server has had time to set the flag correctly we need to make sure
            // that at least one server tick method has *started* since the flag was set.
            // We can't do this by catching the onServerTick events, since we don't receive them when the game is paused.
            
            // The following code makes use of the fact that the server both locks and empties the server's futureQueue,
            // every time through the server tick method.
            // This locking means that if the client - which needs to wait on the lock -
            // tries to add an event to the queue in response to an event on the queue being executed,
            // the newly added event will have to happen in a subsequent tick.
            if ((Minecraft.getMinecraft().isGamePaused() || Minecraft.getMinecraft().player == null) && ev != null && ev.phase == Phase.END && this.clientTickCount == this.serverTickCount && this.clientTickCount <= 2)
            {
                this.clientTickCount++; // Increment our count, and wait for the server to catch up.
                Minecraft.getMinecraft().getIntegratedServer().addScheduledTask(new Runnable()
                {
                    public void run()
                    {
                        // Increment the server count.
                        PauseOldServerEpisode.this.serverTickCount++;
                    }
                });
            }

            if (this.serverTickCount > 2) {
                episodeHasCompleted(ClientState.CLOSING_OLD_SERVER);
            } else if (++totalTicks > WAIT_MAX_TICKS) {
                String msg = "Too long waiting for server episode to pause.";
                TCPUtils.Log(Level.SEVERE, msg);
                episodeHasCompletedWithErrors(ClientState.ERROR_TIMED_OUT_WAITING_FOR_EPISODE_PAUSE, msg);
            }
        }
    }

    // ---------------------------------------------------------------------------------------------------------
    /**
     * Send a disconnecting message to the current server - sent before attempting to load a new world.
     */
    public class CloseOldServerEpisode extends ConfigAwareStateEpisode
    {
        int totalTicks;

        CloseOldServerEpisode(ClientStateMachine machine)
        {
            super(machine);
        }

        @Override
        protected void execute()
        {
            totalTicks = 0;

            if (Minecraft.getMinecraft().world != null)
            {
                // If the Minecraft server isn't paused at this point,
                // then the following line will cause the server thread to exit...
                Minecraft.getMinecraft().world.sendQuittingDisconnectingPacket();
                // ...in which case the next line will hang.
                Minecraft.getMinecraft().loadWorld((WorldClient) null);
                // Must display the GUI or Minecraft will attempt to access a non-existent player in the client tick.
                Minecraft.getMinecraft().displayGuiScreen(new GuiMainMenu());

                // Allow shutdown messages to flow through.
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException ie) {
                }
            }
        }

        @Override
        public void onClientTick(TickEvent.ClientTickEvent ev)
        {
            // Check to see whether anything has caused us to abort - if so, go to the abort state.
            if (inAbortState())
                episodeHasCompleted(ClientState.MISSION_ABORTED);

            if (ev.phase == Phase.END)
                episodeHasCompleted(ClientState.CREATING_NEW_WORLD);

            if (++totalTicks > WAIT_MAX_TICKS)
            {
                String msg = "Too long waiting for server episode to close.";
                TCPUtils.Log(Level.SEVERE, msg);
                episodeHasCompletedWithErrors(ClientState.ERROR_TIMED_OUT_WAITING_FOR_EPISODE_CLOSE, msg);
            }
        }
    }

    // ---------------------------------------------------------------------------------------------------------
    /**
     * Attempt to create a world.
     */
    public class CreateWorldEpisode extends ConfigAwareStateEpisode
    {
        boolean serverStarted = false;
        boolean worldCreated = false;
        int totalTicks = 0;

        CreateWorldEpisode(ClientStateMachine machine)
        {
            super(machine);
        }

        @Override
        protected void execute()
        {
            try
            {
                totalTicks = 0;

                // We need to use the server's MissionHandlers here:
                MissionBehaviour serverHandlers = MissionBehaviour.createServerHandlersFromMissionInit(currentMissionInit());
                if (serverHandlers != null && serverHandlers.worldGenerator != null)
                {
                    if (serverHandlers.worldGenerator.createWorld(currentMissionInit()))
                    {
                        this.worldCreated = true;
                        if (Minecraft.getMinecraft().getIntegratedServer() != null)
                            Minecraft.getMinecraft().getIntegratedServer().setOnlineMode(false);
                    }
                    else
                    {
                        // World has not been created.
                        episodeHasCompletedWithErrors(ClientState.ERROR_CANNOT_CREATE_WORLD, "Server world-creation handler failed to create a world: " + serverHandlers.worldGenerator.getErrorDetails());
                    }
                }
            }
            catch (Exception e)
            {
                episodeHasCompletedWithErrors(ClientState.ERROR_CANNOT_CREATE_WORLD, "Server world-creation handler failed to create a world: " + e.getMessage());
            }
        }

        @Override
        protected void onServerTick(ServerTickEvent ev)
        {
            if (this.worldCreated && !this.serverStarted)
            {
                // The server has started ticking - we can set up its state machine,
                // and move on to the next state in our own machine.
                this.serverStarted = true;
                MalmoMod.instance.initIntegratedServer(currentMissionInit()); // Needs to be done from the server thread.
                episodeHasCompleted(ClientState.WAITING_FOR_SERVER_READY);
            }
        }

        @Override
        public void onClientTick(TickEvent.ClientTickEvent ev)
        {
            // Check to see whether anything has caused us to abort - if so, go to the abort state.
            if (inAbortState())
                episodeHasCompleted(ClientState.MISSION_ABORTED);

            if (++totalTicks > WAIT_MAX_TICKS)
            {
                String msg = "Too long waiting for world to be created.";
                TCPUtils.Log(Level.SEVERE, msg);
                episodeHasCompletedWithErrors(ClientState.ERROR_TIMED_OUT_WAITING_FOR_WORLD_CREATE, msg);
            }
        }
    }

    // ---------------------------------------------------------------------------------------------------------
    /**
     * State in which an agent has finished the mission, but is waiting for the server to draw stumps.
     */
    public class MissionIdlingEpisode extends ConfigAwareStateEpisode
    {
        int totalTicks = 0;

        protected MissionIdlingEpisode(ClientStateMachine machine)
        {
            super(machine);
            MalmoMod.MalmoMessageHandler.registerForMessage(this, MalmoMessageType.SERVER_STOPAGENTS);
        }

        @Override
        protected void execute()
        {
            totalTicks = 0;
            TimeHelper.SyncManager.numTicks = 0;
        }

        @Override
        public void onMessage(MalmoMessageType messageType, Map<String, String> data)
        {
            super.onMessage(messageType, data);
            // This message will be sent to us once the server has decided the mission is over.
            if (messageType == MalmoMessageType.SERVER_STOPAGENTS)
                episodeHasCompleted(ClientState.MISSION_ENDED);
        }

        @Override
        public void cleanup()
        {
            super.cleanup();
            MalmoMod.MalmoMessageHandler.deregisterForMessage(this, MalmoMessageType.SERVER_STOPAGENTS);
        }

        @Override
        public void onClientTick(TickEvent.ClientTickEvent ev)
        {
            // Check to see whether anything has caused us to abort - if so, go to the abort state.
            if (inAbortState())
                episodeHasCompleted(ClientState.MISSION_ABORTED);

            ++totalTicks;
        }
    }

    // ---------------------------------------------------------------------------------------------------------
    /**
     * State in which a mission is running.<br>
     * This state is ended by the death of the player or by the IWantToQuit
     * handler, or by the server declaring the mission is over.
     */
    public class MissionRunningEpisode extends ConfigAwareStateEpisode implements VideoProducedObserver
    {
        public static final int FailedTCPSendCountTolerance = 3; // Number of TCP timeouts before we cancel the mission

        protected MissionRunningEpisode(ClientStateMachine machine)
        {
            super(machine);
            MalmoMod.MalmoMessageHandler.registerForMessage(this, MalmoMessageType.SERVER_STOPAGENTS);
            MalmoMod.MalmoMessageHandler.registerForMessage(this, MalmoMessageType.SERVER_GO);
        }

        boolean serverHasFiredStartingPistol = false;
        boolean playerDied = false;
        private int failedTCPRewardSendCount = 0;
        private int failedTCPObservationSendCount = 0;
        private boolean wantsToQuit = false; // We have decided our mission is at an end
        private List<VideoHook> videoHooks = new ArrayList<VideoHook>();
        private String quitCode = "";
        private TCPSocketChannel observationSocket = null;
        private TCPSocketChannel rewardSocket = null;
        private long lastPingSent = 0;
        private long pingFrequencyMs = 1000;
        private boolean serverWantsToEndMission = false;
        private long frameTimestamp = 0;

        public void frameProduced() {
            this.frameTimestamp = System.currentTimeMillis();
        }

        protected void onMissionStarted()
        {
            frameTimestamp = 0;

            // Open our communication channels:
            openSockets();

            // Reset our statistics
            if (Minecraft.getMinecraft().getIntegratedServer() != null){
                for (EntityPlayerMP player : Minecraft.getMinecraft().getIntegratedServer().getPlayerList().playerEntityList) {
                    StatisticsManagerServer sfw = Objects.requireNonNull(Minecraft.getMinecraft().getIntegratedServer()).getPlayerList().getPlayerStatsFile(player);
                    sfw.readStatFile();
                }
            }

            this.serverWantsToEndMission = false;
            // Tell the server we have started:
            HashMap<String, String> map = new HashMap<String, String>();
            map.put("username", Minecraft.getMinecraft().player.getName());
            MalmoMod.network.sendToServer(new MalmoMod.MalmoMessage(MalmoMessageType.CLIENT_AGENTRUNNING, 0, map));

            // Set up our mission handlers:
            if (currentMissionBehaviour().commandHandler != null)
            {
                currentMissionBehaviour().commandHandler.install(currentMissionInit());
                currentMissionBehaviour().commandHandler.setOverriding(true);
            }

            if (currentMissionBehaviour().observationProducer != null)
                currentMissionBehaviour().observationProducer.prepare(currentMissionInit());

            if (currentMissionBehaviour().quitProducer != null)
                currentMissionBehaviour().quitProducer.prepare(currentMissionInit());

            if (currentMissionBehaviour().rewardProducer != null)
                currentMissionBehaviour().rewardProducer.prepare(currentMissionInit());

            if (currentMissionBehaviour().performanceProducer != null)
                currentMissionBehaviour().performanceProducer.prepare(currentMissionInit());


            // Graphics Settings
            Minecraft.getMinecraft().gameSettings.gammaSetting = missionBehaviour.gammaSetting;
            Minecraft.getMinecraft().gameSettings.guiScale = missionBehaviour.guiScale;
            Minecraft.getMinecraft().gameSettings.fovSetting = missionBehaviour.fovSetting;

            if (missionBehaviour.lowLevelInputs) {
                Minecraft.getMinecraft().gameSettings.hideGUI = false;
                Minecraft.getMinecraft().gameSettings.fancyGraphics = true;
            } else {
                // Disable the gui for the episode!
                Minecraft.getMinecraft().gameSettings.hideGUI = true;
            }

            for (IVideoProducer videoProducer : currentMissionBehaviour().videoProducers)
            {
                VideoHook hook = new VideoHook();
                this.videoHooks.add(hook);
                frameProduced();
                hook.start(currentMissionInit(), videoProducer, this, envServer);
            }

            // Make sure we have mouse control:
            ClientStateMachine.this.inputController.setInputType(InputType.AI);
            Minecraft.getMinecraft().inGameHasFocus = true; // Otherwise auto-repeat won't work for mouse clicks.

            // Overclocking:
            ModSettings modsettings = currentMissionInit().getMission().getModSettings();
            if (modsettings != null) {
                if (modsettings.getMsPerTick() != null)
                TimeHelper.setMinecraftClientClockSpeed(1000 / modsettings.getMsPerTick());
                if (modsettings.isPrioritiseOffscreenRendering() == Boolean.TRUE)
                TimeHelper.displayGranularityMs = 1000;
                if (modsettings.getFrameSkip() != null)
                    TimeHelper.frameSkip = modsettings.getFrameSkip();
            }
            TimeHelper.unpause();

            // smelting recipes with regular coal (standard recipes produce coal:1 or charcoal)
            removeSmeltingRecipe(Blocks.LOG);
            removeSmeltingRecipe(Blocks.LOG2);
            // TODO: Move this somewhere else. This is the wrong place.
            FurnaceRecipes.instance().addSmeltingRecipeForBlock(Blocks.LOG, new ItemStack(Items.COAL), 0.15F);
            FurnaceRecipes.instance().addSmeltingRecipeForBlock(Blocks.LOG2, new ItemStack(Items.COAL), 0.15F);

            // Synchronization
            if (envServer != null){
                if(!envServer.doIWantToQuit(currentMissionInit())){

                    // TimeHelper.SyncManager.debugLog(" SETTING SYNCHRONOUS  IN CLIENT STATEMACHINE ON MISSION STARTED");
                    TimeHelper.SyncManager.setSynchronous(envServer.isSynchronous());
                    // TimeHelper.SyncManager.debugLog(" SYNCHRONOUS SET TO " + envServer.isSynchronous());
                } else {
                    // TimeHelper.SyncManager.debugLog(" SETTING SYNCHRONOUS  IN CLIENT STATEMACHINE ON MISSION STARTED");
                    TimeHelper.SyncManager.setSynchronous(false);
                    // TimeHelper.SyncManager.debugLog(" SYNCHRONOUS SET TO " + envServer.isSynchronous());
                }
            }
        }

        private void removeSmeltingRecipe(Block block) {
            Item keyToRemove = Item.getItemFromBlock(block);
            for (Map.Entry<ItemStack, ItemStack> e : FurnaceRecipes.instance().getSmeltingList().entrySet()) {
                if (keyToRemove.unlocalizedName.equals(e.getKey().getItem().unlocalizedName)) {
                    FurnaceRecipes.instance().getSmeltingList().remove(e.getKey());
                    break;
                }
            }
        }

        protected void onMissionEnded(IState nextState, String errorReport) {
            onMissionEnded(nextState, errorReport, true);
        }

        protected void onMissionEnded(IState nextState, String errorReport, boolean worldStillExists)
        {
            //Send the final data associated with the misson here.
            TimeHelper.SyncManager.debugLog("[CLIENT_STATE_MACHINE] Mission ended for reason: " + errorReport + " world still exists: " + Boolean.toString(worldStillExists));
            this.serverWantsToEndMission = false;
            sendData(true, worldStillExists);

            // Tidy up our mission handlers:
            if (currentMissionBehaviour().rewardProducer != null)
                currentMissionBehaviour().rewardProducer.cleanup();

            if (currentMissionBehaviour().quitProducer != null)
                currentMissionBehaviour().quitProducer.cleanup();

            if (currentMissionBehaviour().observationProducer != null)
                currentMissionBehaviour().observationProducer.cleanup();

            if (currentMissionBehaviour().commandHandler != null)
            {
                currentMissionBehaviour().commandHandler.setOverriding(false);
                currentMissionBehaviour().commandHandler.deinstall(currentMissionInit());
            }

            if (AddressHelper.getMissionControlPort() == 0) {
                if (envServer != null) {
                    byte[] obs = envServer.getObservation(false);
                    envServer.endMission();
                }
            }

            // Close our communication channels:
            closeSockets();

            for (VideoHook hook : this.videoHooks)
                hook.stop(ClientStateMachine.this.missionEndedData);

            
            // Disable the gui for the episode!
            Minecraft.getMinecraft().gameSettings.hideGUI = false;

            // Return Minecraft speed to "normal":
            TimeHelper.SyncManager.setPistolFired(false);
            TimeHelper.setMinecraftClientClockSpeed(20);
            TimeHelper.displayGranularityMs = 0;
            TimeHelper.unpause();

            ClientStateMachine.this.missionQuitCode = this.quitCode;
            if (errorReport != null)
                episodeHasCompletedWithErrors(nextState, errorReport);
            else
                episodeHasCompleted(nextState);
        }

        @Override
        protected void execute()
        {
            onMissionStarted();
        }

        @Override
        public void onClientTick(ClientTickEvent event)
        {
            // If we aren't performing synchronous ticking use the client Tick to handle updates
            if(!TimeHelper.SyncManager.isSynchronous()){
                onTick(false, event.phase);
            }
        }

        @Override
        public void onSyncTick(SyncTickEvent ev){
            // If we are performing synchronous ticking
            onTick(true, ev.pos);
        }

        private synchronized void onTick(Boolean synchronous, TickEvent.Phase phase){
            // TimeHelper.SyncManager.debugLog("[CLIENT_STATE_MACHINE] <onTICK> " + phase.toString());
            // Check to see whether anything has caused us to abort - if so, go to the abort state.
            if (inAbortState())
                onMissionEnded(ClientState.MISSION_ABORTED, "Mission was aborted by server: " + ClientStateMachine.this.getErrorDetails());
            // Check to see whether we've been kicked from the server.
            NetHandlerPlayClient npc = Minecraft.getMinecraft().getConnection();
            if(npc == null && phase == Phase.START){
                if(this.serverHasFiredStartingPistol){
                    onMissionEnded(ClientState.ERROR_LOST_NETWORK_CONNECTION, "Server was closed", false);
                    return;
                }
            }
            else if (phase == Phase.START){
                NetworkManager netman = npc.getNetworkManager();
                if (netman != null && !netman.hasNoChannel() && !netman.isChannelOpen())
                {
                    // Connection has been lost.
                    onMissionEnded(ClientState.ERROR_LOST_NETWORK_CONNECTION, "Client was kicked from server - ", false);
                }
    
            }

            // Check we are still in touch with the agent:
            if (System.currentTimeMillis() > this.lastPingSent + this.pingFrequencyMs)
            {
                this.lastPingSent = System.currentTimeMillis();
                // Ping the agent - if serverHasFiredStartingPistol is true, we don't need to abort -
                // we can simply set the wantsToQuit flag and end the mission cleanly.
                // If serverHasFiredStartingPistol is false, then the mission isn't yet running, and
                // setting the quit flag will do nothing - so we need to abort.
                if (!pingAgent(false))
                {
                    if (!this.serverHasFiredStartingPistol){
                        onMissionEnded(ClientState.ERROR_LOST_AGENT, "Lost contact with the agent");
                        return;
                    }
                    else
                    {
                        System.out.println("Error - agent is not responding to pings.");
                        this.wantsToQuit = true;
                        this.quitCode = MalmoMod.AGENT_UNRESPONSIVE_CODE;
                    }
                }
            }


            if (this.frameTimestamp != 0 && (System.currentTimeMillis() - this.frameTimestamp >  VIDEO_MAX_WAIT) && !synchronous) {
                System.out.println("No video produced recently. Aborting mission.");
                if (!this.serverHasFiredStartingPistol)
                    onMissionEnded(ClientState.ERROR_LOST_VIDEO, "No video produced recently.");
                else
                {
                    System.out.println("Error - not receiving video.");
                    this.wantsToQuit = true;
                    this.quitCode = MalmoMod.VIDEO_UNRESPONSIVE_CODE;
                }
            }
            
            if(Minecraft.getMinecraft().world == null){
                if(this.serverHasFiredStartingPistol){
                    onMissionEnded(ClientState.ERROR_NO_WORLD, "No world for client. Must be in main menu");  
                }

                return;   
            
            }
            // Check here to see whether the player has died or not:
            if (!this.playerDied && Minecraft.getMinecraft().player.isDead)
            {
                this.playerDied = true;
                this.quitCode = MalmoMod.AGENT_DEAD_QUIT_CODE;
            }

            // Although we only arrive in this episode once the server has determined that all clients are ready to go,
            // the server itself waits for all clients to begin running before it enters the running state itself.
            // This creates a small vulnerability, since a running client could theoretically *finish* its mission
            // before the server manages to *start*.
            // (This has potentially disastrous effects for the state machine, and is easy to reproduce by,
            // for example, setting the start point and goal of the mission to the same coordinates.)
            
            // To guard against this happening, although we are running, we don't act on anything -
            // we don't check for commands, or send observations or rewards - until we get the SERVER_GO signal,
            // which is sent once the server's running episode has started.

            
            TimeHelper.SyncManager.setPistolFired(this.serverHasFiredStartingPistol);
            if (!this.serverHasFiredStartingPistol){
                return;
            }

            // Perhaps the race condition could be that synchronous is then set to false when the quit command is received!
            if(synchronous && phase == Phase.START){
                checkForControlCommand();
            }
            if (phase == Phase.END)
            {   


                // Check whether or not we want to quit:
                IWantToQuit quitHandler = (currentMissionBehaviour() != null) ? currentMissionBehaviour().quitProducer : null;
                boolean quitHandlerFired = (quitHandler != null && quitHandler.doIWantToQuit(currentMissionInit()));
                // Don't quit on death if there are multiple agents:
                //   1/ Perhaps we want to continue with the surviving agents
                //   2/ If we decide to quit without the other clients knowing, it can mess things up and cause crashes
                boolean quitOnDeath = this.playerDied && currentMissionInit().getMission().getAgentSection().size() == 1;
                if (quitHandlerFired || this.wantsToQuit || quitOnDeath || this.serverWantsToEndMission)
                {
                    if (quitHandlerFired)
                    {
                        this.quitCode = quitHandler.getOutcome();
                    }
                    try
                    {
                        // Save the quit code for anything that needs it:
                        MalmoMod.getPropertiesForCurrentThread().put("QuitCode", this.quitCode);
                    }
                    catch (Exception e)
                    {
                        System.out.println("Failed to get properties - final reward may go missing.");
                    }

                    // Get the final reward data:
                    ClientAgentConnection cac = currentMissionInit().getClientAgentConnection();
                    // if (currentMissionBehaviour() != null && currentMissionBehaviour().rewardProducer != null && cac != null)
                    //     currentMissionBehaviour().rewardProducer.getReward(currentMissionInit(), ClientStateMachine.this.finalReward);

                    // Now send a message to the server saying that we have finished our mission:
                    List<AgentSection> agents = currentMissionInit().getMission().getAgentSection();
                    String agentName = agents.get(currentMissionInit().getClientRole()).getName();
                    HashMap<String, String> map = new HashMap<String, String>();
                    map.put("agentname", agentName);
                    map.put("username", Minecraft.getMinecraft().player.getName());
                    map.put("quitcode", this.quitCode);

                    if(this.serverWantsToEndMission){
                        onMissionEnded(ClientState.MISSION_ENDED, null);
                    } else{
                        // If we terminated on our own accord go to idling
                        onMissionEnded(ClientState.IDLING, null);

                        // TODO: ONE AGENT HARDCODED UID?
                        MalmoMod.network.sendToServer(new MalmoMod.MalmoMessage(MalmoMessageType.CLIENT_AGENTFINISHEDMISSION, 0, map));
                    }
                }
                else
                {
                    // If in the case that we are asynchronous, do this 
                    // wack stuff of checking input at the end of a tick...
                    if(!synchronous){

                        checkForControlCommand();
                    }
                    
                    // Send off observation and reward data:
                    // And see if we have any incoming commands to act upon:
                    sendData(false);
                }
            }
        }

        private void openSockets()
        {
            ClientAgentConnection cac = currentMissionInit().getClientAgentConnection();
            this.observationSocket = new TCPSocketChannel(cac.getAgentIPAddress(), cac.getAgentObservationsPort(), "obs");
            this.rewardSocket = new TCPSocketChannel(cac.getAgentIPAddress(), cac.getAgentRewardsPort(), "rew");
        }

        private void closeSockets()
        {
            this.observationSocket.close();
            this.rewardSocket.close();
        }

        private void sendData(boolean done) {
            sendData(done, true);
        }

        private void sendData(boolean done, boolean worldstillExists)
        {
            TCPUtils.LogSection ls = new TCPUtils.LogSection("Sending data");

            Minecraft.getMinecraft().mcProfiler.startSection("malmoSendData");
            // Create the observation data:
            String data = "";
            Minecraft.getMinecraft().mcProfiler.startSection("malmoGatherObservationJSON");

            if (!worldstillExists) {
                if (envServer != null) {
                    envServer.usePreviousState();
                }
                return;
            }

            if (currentMissionBehaviour() != null && currentMissionBehaviour().observationProducer != null)
            {
                JsonObject json = new JsonObject();
                currentMissionBehaviour().observationProducer.writeObservationsToJSON(json, currentMissionInit());
                data = json.toString();
            }
            Minecraft.getMinecraft().mcProfiler.endSection(); //malmogatherjson
            Minecraft.getMinecraft().mcProfiler.startSection("malmoSendTCPObservations");

            ClientAgentConnection cac = currentMissionInit().getClientAgentConnection();

            if (data != null && data.length() > 2 && cac != null) // An empty json string will be "{}" (length 2) - don't send these.
            {
                // TimeHelper.SyncManager.debugLog("[CLIENT_STATE_MACHINE INFO] " + Integer.toString(AddressHelper.getMissionControlPort()));
                if (AddressHelper.getMissionControlPort() == 0) {
                    if (envServer != null) {
                        // TODO weird, aren't we doing this?
                        envServer.observation(data);
                    }
                } else {
                    // Bung the whole shebang off via TCP:
                    if (this.observationSocket.sendTCPString(data)) {
                        this.failedTCPObservationSendCount = 0;
                    } else {
                        // Failed to send observation message.
                        this.failedTCPObservationSendCount++;
                        TCPUtils.Log(Level.WARNING, "Observation signal delivery failure count at " + this.failedTCPObservationSendCount);
                        ClientStateMachine.this.getScreenHelper().addFragment("ERROR: Agent missed observation signal", TextCategory.TXT_CLIENT_WARNING, 5000);
                    }
                }
            }
            Minecraft.getMinecraft().mcProfiler.endSection(); //malmotcp
            Minecraft.getMinecraft().mcProfiler.startSection("malmoGatherRewardSignal");
            
            // Now create the reward signal:
            if (currentMissionBehaviour() != null && currentMissionBehaviour().rewardProducer != null && cac != null)
            {
                MultidimensionalReward reward = new MultidimensionalReward();
                currentMissionBehaviour().rewardProducer.getReward(currentMissionInit(), reward);
                
                if (!reward.isEmpty())
                {

                    String strReward = reward.getAsSimpleString();
                    Minecraft.getMinecraft().mcProfiler.startSection("malmoSendTCPReward");

                    ScoreHelper.logReward(strReward);

                    if (AddressHelper.getMissionControlPort() == 0) {
                        // MalmoEnvServer - reward
                        if (envServer != null) {
                            envServer.addRewards(reward.getRewardTotal());
                        }
                    } else {
                        if (this.rewardSocket.sendTCPString(strReward)) {
                            this.failedTCPRewardSendCount = 0; // Reset the count of consecutive TCP failures.
                        } else {
                            // Failed to send TCP message - probably because the agent has quit under our feet.
                            // (This happens a lot when developing a Python agent - the developer has no easy way to quit
                            // the agent cleanly, so tends to kill the process.)
                            this.failedTCPRewardSendCount++;
                            TCPUtils.Log(Level.WARNING, "Reward signal delivery failure count at " + this.failedTCPRewardSendCount);
                            ClientStateMachine.this.getScreenHelper().addFragment("ERROR: Agent missed reward signal", TextCategory.TXT_CLIENT_WARNING, 5000);
                        }
                    }

                    Minecraft.getMinecraft().mcProfiler.endSection(); //sendTCP reward.
                }
                if (currentMissionBehaviour().performanceProducer != null)
                    currentMissionBehaviour().performanceProducer.step(reward.getRewardTotal(), done);
            }
            else if(currentMissionBehaviour() != null){
                if (currentMissionBehaviour().performanceProducer != null)
                currentMissionBehaviour().performanceProducer.step(0, done);
            }
            Minecraft.getMinecraft().mcProfiler.endSection(); //Gather reward.
            Minecraft.getMinecraft().mcProfiler.endSection(); //sendData

            int maxFailedTCPSendCount = 0;
            for (VideoHook hook : this.videoHooks)
            {
                if (hook.failedTCPSendCount > maxFailedTCPSendCount)
                    maxFailedTCPSendCount = hook.failedTCPSendCount;
            }
            if (maxFailedTCPSendCount > 0)
                TCPUtils.Log(Level.WARNING, "Video signal failure count at " + maxFailedTCPSendCount);
            // Check that our messages are getting through:
            int maxFailed = Math.max(this.failedTCPRewardSendCount, maxFailedTCPSendCount);
            maxFailed = Math.max(maxFailed, this.failedTCPObservationSendCount);
            if (maxFailed > FailedTCPSendCountTolerance)
            {
                // They're not - and we've exceeded the count of allowed TCP failures.
                System.out.println("ERROR: TCP messages are not getting through - quitting mission.");
                this.wantsToQuit = true;
                this.quitCode = MalmoMod.AGENT_UNRESPONSIVE_CODE;
            }
            ls.close();
        }

        /**
         * Check to see if any control instructions have been received and act on them if so.
         */
        public void checkForControlCommand()
        {
            Minecraft.getMinecraft().mcProfiler.endStartSection("malmoCommandHandling");
            String command;
            boolean quitHandlerFired = false;
            IWantToQuit quitHandler = (currentMissionBehaviour() != null) ? currentMissionBehaviour().quitProducer : null;

            if (envServer != null) {
                command = envServer.getCommand();
            } else {
                command = ClientStateMachine.this.controlInputPoller.getCommand();
            }
            while (command != null && command.length() > 0 && !quitHandlerFired)
            {
                // TCPUtils.Log(Level.INFO, "Act on " + command);
                // Pass the command to our various control overrides:
                Minecraft.getMinecraft().mcProfiler.startSection("malmoCommandAct");

                boolean handled = handleCommand(command);
                // Get the next command:
                if (envServer != null) {
                    command = envServer.getCommand();
                } else {
                    command = ClientStateMachine.this.controlInputPoller.getCommand();
                }
                // If there *is* another command (commands came in faster than one per client tick),
                // then we should check our quit producer before deciding whether to execute it.
                Minecraft.getMinecraft().mcProfiler.endStartSection("malmoCommandRecheckQuitHandlers");
                if (command != null && command.length() > 0 && handled)
                    quitHandlerFired = (quitHandler != null && quitHandler.doIWantToQuit(currentMissionInit()));
                Minecraft.getMinecraft().mcProfiler.endSection();
            }
        }

        /**
         * Attempt to handle a command string by passing it to our various external controllers in turn.
         * 
         * @param command the command string to be handled.
         * @return true if the command was handled.
         */
        private boolean handleCommand(String command)
        {
            if (currentMissionBehaviour() != null && currentMissionBehaviour().commandHandler != null)
            {
                return currentMissionBehaviour().commandHandler.execute(command, currentMissionInit());
            }
            return false;
        }

        @Override
        public void onMessage(MalmoMessageType messageType, Map<String, String> data)
        {
            super.onMessage(messageType, data);
            // This message will be sent to us once the server has decided the mission is over.
            if (messageType == MalmoMessageType.SERVER_STOPAGENTS)
            {
                this.quitCode = data.containsKey("QuitCode") ? data.get("QuitCode") : "";
                try
                {
                    // Save the quit code for anything that needs it:
                    MalmoMod.getPropertiesForCurrentThread().put("QuitCode", this.quitCode);
                }
                catch (Exception e)
                {
                    System.out.println("Failed to get properties - final reward may go missing.");
                }
                // Get the final reward data:
                ClientAgentConnection cac = currentMissionInit().getClientAgentConnection();
                if (currentMissionBehaviour() != null && currentMissionBehaviour().rewardProducer != null && cac != null)
                    currentMissionBehaviour().rewardProducer.getReward(currentMissionInit(), ClientStateMachine.this.finalReward);

                this.serverWantsToEndMission = true;
                
            }
            else if (messageType == MalmoMessageType.SERVER_GO)
            {
                // First, force all entities to get re-added to their chunks, clearing out any old entities in the process.
                // We need to do this because the process of teleporting all agents to their start positions, combined
                // with setting them to/from spectator mode, leaves the client chunk entity lists etc in a parlous state.
                List lel = Minecraft.getMinecraft().world.loadedEntityList;
                for (int i = 0; i < lel.size(); i++)
                {
                    Entity entity = (Entity)lel.get(i);
                    Chunk chunk = Minecraft.getMinecraft().world.getChunkFromChunkCoords(entity.chunkCoordX, entity.chunkCoordZ);
                    List<Entity> entitiesToRemove = new ArrayList<Entity>();
                    for (int k = 0; k < chunk.getEntityLists().length; k++)
                    {
                        Iterator iterator = chunk.getEntityLists()[k].iterator();
                        while (iterator.hasNext())
                        {
                            Entity chunkent = (Entity)iterator.next();
                            if (chunkent.getEntityId() == entity.getEntityId())
                            {
                                entitiesToRemove.add(chunkent);
                            }
                        }
                    }
                    for (Entity removeEnt : entitiesToRemove)
                    {
                        chunk.removeEntity(removeEnt);
                    }
                    entity.addedToChunk = false;    // Will force it to get re-added to the chunk list.
                    if (entity instanceof EntityLivingBase)
                    {
                        // If we want the entities to be rendered with the correct yaw from the outset,
                        // we need to set their render offset manually.
                        // (Set the offset from the outset to avoid the onset of upset.)
                        ((EntityLivingBase)entity).renderYawOffset = entity.rotationYaw;
                        ((EntityLivingBase)entity).prevRenderYawOffset = entity.rotationYaw;
                    }
                    if (entity instanceof EntityPlayerSP)
                    {
                        // Although the following call takes place on the server, and should have taken effect already,
                        // there is some discontinuity which is causing the effects to get lost, so we call it here too:
                        entity.setInvisible(false);
                    }
                }
                this.serverHasFiredStartingPistol = true; // GO GO GO!
            }
        }

        @Override
        public void cleanup()
        {
            super.cleanup();
            MalmoMod.MalmoMessageHandler.deregisterForMessage(this, MalmoMessageType.SERVER_STOPAGENTS);
            MalmoMod.MalmoMessageHandler.deregisterForMessage(this, MalmoMessageType.SERVER_GO);
        }
    }

    // ---------------------------------------------------------------------------------------------------------
    /**
     * State that occurs at the end of the mission, whether due to death,
     * failure, success, error, or whatever.
     */
    public class MissionEndedEpisode extends ConfigAwareStateEpisode
    {
        private MissionResult result;
        private boolean aborting;
        private boolean informServer;
        private boolean informAgent;
        private int totalTicks = 0;

        public MissionEndedEpisode(ClientStateMachine machine, MissionResult mr, boolean aborting, boolean informServer, boolean informAgent)
        {
            super(machine);
            this.result = mr;
            this.aborting = aborting;
            this.informServer = informServer;
            this.informAgent = informAgent;
        }

        @Override
        protected void execute()
        {
            totalTicks = 0;

            // Get a text report:
            String errorFeedback = ClientStateMachine.this.getErrorDetails();
            String quitFeedback = ClientStateMachine.this.missionQuitCode;
            String concatenation = (errorFeedback != null && !errorFeedback.isEmpty() && quitFeedback != null && !quitFeedback.isEmpty()) ? ";\n" : "";
            String report = quitFeedback + concatenation + errorFeedback;

            if (this.informServer)
            {
                // Inform the server of what has happened.
                HashMap<String, String> map = new HashMap<String, String>();
                if (Minecraft.getMinecraft().player != null) // Might not be a player yet.
                    map.put("username", Minecraft.getMinecraft().player.getName());
                map.put("error", ClientStateMachine.this.getErrorDetails());
                MalmoMod.network.sendToServer(new MalmoMod.MalmoMessage(MalmoMessageType.CLIENT_BAILED, 0, map));
            }

            if (this.informAgent)
            {
                // Create a MissionEnded instance for this result:
                MissionEnded missionEnded = new MissionEnded();
                missionEnded.setStatus(this.result);
                if (ClientStateMachine.this.missionQuitCode != null && ClientStateMachine.this.missionQuitCode.equals(MalmoMod.AGENT_DEAD_QUIT_CODE))
                    missionEnded.setStatus(MissionResult.PLAYER_DIED); // Need to do this manually.
                missionEnded.setHumanReadableStatus(report);
                
                // TODO: WE HAVE TO MOVE THIS TO THE onMISSIONENDED of Client Mission
                // BECAUSE IT WOULD TAKE AN EXTRA TICK TO HAVE THIS APPEAR PROPERLY.
                // THIS MOVE IS INCOMPATIBLE WITH MULTIPLE AGENTS AND REWARD DISTRIBUTION
                // A PROPER REHAUL OF THE WHOLE SIMULATOR TO SUPPORT SYNCHRONOUS TICKING
                // ACROSS MULTIPLE AGENTS AND A STATE MACHINE WHOSE STATE CHANGES INDEPENDENT
                // OF CLIENT TICKS IS REQUIRED.
                // if (!ClientStateMachine.this.finalReward.isEmpty())
                // {
                //     if (envServer != null) {
                //         envServer.addRewards(ClientStateMachine.this.finalReward.getRewardTotal());
                //     }
                //     missionEnded.setReward(ClientStateMachine.this.finalReward.getAsReward());
                //     ClientStateMachine.this.finalReward.clear();
                // }
                missionEnded.setMissionDiagnostics(ClientStateMachine.this.missionEndedData);	// send our diagnostics
                ClientStateMachine.this.missionEndedData = new MissionDiagnostics();			// and clear them for the next mission
                // And send MissionEnded message to the agent to inform it that the mission has ended:
                System.out.println("inform the agent");
                sendMissionEnded(missionEnded);
            }

            if (this.aborting) // Take the shortest path back to dormant.
                episodeHasCompleted(ClientState.DORMANT);
        }

        private void sendMissionEnded(MissionEnded missionEnded)
        {
            // Send a MissionEnded message to the agent to inform it that the mission has ended.
            // Create a string XML representation:
            String missionEndedString = null;
            try
            {
                missionEndedString = SchemaHelper.serialiseObject(missionEnded, MissionEnded.class);
                if (ScoreHelper.isScoring()) {
                    Reward reward = missionEnded.getReward();
                    if (reward == null) {
                        reward = new Reward();
                    }
                    ScoreHelper.logMissionEndRewards(reward);
                }
            }
            catch (JAXBException e)
            {
                TCPUtils.Log(Level.SEVERE, "Failed mission end XML serialization: " + e);
            }

            boolean sentOkay = false;
            if (missionEndedString != null)
            {
                if (AddressHelper.getMissionControlPort() == 0) {
                    sentOkay = true;
                } else {
                    TCPSocketChannel sender = ClientStateMachine.this.getMissionControlSocket();
                    System.out.println(String.format("Sending mission ended message to %s:%d.", sender.getAddress(), sender.getPort()));
                    sentOkay = sender.sendTCPString(missionEndedString);
                    sender.close();
                }
            }

            if (!sentOkay)
            {
                // Couldn't formulate a reply to the agent - bit of a problem.
                // Can't do much to alert the agent itself,
                // will have to settle for alerting anyone who is watching the mod:
                ClientStateMachine.this.getScreenHelper().addFragment("ERROR: Could not send mission ended message - agent may need manually resetting.", TextCategory.TXT_CLIENT_WARNING, 10000);
            }
        }

        @Override
        public void onClientTick(ClientTickEvent event)
        {
            if (!this.aborting)
                episodeHasCompleted(ClientState.WAITING_FOR_SERVER_MISSION_END);

            if (++totalTicks > WAIT_MAX_TICKS)
            {
                String msg = "Too long waiting for server to end mission.";
                TCPUtils.Log(Level.SEVERE, msg);
                episodeHasCompletedWithErrors(ClientState.ERROR_TIMED_OUT_WAITING_FOR_MISSION_END, msg);
            }
        }
    }
}
