Files

2485 lines
113 KiB
Java

// --------------------------------------------------------------------------------------------------
// 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.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import javax.xml.bind.JAXBException;
import javax.xml.stream.XMLStreamException;
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.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.launchwrapper.Launch;
import net.minecraft.network.NetworkManager;
import net.minecraft.server.integrated.IntegratedServer;
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.AgentSection;
import com.microsoft.Malmo.Schemas.AgentStart;
import com.microsoft.Malmo.Schemas.ClientAgentConnection;
import com.microsoft.Malmo.Schemas.MinecraftServerConnection;
import com.microsoft.Malmo.Schemas.Mission;
import com.microsoft.Malmo.Schemas.MissionDiagnostics;
import com.microsoft.Malmo.Schemas.MissionEnded;
import com.microsoft.Malmo.Schemas.MissionInit;
import com.microsoft.Malmo.Schemas.MissionResult;
import com.microsoft.Malmo.Schemas.Reward;
import com.microsoft.Malmo.Schemas.ModSettings;
import com.microsoft.Malmo.Schemas.PosAndDirection;
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
{
// AOG - Dropped from 2000 to 1000 to speed up detection of failed server restarts
private static final int WAIT_MAX_TICKS = 1000; // 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.substring(0, Math.min(command.length(), 1024)));
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);
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();
}
@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);
// If the player's profile properties are empty, MC will keep pinging the Minecraft session service
// to fill them, resulting in multiple http requests and grumpy responses from the server
// (see https://github.com/Microsoft/malmo/issues/568).
// To prevent this, we add a dummy property.
Minecraft.getMinecraft().getProfileProperties().put("dummy", new Property("dummy", "property"));
// 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);
// AOG - If we have timed out waiting for the server to be ready, then the
// MalmoEnvServer is also likely stuck trying to handle a peek request from
// Python client. We need to signal the env server should abort the request
// so that the client detects the error and can retry.
if (envServer != null) {
envServer.abort();
}
episodeHasCompletedWithErrors(ClientState.ERROR_TIMED_OUT_WAITING_FOR_EPISODE_START, msg);
}
}
@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);
}
this.waitingForPlayer = false;
}
}
protected void handleLan()
{
// 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 - make sure the server is open to the LAN:
{
MinecraftServerConnection msc = new MinecraftServerConnection();
String address = currentMissionInit().getClientAgentConnection().getClientIPAddress();
// Do we need to open to LAN?
if (Minecraft.getMinecraft().isSingleplayer() && !Minecraft.getMinecraft().getIntegratedServer().getPublic())
{
String portstr = Minecraft.getMinecraft().getIntegratedServer().shareToLAN(GameType.SURVIVAL, true); // Set to true to stop spam kicks.
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 shouldMissionEnd = false;
private long frameTimestamp = 0;
public void frameProduced() {
this.frameTimestamp = System.currentTimeMillis();
}
protected void onMissionStarted()
{
frameTimestamp = 0;
// Open our communication channels:
openSockets();
this.shouldMissionEnd = 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());
// 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 && modsettings.getMsPerTick() != null)
TimeHelper.setMinecraftClientClockSpeed(1000 / modsettings.getMsPerTick());
if (modsettings != null && modsettings.isPrioritiseOffscreenRendering() == Boolean.TRUE)
TimeHelper.displayGranularityMs = 1000;
TimeHelper.unpause();
// Synchronization
if (envServer != null){
if(!envServer.doIWantToQuit(currentMissionInit())){
TimeHelper.SyncManager.setSynchronous(envServer.isSynchronous());
} else {
TimeHelper.SyncManager.setSynchronous(false);
}
}
}
protected void onMissionEnded(IState nextState, String errorReport)
{
//Send the final data associated with the misson here.
this.shouldMissionEnd = false;
sendData(true);
// 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();
TimeHelper.SyncManager.setSynchronous(false);
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){
if(this.serverHasFiredStartingPistol){
onMissionEnded(ClientState.ERROR_LOST_NETWORK_CONNECTION, "Server was closed");
return;
}
}
else{
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 - " + netman.getExitMessage().getUnformattedText());
}
}
// 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 recieved!
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()));
if (quitHandlerFired || this.wantsToQuit || this.playerDied || this.shouldMissionEnd)
{
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);
MalmoMod.network.sendToServer(new MalmoMod.MalmoMessage(MalmoMessageType.CLIENT_AGENTFINISHEDMISSION, 0, map));
onMissionEnded(ClientState.MISSION_ABORTED, null);
}
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)
{
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 (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 wierd, 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.shouldMissionEnd = 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 SUPPROT SYNCHRONOUS TICKING
// ACCROSS 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);
}
}
}
}