From de6a6968027524586b3ed050aa778e6130a718b4 Mon Sep 17 00:00:00 2001 From: davey Date: Tue, 7 Jun 2016 10:54:30 +0100 Subject: [PATCH] Added ability to write strings longer than 64k in message data (issue #52) --- .../java/com/microsoft/Malmo/MalmoMod.java | 557 ++++++++++-------- 1 file changed, 298 insertions(+), 259 deletions(-) diff --git a/Minecraft/src/main/java/com/microsoft/Malmo/MalmoMod.java b/Minecraft/src/main/java/com/microsoft/Malmo/MalmoMod.java index 3d7c59a36..1afc2af33 100755 --- a/Minecraft/src/main/java/com/microsoft/Malmo/MalmoMod.java +++ b/Minecraft/src/main/java/com/microsoft/Malmo/MalmoMod.java @@ -52,7 +52,7 @@ public class MalmoMod public static final String AUTHENTICATION_CONFIGS = "malmologins"; public static final String AGENT_DEAD_QUIT_CODE = "MALMO_AGENT_DIED"; public static final String AGENT_UNRESPONSIVE_CODE = "MALMO_AGENT_NOT_RESPONDING"; - + protected static Hashtable clientProperties = new Hashtable(); protected static Hashtable serverProperties = new Hashtable(); @@ -60,304 +60,343 @@ public class MalmoMod MalmoModServer server = null; Configuration sessionConfig = null; // Configs just for this session - used in place of command-line arguments, overwritten by LaunchClient.bat Configuration permanentConfig = null; // Configs that persist - not overwritten by LaunchClient.bat - + @Instance(value = MalmoMod.MODID) //Tell Forge what instance to use. - public static MalmoMod instance; - + public static MalmoMod instance; + public static SimpleNetworkWrapper network; - + @EventHandler public void preInit(FMLPreInitializationEvent event) { - ModMetadata metadata = event.getModMetadata(); - metadata.autogenerated = false; - List authors = new ArrayList(); - authors.add("Tim"); - authors.add("Dave"); - authors.add("Katja"); - authors.add("Matt"); - metadata.authorList = authors; - metadata.url = "http://research.microsoft.com"; - // Load the correct configs (client or server) + ModMetadata metadata = event.getModMetadata(); + metadata.autogenerated = false; + List authors = new ArrayList(); + authors.add("Tim"); + authors.add("Dave"); + authors.add("Katja"); + authors.add("Matt"); + metadata.authorList = authors; + metadata.url = "http://research.microsoft.com"; + // Load the correct configs (client or server) File configDir = event.getModConfigurationDirectory(); File sessionConfigFile = new File(configDir, MODID + event.getSide().toString() + ".cfg"); File permanentConfigFile = new File(configDir, MODID + event.getSide().toString() + "Permanent.cfg"); - this.sessionConfig = new Configuration(sessionConfigFile); - this.sessionConfig.load(); - this.permanentConfig = new Configuration(permanentConfigFile); - this.permanentConfig.load(); - - AddressHelper.update(this.sessionConfig); - ScreenHelper.update(this.permanentConfig); - - network = NetworkRegistry.INSTANCE.newSimpleChannel("Malmo"); - network.registerMessage(ObservationFromFullStatsImplementation.FullStatsRequestMessageHandler.class, ObservationFromFullStatsImplementation.FullStatsRequestMessage.class, 1, Side.SERVER); - network.registerMessage(ObservationFromGridImplementation.GridRequestMessageHandler.class, ObservationFromGridImplementation.GridRequestMessage.class, 2, Side.SERVER); - network.registerMessage(MalmoMessageHandler.class, MalmoMessage.class, 3, Side.CLIENT); // Malmo messages from server to client - network.registerMessage(ObservationFromMazeOptimalPathImplementation.OptimalPathRequestMessageHandler.class, ObservationFromMazeOptimalPathImplementation.OptimalPathRequestMessage.class, 4, Side.SERVER); - network.registerMessage(AbsoluteMovementCommandsImplementation.TeleportMessageHandler.class, AbsoluteMovementCommandsImplementation.TeleportMessage.class, 5, Side.SERVER); - network.registerMessage(MalmoMessageHandler.class, MalmoMessage.class, 6, Side.SERVER); // Malmo messages from client to server + this.sessionConfig = new Configuration(sessionConfigFile); + this.sessionConfig.load(); + this.permanentConfig = new Configuration(permanentConfigFile); + this.permanentConfig.load(); + + AddressHelper.update(this.sessionConfig); + ScreenHelper.update(this.permanentConfig); + + network = NetworkRegistry.INSTANCE.newSimpleChannel("Malmo"); + network.registerMessage(ObservationFromFullStatsImplementation.FullStatsRequestMessageHandler.class, ObservationFromFullStatsImplementation.FullStatsRequestMessage.class, 1, Side.SERVER); + network.registerMessage(ObservationFromGridImplementation.GridRequestMessageHandler.class, ObservationFromGridImplementation.GridRequestMessage.class, 2, Side.SERVER); + network.registerMessage(MalmoMessageHandler.class, MalmoMessage.class, 3, Side.CLIENT); // Malmo messages from server to client + network.registerMessage(ObservationFromMazeOptimalPathImplementation.OptimalPathRequestMessageHandler.class, ObservationFromMazeOptimalPathImplementation.OptimalPathRequestMessage.class, 4, Side.SERVER); + network.registerMessage(AbsoluteMovementCommandsImplementation.TeleportMessageHandler.class, AbsoluteMovementCommandsImplementation.TeleportMessage.class, 5, Side.SERVER); + network.registerMessage(MalmoMessageHandler.class, MalmoMessage.class, 6, Side.SERVER); // Malmo messages from client to server } public Configuration getModSessionConfigFile() { return this.sessionConfig; } public Configuration getModPermanentConfigFile() { return this.permanentConfig; } - + public static Hashtable getPropertiesForCurrentThread() throws Exception { - if (Minecraft.getMinecraft().isCallingFromMinecraftThread()) - return clientProperties; - if (MinecraftServer.getServer() != null && MinecraftServer.getServer().isCallingFromMinecraftThread()) - return serverProperties; - else throw new Exception("Request for properties made from unrecognised thread."); + if (Minecraft.getMinecraft().isCallingFromMinecraftThread()) + return clientProperties; + if (MinecraftServer.getServer() != null && MinecraftServer.getServer().isCallingFromMinecraftThread()) + return serverProperties; + else throw new Exception("Request for properties made from unrecognised thread."); } - + @EventHandler public void init(FMLInitializationEvent event) { - if (event.getSide().isClient()) - { - this.client = new MalmoModClient(); - this.client.init(event); - } - if (event.getSide().isServer()) - { - this.server = new MalmoModServer(); - this.server.init(event); - } + if (event.getSide().isClient()) + { + this.client = new MalmoModClient(); + this.client.init(event); + } + if (event.getSide().isServer()) + { + this.server = new MalmoModServer(); + this.server.init(event); + } } - + public void initIntegratedServer(MissionInit minit) { - // Will replace any existing server objects. - this.server = new MalmoModServer(); - this.server.init(minit); - } + // Will replace any existing server objects. + this.server = new MalmoModServer(); + this.server.init(minit); + } public void sendMissionInitDirectToServer(MissionInit minit) throws Exception { - if (this.server == null) - throw new Exception("Trying to send a mission request directly when no server has been created!"); + if (this.server == null) + throw new Exception("Trying to send a mission request directly when no server has been created!"); - this.server.sendMissionInitDirectToServer(minit); + this.server.sendMissionInitDirectToServer(minit); } public enum MalmoMessageType { - SERVER_NULLMESSASGE, - SERVER_ALLPLAYERSJOINED, - SERVER_STOPAGENTS, // Server request for all agents to stop what they are doing (mission is over) - SERVER_MISSIONOVER, // Server informing that all agents have stopped, and the mission is now over. - SERVER_OBSERVATIONSREADY, - SERVER_TEXT, - SERVER_ABORT, - SERVER_SOMEOTHERMESSAGE, - CLIENT_AGENTREADY, // Client response to server's ready request - CLIENT_AGENTRUNNING, // Client has just started running - CLIENT_AGENTSTOPPED, // Client response to server's stop request - CLIENT_AGENTFINISHEDMISSION,// Individual agent has finished a mission - CLIENT_BAILED, // Client has hit an error and been forced to enter error state - CLIENT_SOMEOTHERMESSAGE + SERVER_NULLMESSASGE, + SERVER_ALLPLAYERSJOINED, + SERVER_STOPAGENTS, // Server request for all agents to stop what they are doing (mission is over) + SERVER_MISSIONOVER, // Server informing that all agents have stopped, and the mission is now over. + SERVER_OBSERVATIONSREADY, + SERVER_TEXT, + SERVER_ABORT, + SERVER_SOMEOTHERMESSAGE, + CLIENT_AGENTREADY, // Client response to server's ready request + CLIENT_AGENTRUNNING, // Client has just started running + CLIENT_AGENTSTOPPED, // Client response to server's stop request + CLIENT_AGENTFINISHEDMISSION,// Individual agent has finished a mission + CLIENT_BAILED, // Client has hit an error and been forced to enter error state + CLIENT_SOMEOTHERMESSAGE } /** General purpose messaging class
* Used to pass messages from the server to the client. */ static public class MalmoMessage implements IMessage - { - private MalmoMessageType messageType = MalmoMessageType.SERVER_NULLMESSASGE; - private int uid = 0; - private Map data = new HashMap(); - - public MalmoMessage() - { - } - - /** Construct a message for all listeners of that messageType - * @param messageType - * @param message - */ - public MalmoMessage(MalmoMessageType messageType, String message) - { - this.messageType = messageType; - this.uid = 0; - this.data.put("message", message); - } - - /** Construct a message for the (hopefully) single listener that matches the uid - * @param messageType - * @param uid a hash code that (more or less) uniquely identifies the targeted listener - * @param message - */ - public MalmoMessage(MalmoMessageType messageType, int uid, Map data) - { - this.messageType = messageType; - this.uid = uid; - this.data = data; - } - - @Override - public void fromBytes(ByteBuf buf) - { - int i = ByteBufUtils.readVarInt(buf, 1); // Read message type from first byte. - if (i >= 0 && i <= MalmoMessageType.values().length) - this.messageType = MalmoMessageType.values()[i]; - else - this.messageType = MalmoMessageType.SERVER_NULLMESSASGE; - - // Now read the uid: - this.uid = buf.readInt(); - - // And the actual message content: - // First, the number of entries in the map: - int length = buf.readInt(); - this.data = new HashMap(); - // Now read each key/value pair: - ByteBufInputStream bbis = new ByteBufInputStream(buf); - for (i = 0; i < length; i++) - { - String key; - String value; - try - { - key = bbis.readUTF(); - value = bbis.readUTF(); - this.data.put(key, value); - } - catch (IOException e) - { - } - } - try - { - bbis.close(); - } - catch (IOException e) - { - } - } - - @Override - public void toBytes(ByteBuf buf) - { - ByteBufUtils.writeVarInt(buf, this.messageType.ordinal(), 1); // First byte is the message type. - buf.writeInt(this.uid); - // Now write the data as a set of string pairs: - ByteBufOutputStream bbos = new ByteBufOutputStream(buf); - buf.writeInt(this.data.size()); - for (Entry e : this.data.entrySet()) - { - try - { - bbos.writeUTF(e.getKey()); - bbos.writeUTF(e.getValue()); - } - catch (IOException e1) - { - // Data lost! - } - } - try - { - bbos.close(); - } - catch (IOException e1) - { - // Data lost? - } - } - } - + { + private MalmoMessageType messageType = MalmoMessageType.SERVER_NULLMESSASGE; + private int uid = 0; + private Map data = new HashMap(); + + public MalmoMessage() + { + } + + /** Construct a message for all listeners of that messageType + * @param messageType + * @param message + */ + public MalmoMessage(MalmoMessageType messageType, String message) + { + this.messageType = messageType; + this.uid = 0; + this.data.put("message", message); + } + + /** Construct a message for the (hopefully) single listener that matches the uid + * @param messageType + * @param uid a hash code that (more or less) uniquely identifies the targeted listener + * @param message + */ + public MalmoMessage(MalmoMessageType messageType, int uid, Map data) + { + this.messageType = messageType; + this.uid = uid; + this.data = data; + } + + /** Read a UTF8 string that could potentially be larger than 64k
+ * The ByteBufInputStream.readUTF() and writeUTF() calls use the first two bytes of the message + * to encode the length of the string, which limits the string length to 64k. + * This method gets around that limitation by using a four byte header. + * @param bbis ByteBufInputStream we are reading from + * @return the (potentially large) string we read + * @throws IOException + */ + private String readLargeUTF(ByteBufInputStream bbis) throws IOException + { + int length = bbis.readInt(); + if (length == 0) + return ""; + + byte[] data = new byte[length]; + int length_read = bbis.read(data, 0, length); + if (length_read != length) + throw new IOException("Failed to read whole message"); + + return new String(data, "utf-8"); + } + + /** Write a potentially long string as UTF8
+ * The ByteBufInputStream.readUTF() and writeUTF() calls use the first two bytes of the message + * to encode the length of the string, which limits the string length to 64k. + * This method gets around that limitation by using a four byte header. + * @param s The string we are sending + * @param bbos The ByteBufOutputStream we are writing to + * @throws IOException + */ + private void writeLargeUTF(String s, ByteBufOutputStream bbos) throws IOException + { + byte[] data = s.getBytes("utf-8"); + bbos.writeInt(data.length); + bbos.write(data); + } + + @Override + public void fromBytes(ByteBuf buf) + { + int i = ByteBufUtils.readVarInt(buf, 1); // Read message type from first byte. + if (i >= 0 && i <= MalmoMessageType.values().length) + this.messageType = MalmoMessageType.values()[i]; + else + this.messageType = MalmoMessageType.SERVER_NULLMESSASGE; + + // Now read the uid: + this.uid = buf.readInt(); + + // And the actual message content: + // First, the number of entries in the map: + int length = buf.readInt(); + this.data = new HashMap(); + // Now read each key/value pair: + ByteBufInputStream bbis = new ByteBufInputStream(buf); + for (i = 0; i < length; i++) + { + String key; + String value; + try + { + key = bbis.readUTF(); + value = readLargeUTF(bbis); + this.data.put(key, value); + } + catch (IOException e) + { + System.out.println("Warning - failed to read message data"); + } + } + try + { + bbis.close(); + } + catch (IOException e) + { + System.out.println("Warning - failed to read message data"); + } + } + + @Override + public void toBytes(ByteBuf buf) + { + ByteBufUtils.writeVarInt(buf, this.messageType.ordinal(), 1); // First byte is the message type. + buf.writeInt(this.uid); + // Now write the data as a set of string pairs: + ByteBufOutputStream bbos = new ByteBufOutputStream(buf); + buf.writeInt(this.data.size()); + for (Entry e : this.data.entrySet()) + { + try + { + bbos.writeUTF(e.getKey()); + writeLargeUTF(e.getValue(), bbos); + } + catch (IOException e1) + { + System.out.println("Warning - failed to write message data"); + } + } + try + { + bbos.close(); + } + catch (IOException e1) + { + System.out.println("Warning - failed to write message data"); + } + } + } + public interface IMalmoMessageListener { - void onMessage(MalmoMessageType messageType, Map data); + void onMessage(MalmoMessageType messageType, Map data); } - + /** Handler for messages from the server to the clients. Register with this to receive specific messages. - */ + */ public static class MalmoMessageHandler implements IMessageHandler - { - static private Map> listeners = new HashMap>(); - public MalmoMessageHandler() - { - } - - public static boolean registerForMessage(IMalmoMessageListener listener, MalmoMessageType messageType) - { - if (!listeners.containsKey(messageType)) - listeners.put(messageType, new ArrayList()); - - if (listeners.get(messageType).contains(listener)) - return false; // Already registered. - - listeners.get(messageType).add(listener); - return true; - } - - public static boolean deregisterForMessage(IMalmoMessageListener listener, MalmoMessageType messageType) - { - if (!listeners.containsKey(messageType)) - return false; // Not registered. - - return listeners.get(messageType).remove(listener); // Will return false if not present. - } - - @Override - public IMessage onMessage(final MalmoMessage message, final MessageContext ctx) - { - final List interestedParties = listeners.get(message.messageType); - if (interestedParties != null && interestedParties.size() > 0) - { - IThreadListener mainThread = null; - if (ctx.side == Side.CLIENT) - mainThread = Minecraft.getMinecraft(); - else - mainThread = MinecraftServer.getServer(); - mainThread.addScheduledTask(new Runnable() - { - @Override - public void run() - { - for (IMalmoMessageListener l : interestedParties) - { - // If the message's uid is set (ie non-zero), then use it to ensure that only the matching listener receives this message. - // Otherwise, let all listeners who are interested get a look. - if (message.uid == 0 || System.identityHashCode(l) == message.uid) - l.onMessage(message.messageType, message.data); - } - } - }); - } - return null; // no response in this case - } + { + static private Map> listeners = new HashMap>(); + public MalmoMessageHandler() + { + } + + public static boolean registerForMessage(IMalmoMessageListener listener, MalmoMessageType messageType) + { + if (!listeners.containsKey(messageType)) + listeners.put(messageType, new ArrayList()); + + if (listeners.get(messageType).contains(listener)) + return false; // Already registered. + + listeners.get(messageType).add(listener); + return true; + } + + public static boolean deregisterForMessage(IMalmoMessageListener listener, MalmoMessageType messageType) + { + if (!listeners.containsKey(messageType)) + return false; // Not registered. + + return listeners.get(messageType).remove(listener); // Will return false if not present. + } + + @Override + public IMessage onMessage(final MalmoMessage message, final MessageContext ctx) + { + final List interestedParties = listeners.get(message.messageType); + if (interestedParties != null && interestedParties.size() > 0) + { + IThreadListener mainThread = null; + if (ctx.side == Side.CLIENT) + mainThread = Minecraft.getMinecraft(); + else + mainThread = MinecraftServer.getServer(); + mainThread.addScheduledTask(new Runnable() + { + @Override + public void run() + { + for (IMalmoMessageListener l : interestedParties) + { + // If the message's uid is set (ie non-zero), then use it to ensure that only the matching listener receives this message. + // Otherwise, let all listeners who are interested get a look. + if (message.uid == 0 || System.identityHashCode(l) == message.uid) + l.onMessage(message.messageType, message.data); + } + } + }); + } + return null; // no response in this case + } } - public static void safeSendToAll(MalmoMessageType malmoMessage) - { - // network.sendToAll() is buggy - race conditions result in the message getting trashed if there is more than one client. - MinecraftServer server = MinecraftServer.getServer(); - for (Object player : server.getConfigurationManager().playerEntityList) - { - if (player != null && player instanceof EntityPlayerMP) - { - // Must construct a new message for each client: - network.sendTo(new MalmoMod.MalmoMessage(malmoMessage, ""), (EntityPlayerMP)player); - } - } - } - - public static void safeSendToAll(MalmoMessageType malmoMessage, Map data) - { - // network.sendToAll() is buggy - race conditions result in the message getting trashed if there is more than one client. - MinecraftServer server = MinecraftServer.getServer(); - for (Object player : server.getConfigurationManager().playerEntityList) - { - if (player != null && player instanceof EntityPlayerMP) - { - // Must construct a new message for each client: - Map dataCopy = new HashMap(); - dataCopy.putAll(data); - network.sendTo(new MalmoMod.MalmoMessage(malmoMessage, 0, dataCopy), (EntityPlayerMP)player); - } - } - } + public static void safeSendToAll(MalmoMessageType malmoMessage) + { + // network.sendToAll() is buggy - race conditions result in the message getting trashed if there is more than one client. + MinecraftServer server = MinecraftServer.getServer(); + for (Object player : server.getConfigurationManager().playerEntityList) + { + if (player != null && player instanceof EntityPlayerMP) + { + // Must construct a new message for each client: + network.sendTo(new MalmoMod.MalmoMessage(malmoMessage, ""), (EntityPlayerMP)player); + } + } + } + + public static void safeSendToAll(MalmoMessageType malmoMessage, Map data) + { + // network.sendToAll() is buggy - race conditions result in the message getting trashed if there is more than one client. + MinecraftServer server = MinecraftServer.getServer(); + for (Object player : server.getConfigurationManager().playerEntityList) + { + if (player != null && player instanceof EntityPlayerMP) + { + // Must construct a new message for each client: + Map dataCopy = new HashMap(); + dataCopy.putAll(data); + network.sendTo(new MalmoMod.MalmoMessage(malmoMessage, 0, dataCopy), (EntityPlayerMP)player); + } + } + } }