From dc0bcb5d1366ccc0751580801e92fdd403292f74 Mon Sep 17 00:00:00 2001 From: Daniel Johansson Date: Thu, 12 Nov 2015 15:11:41 +0000 Subject: [PATCH 01/16] Resolves #378, adding support for detecting ARMv8 on Android. --- .../main/java/com/jme3/system/android/JmeAndroidSystem.java | 4 ++++ .../src/main/java/com/jme3/system/JmeSystemDelegate.java | 4 ++++ jme3-core/src/main/java/com/jme3/system/Platform.java | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/jme3-android/src/main/java/com/jme3/system/android/JmeAndroidSystem.java b/jme3-android/src/main/java/com/jme3/system/android/JmeAndroidSystem.java index bbbfaca7a..efaf4f3bb 100644 --- a/jme3-android/src/main/java/com/jme3/system/android/JmeAndroidSystem.java +++ b/jme3-android/src/main/java/com/jme3/system/android/JmeAndroidSystem.java @@ -122,9 +122,13 @@ public class JmeAndroidSystem extends JmeSystemDelegate { return Platform.Android_ARM6; } else if (arch.contains("v7")) { return Platform.Android_ARM7; + } else if (arch.contains("v8")) { + return Platform.Android_ARM8; } else { return Platform.Android_ARM5; // unknown ARM } + } else if (arch.contains("aarch")) { + return Platform.Android_ARM8; } else { return Platform.Android_Other; } diff --git a/jme3-core/src/main/java/com/jme3/system/JmeSystemDelegate.java b/jme3-core/src/main/java/com/jme3/system/JmeSystemDelegate.java index 150275d46..2134e0a7e 100644 --- a/jme3-core/src/main/java/com/jme3/system/JmeSystemDelegate.java +++ b/jme3-core/src/main/java/com/jme3/system/JmeSystemDelegate.java @@ -153,6 +153,10 @@ public abstract class JmeSystemDelegate { return false; } else if (arch.equals("universal")) { return false; + } else if (arch.equals("aarch32")) { + return false; + } else if (arch.equals("aarch64")) { + return true; } else if (arch.equals("arm")) { return false; } else { diff --git a/jme3-core/src/main/java/com/jme3/system/Platform.java b/jme3-core/src/main/java/com/jme3/system/Platform.java index 8a30fff0c..f9206d33f 100644 --- a/jme3-core/src/main/java/com/jme3/system/Platform.java +++ b/jme3-core/src/main/java/com/jme3/system/Platform.java @@ -88,6 +88,11 @@ public enum Platform { */ Android_ARM7, + /** + * Android ARM8 + */ + Android_ARM8, + /** * Android x86 */ From 9e80d8a7aa55b18cdc0035b8a8ab6a822d42a8b6 Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Sun, 15 Nov 2015 01:20:28 -0500 Subject: [PATCH 02/16] Beefing up the client server tests a little to add some listeners and to better report what's going on to the console. This is in prep for making a combined test. --- .../java/jme3test/network/TestChatClient.java | 47 ++++++++++++++--- .../java/jme3test/network/TestChatServer.java | 50 +++++++++++++++---- 2 files changed, 82 insertions(+), 15 deletions(-) diff --git a/jme3-examples/src/main/java/jme3test/network/TestChatClient.java b/jme3-examples/src/main/java/jme3test/network/TestChatClient.java index fddb047c4..219ccd58f 100644 --- a/jme3-examples/src/main/java/jme3test/network/TestChatClient.java +++ b/jme3-examples/src/main/java/jme3test/network/TestChatClient.java @@ -32,6 +32,7 @@ package jme3test.network; import com.jme3.network.Client; +import com.jme3.network.ClientStateListener; import com.jme3.network.Message; import com.jme3.network.MessageListener; import com.jme3.network.Network; @@ -51,11 +52,11 @@ import jme3test.network.TestChatServer.ChatMessage; */ public class TestChatClient extends JFrame { - private Client client; - private JEditorPane chatLog; - private StringBuilder chatMessages = new StringBuilder(); - private JTextField nameField; - private JTextField messageField; + private final Client client; + private final JEditorPane chatLog; + private final StringBuilder chatMessages = new StringBuilder(); + private final JTextField nameField; + private final JTextField messageField; public TestChatClient(String host) throws IOException { super("jME3 Test Chat Client - to:" + host); @@ -90,9 +91,17 @@ public class TestChatClient extends JFrame { client = Network.connectToServer(TestChatServer.NAME, TestChatServer.VERSION, host, TestChatServer.PORT, TestChatServer.UDP_PORT); client.addMessageListener(new ChatHandler(), ChatMessage.class); + client.addClientStateListener(new ChatClientStateListener()); client.start(); } + @Override + public void dispose() { + System.out.println("Chat window closing."); + super.dispose(); + client.close(); + } + public static String getString(Component owner, String title, String message, String initialValue) { return (String) JOptionPane.showInputDialog(owner, message, title, JOptionPane.PLAIN_MESSAGE, null, null, initialValue); @@ -108,12 +117,23 @@ public class TestChatClient extends JFrame { return; } + // Register a shutdown hook to get a message on the console when the + // app actually finishes + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + System.out.println("Chat client is terminating."); + } + }); + + TestChatClient test = new TestChatClient(s); test.setVisible(true); } private class ChatHandler implements MessageListener { + @Override public void messageReceived(Client source, Message m) { ChatMessage chat = (ChatMessage) m; @@ -134,15 +154,30 @@ public class TestChatClient extends JFrame { } } + private class ChatClientStateListener implements ClientStateListener { + + @Override + public void clientConnected(Client c) { + System.out.println("clientConnected()"); + } + + @Override + public void clientDisconnected(Client c, DisconnectInfo info) { + System.out.println("clientDisconnected()"); + } + + } + private class SendAction extends AbstractAction { - private boolean reliable; + private final boolean reliable; public SendAction(boolean reliable) { super(reliable ? "TCP" : "UDP"); this.reliable = reliable; } + @Override public void actionPerformed(ActionEvent evt) { String name = nameField.getText(); String message = messageField.getText(); diff --git a/jme3-examples/src/main/java/jme3test/network/TestChatServer.java b/jme3-examples/src/main/java/jme3test/network/TestChatServer.java index a2aa2ee1c..cde870f26 100644 --- a/jme3-examples/src/main/java/jme3test/network/TestChatServer.java +++ b/jme3-examples/src/main/java/jme3test/network/TestChatServer.java @@ -34,6 +34,7 @@ package jme3test.network; import com.jme3.network.*; import com.jme3.network.serializing.Serializable; import com.jme3.network.serializing.Serializer; +import java.io.IOException; /** * A simple test chat server. When SM implements a set @@ -51,22 +52,37 @@ public class TestChatServer { public static final int PORT = 5110; public static final int UDP_PORT = 5110; - public static void initializeClasses() { - // Doing it here means that the client code only needs to - // call our initialize. - Serializer.registerClass(ChatMessage.class); - } - - public static void main(String... args) throws Exception { + private Server server; + + public TestChatServer() throws IOException { initializeClasses(); // Use this to test the client/server name version check - Server server = Network.createServer(NAME, VERSION, PORT, UDP_PORT); - server.start(); + this.server = Network.createServer(NAME, VERSION, PORT, UDP_PORT); ChatHandler handler = new ChatHandler(); server.addMessageListener(handler, ChatMessage.class); + + server.addConnectionListener(new ChatConnectionListener()); + } + + public void start() { + server.start(); + } + public static void initializeClasses() { + // Doing it here means that the client code only needs to + // call our initialize. + Serializer.registerClass(ChatMessage.class); + } + + public static void main(String... args) throws Exception { + + TestChatServer chatServer = new TestChatServer(); + chatServer.start(); + + System.out.println("Waiting for connections on port:" + PORT); + // Keep running basically forever synchronized (NAME) { NAME.wait(); @@ -78,6 +94,7 @@ public class TestChatServer { public ChatHandler() { } + @Override public void messageReceived(HostedConnection source, Message m) { if (m instanceof ChatMessage) { // Keep track of the name just in case we @@ -96,6 +113,20 @@ public class TestChatServer { } } + private static class ChatConnectionListener implements ConnectionListener { + + @Override + public void connectionAdded( Server server, HostedConnection conn ) { + System.out.println("connectionAdded(" + conn + ")"); + } + + @Override + public void connectionRemoved(Server server, HostedConnection conn) { + System.out.println("connectionRemoved(" + conn + ")"); + } + + } + @Serializable public static class ChatMessage extends AbstractMessage { @@ -126,6 +157,7 @@ public class TestChatServer { return message; } + @Override public String toString() { return name + ":" + message; } From 38fe771ed80d66d89c1cce94775bcae18c22a279 Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Sun, 15 Nov 2015 01:22:17 -0500 Subject: [PATCH 03/16] Modified the client state messages to include the chat instance in case we add a multi-client test. --- .../src/main/java/jme3test/network/TestChatClient.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/jme3-examples/src/main/java/jme3test/network/TestChatClient.java b/jme3-examples/src/main/java/jme3test/network/TestChatClient.java index 219ccd58f..0cdc46c39 100644 --- a/jme3-examples/src/main/java/jme3test/network/TestChatClient.java +++ b/jme3-examples/src/main/java/jme3test/network/TestChatClient.java @@ -93,6 +93,8 @@ public class TestChatClient extends JFrame { client.addMessageListener(new ChatHandler(), ChatMessage.class); client.addClientStateListener(new ChatClientStateListener()); client.start(); + + System.out.println("Started client:" + client); } @Override @@ -158,12 +160,12 @@ public class TestChatClient extends JFrame { @Override public void clientConnected(Client c) { - System.out.println("clientConnected()"); + System.out.println("clientConnected(" + c + ")"); } @Override public void clientDisconnected(Client c, DisconnectInfo info) { - System.out.println("clientDisconnected()"); + System.out.println("clientDisconnected(" + c + ")"); } } From cea36ffc4753752ca135f2a036b2e3f174cf6c3d Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Sun, 15 Nov 2015 01:53:32 -0500 Subject: [PATCH 04/16] Added some more client-side error and connection handling to make a more complete example. It should now be relatively well behaved through all normal shutdown paths. Modified the server to gracefully close the client connections when shutting down rather than just letting the sockets die. --- .../java/jme3test/network/TestChatClient.java | 28 ++++++++- .../java/jme3test/network/TestChatServer.java | 63 ++++++++++++++++--- 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/jme3-examples/src/main/java/jme3test/network/TestChatClient.java b/jme3-examples/src/main/java/jme3test/network/TestChatClient.java index 0cdc46c39..e8af4329a 100644 --- a/jme3-examples/src/main/java/jme3test/network/TestChatClient.java +++ b/jme3-examples/src/main/java/jme3test/network/TestChatClient.java @@ -33,6 +33,7 @@ package jme3test.network; import com.jme3.network.Client; import com.jme3.network.ClientStateListener; +import com.jme3.network.ErrorListener; import com.jme3.network.Message; import com.jme3.network.MessageListener; import com.jme3.network.Network; @@ -92,6 +93,7 @@ public class TestChatClient extends JFrame { host, TestChatServer.PORT, TestChatServer.UDP_PORT); client.addMessageListener(new ChatHandler(), ChatMessage.class); client.addClientStateListener(new ChatClientStateListener()); + client.addErrorListener(new ChatErrorListener()); client.start(); System.out.println("Started client:" + client); @@ -101,7 +103,9 @@ public class TestChatClient extends JFrame { public void dispose() { System.out.println("Chat window closing."); super.dispose(); - client.close(); + if( client.isConnected() ) { + client.close(); + } } public static String getString(Component owner, String title, String message, String initialValue) { @@ -165,7 +169,27 @@ public class TestChatClient extends JFrame { @Override public void clientDisconnected(Client c, DisconnectInfo info) { - System.out.println("clientDisconnected(" + c + ")"); + System.out.println("clientDisconnected(" + c + "):" + info); + if( info != null ) { + // The connection was closed by the server + JOptionPane.showMessageDialog(rootPane, + info.reason, + "Connection Closed", + JOptionPane.INFORMATION_MESSAGE); + dispose(); + } + } + } + + private class ChatErrorListener implements ErrorListener { + + @Override + public void handleError( Client source, Throwable t ) { + System.out.println("handleError(" + source + ", " + t + ")"); + JOptionPane.showMessageDialog(rootPane, + String.valueOf(t), + "Connection Error", + JOptionPane.ERROR_MESSAGE); } } diff --git a/jme3-examples/src/main/java/jme3test/network/TestChatServer.java b/jme3-examples/src/main/java/jme3test/network/TestChatServer.java index cde870f26..0f9021152 100644 --- a/jme3-examples/src/main/java/jme3test/network/TestChatServer.java +++ b/jme3-examples/src/main/java/jme3test/network/TestChatServer.java @@ -53,6 +53,7 @@ public class TestChatServer { public static final int UDP_PORT = 5110; private Server server; + private boolean isRunning; public TestChatServer() throws IOException { initializeClasses(); @@ -66,10 +67,49 @@ public class TestChatServer { server.addConnectionListener(new ChatConnectionListener()); } - public void start() { + public synchronized void start() { + if( isRunning ) { + return; + } server.start(); + isRunning = true; + } + + public synchronized void close() { + if( !isRunning ) { + return; + } + + // Gracefully let any connections know that the server is + // going down. Without this, their connections will simply + // error out. + for( HostedConnection conn : server.getConnections() ) { + conn.close("Server is shutting down."); + } + try { + Thread.sleep(1000); // wait a couple beats to let the messages go out + } catch( InterruptedException e ) { + e.printStackTrace(); + } + + server.close(); + isRunning = false; + notifyAll(); } + protected void runCommand( HostedConnection conn, String user, String command ) { + if( "/shutdown".equals(command) ) { + server.broadcast(new ChatMessage("server", "Server is shutting down.")); + close(); + } else if( "/help".equals(command) ) { + StringBuilder sb = new StringBuilder(); + sb.append("Chat commands:\n"); + sb.append("/help - prints this message.\n"); + sb.append("/shutdown - shuts down the server."); + server.broadcast(new ChatMessage("server", sb.toString())); + } + } + public static void initializeClasses() { // Doing it here means that the client code only needs to // call our initialize. @@ -84,12 +124,14 @@ public class TestChatServer { System.out.println("Waiting for connections on port:" + PORT); // Keep running basically forever - synchronized (NAME) { - NAME.wait(); + while( chatServer.isRunning ) { + synchronized (chatServer) { + chatServer.wait(); + } } } - private static class ChatHandler implements MessageListener { + private class ChatHandler implements MessageListener { public ChatHandler() { } @@ -100,20 +142,27 @@ public class TestChatServer { // Keep track of the name just in case we // want to know it for some other reason later and it's // a good example of session data - source.setAttribute("name", ((ChatMessage) m).getName()); + ChatMessage cm = (ChatMessage)m; + source.setAttribute("name", cm.getName()); + + // Check for a / command + if( cm.message.startsWith("/") ) { + runCommand(source, cm.name, cm.message); + return; + } System.out.println("Broadcasting:" + m + " reliable:" + m.isReliable()); // Just rebroadcast... the reliable flag will stay the // same so if it came in on UDP it will go out on that too - source.getServer().broadcast(m); + source.getServer().broadcast(cm); } else { System.err.println("Received odd message:" + m); } } } - private static class ChatConnectionListener implements ConnectionListener { + private class ChatConnectionListener implements ConnectionListener { @Override public void connectionAdded( Server server, HostedConnection conn ) { From 17df399f68bd6cc7586a1550ddc8073102933fbf Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Sun, 15 Nov 2015 02:13:58 -0500 Subject: [PATCH 05/16] Commented out the message class serialization and left a comment as to why: in 3.1 there is a default service that automatically does this on clients. --- .../src/main/java/jme3test/network/TestChatClient.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jme3-examples/src/main/java/jme3test/network/TestChatClient.java b/jme3-examples/src/main/java/jme3test/network/TestChatClient.java index e8af4329a..a36853913 100644 --- a/jme3-examples/src/main/java/jme3test/network/TestChatClient.java +++ b/jme3-examples/src/main/java/jme3test/network/TestChatClient.java @@ -114,7 +114,12 @@ public class TestChatClient extends JFrame { } public static void main(String... args) throws Exception { - TestChatServer.initializeClasses(); + + // Note: in JME 3.1 this is generally unnecessary as the server will + // send a message with all server-registered classes. + // TestChatServer.initializeClasses(); + // Leaving the call commented out to be illustrative regarding the + // common old pattern. // Grab a host string from the user String s = getString(null, "Host Info", "Enter chat host:", "localhost"); From e832ad5c949facfd793b178c557de1e46ecc801f Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Sun, 15 Nov 2015 02:14:26 -0500 Subject: [PATCH 06/16] Added an isRunning() method so that other classes can check if the server is still running. --- .../src/main/java/jme3test/network/TestChatServer.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jme3-examples/src/main/java/jme3test/network/TestChatServer.java b/jme3-examples/src/main/java/jme3test/network/TestChatServer.java index 0f9021152..4d8642881 100644 --- a/jme3-examples/src/main/java/jme3test/network/TestChatServer.java +++ b/jme3-examples/src/main/java/jme3test/network/TestChatServer.java @@ -67,6 +67,10 @@ public class TestChatServer { server.addConnectionListener(new ChatConnectionListener()); } + public boolean isRunning() { + return isRunning; + } + public synchronized void start() { if( isRunning ) { return; From 2c337123a90138de119fb5e9cab4d2cf9aa4f120 Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Sun, 15 Nov 2015 02:15:30 -0500 Subject: [PATCH 07/16] Added a test/example of running a client and a server in the same JVM. a) this makes a good example of self-hosted style LAN multiplayer games, and b) it causes the serialization bug to show up so I can fix it. (Already fixed it and that commit will be next... it's almost like TDD.) --- .../network/TestChatClientAndServer.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 jme3-examples/src/main/java/jme3test/network/TestChatClientAndServer.java diff --git a/jme3-examples/src/main/java/jme3test/network/TestChatClientAndServer.java b/jme3-examples/src/main/java/jme3test/network/TestChatClientAndServer.java new file mode 100644 index 000000000..ee08067ae --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/network/TestChatClientAndServer.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package jme3test.network; + + +/** + * Combines the server instance and a client instance into the + * same JVM to show an example of, and to test, a pattern like + * self-hosted multiplayer games. + * + * @author Paul Speed + */ +public class TestChatClientAndServer { + + public static void main( String... args ) throws Exception { + + System.out.println("Starting chat server..."); + TestChatServer chatServer = new TestChatServer(); + chatServer.start(); + + System.out.println("Waiting for connections on port:" + TestChatServer.PORT); + + // Now launch a client + + TestChatClient test = new TestChatClient("localhost"); + test.setVisible(true); + + // Register a shutdown hook to get a message on the console when the + // app actually finishes + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + System.out.println("Client and server test is terminating."); + } + }); + + // Keep running basically forever or until the server + // shuts down + while( chatServer.isRunning() ) { + synchronized (chatServer) { + chatServer.wait(); + } + } + } +} From 4b2f3610266b6057e43c88337a81fa0928019f85 Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Sun, 15 Nov 2015 02:22:53 -0500 Subject: [PATCH 08/16] Modified to skip registering the message classes if they are already registered. This avoids one of the issues of a client running in the same JVM as a server that already registered these classes. This was the easy fix. --- .../ClientSerializerRegistrationsService.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/jme3-networking/src/main/java/com/jme3/network/service/serializer/ClientSerializerRegistrationsService.java b/jme3-networking/src/main/java/com/jme3/network/service/serializer/ClientSerializerRegistrationsService.java index 911ce0fb1..be8dfc74a 100644 --- a/jme3-networking/src/main/java/com/jme3/network/service/serializer/ClientSerializerRegistrationsService.java +++ b/jme3-networking/src/main/java/com/jme3/network/service/serializer/ClientSerializerRegistrationsService.java @@ -39,6 +39,8 @@ import com.jme3.network.message.SerializerRegistrationsMessage; import com.jme3.network.serializing.Serializer; import com.jme3.network.service.AbstractClientService; import com.jme3.network.service.ClientServiceManager; +import java.util.logging.Level; +import java.util.logging.Logger; /** @@ -48,14 +50,21 @@ import com.jme3.network.service.ClientServiceManager; */ public class ClientSerializerRegistrationsService extends AbstractClientService implements MessageListener { + + static final Logger log = Logger.getLogger(SerializerRegistrationsMessage.class.getName()); @Override protected void onInitialize( ClientServiceManager serviceManager ) { - // Make sure our message type is registered - // This is the minimum we'd need just to be able to register - // the rest... otherwise we can't even receive this message. - Serializer.registerClass(SerializerRegistrationsMessage.class); - Serializer.registerClass(SerializerRegistrationsMessage.Registration.class); + + // Make sure our message type is registered if it isn't already + if( Serializer.getSerializer(SerializerRegistrationsMessage.class, false) == null ) { + // This is the minimum we'd need just to be able to register + // the rest... otherwise we can't even receive this message. + Serializer.registerClass(SerializerRegistrationsMessage.class); + Serializer.registerClass(SerializerRegistrationsMessage.Registration.class); + } else { + log.log(Level.INFO, "Skipping registration of SerializerRegistrationsMessage."); + } // Add our listener for that message type serviceManager.getClient().addMessageListener(this, SerializerRegistrationsMessage.class); From dfe4b083f0962266f77be913c67b2b1fb01daa2d Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Sun, 15 Nov 2015 02:24:18 -0500 Subject: [PATCH 09/16] Added a check to try and detect the case where a server and a client are running on the same instance. This should cover 99% of the cases where this would come up... and the others can't really use this service anyway and so must disable it. --- .../SerializerRegistrationsMessage.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/jme3-networking/src/main/java/com/jme3/network/message/SerializerRegistrationsMessage.java b/jme3-networking/src/main/java/com/jme3/network/message/SerializerRegistrationsMessage.java index 9c3896475..4d1decc08 100644 --- a/jme3-networking/src/main/java/com/jme3/network/message/SerializerRegistrationsMessage.java +++ b/jme3-networking/src/main/java/com/jme3/network/message/SerializerRegistrationsMessage.java @@ -94,7 +94,7 @@ public class SerializerRegistrationsMessage extends AbstractMessage { public static Registration[] compiled; private static final Serializer fieldSerializer = new FieldSerializer(); - private Registration[] registrations; + private Registration[] registrations; public SerializerRegistrationsMessage() { setReliable(true); @@ -132,7 +132,25 @@ public class SerializerRegistrationsMessage extends AbstractMessage { Serializer.setReadOnly(true); } - public void registerAll() { + public void registerAll() { + + // See if we will have problems because our registry is locked + if( Serializer.isReadOnly() ) { + // Check to see if maybe we are executing this from the + // same JVM that sent the message, ie: client and server are running on + // the same JVM + // There could be more advanced checks than this but for now we'll + // assume that if the registry was compiled here then it means + // we are also the server process. Note that this wouldn't hold true + // under complicated examples where there are clients of one server + // that also run their own servers but realistically they would have + // to disable the ServerSerializerRegistrationsServer anyway. + if( compiled != null ) { + log.log( Level.INFO, "Skipping registration as registry is locked, presumably by a local server process."); + return; + } + } + for( Registration reg : registrations ) { log.log( Level.INFO, "Registering:{0}", reg); reg.register(); From 95603c46c49a6a6dc564161bf2ce379ea980279c Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Mon, 16 Nov 2015 02:03:27 -0500 Subject: [PATCH 10/16] Added a better check. The old one had the side-effect of registering the class if it wasn't already registered. --- .../serializer/ClientSerializerRegistrationsService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jme3-networking/src/main/java/com/jme3/network/service/serializer/ClientSerializerRegistrationsService.java b/jme3-networking/src/main/java/com/jme3/network/service/serializer/ClientSerializerRegistrationsService.java index be8dfc74a..61632282a 100644 --- a/jme3-networking/src/main/java/com/jme3/network/service/serializer/ClientSerializerRegistrationsService.java +++ b/jme3-networking/src/main/java/com/jme3/network/service/serializer/ClientSerializerRegistrationsService.java @@ -55,9 +55,9 @@ public class ClientSerializerRegistrationsService extends AbstractClientService @Override protected void onInitialize( ClientServiceManager serviceManager ) { - + // Make sure our message type is registered if it isn't already - if( Serializer.getSerializer(SerializerRegistrationsMessage.class, false) == null ) { + if( Serializer.getExactSerializerRegistration(SerializerRegistrationsMessage.class) == null ) { // This is the minimum we'd need just to be able to register // the rest... otherwise we can't even receive this message. Serializer.registerClass(SerializerRegistrationsMessage.class); @@ -70,6 +70,7 @@ public class ClientSerializerRegistrationsService extends AbstractClientService serviceManager.getClient().addMessageListener(this, SerializerRegistrationsMessage.class); } + @Override public void messageReceived( Client source, Message m ) { // We only wait for one kind of message... SerializerRegistrationsMessage msg = (SerializerRegistrationsMessage)m; From 95aa2d72d046ca2e48ae4b3614ea3e6685b6d0d3 Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Mon, 16 Nov 2015 02:04:31 -0500 Subject: [PATCH 11/16] Added a basic RMI service that uses the RPC service. I'll add some more javadoc in a sec. --- .../network/service/rmi/Asynchronous.java | 54 +++ .../jme3/network/service/rmi/CallType.java | 43 +++ .../jme3/network/service/rmi/ClassInfo.java | 111 ++++++ .../service/rmi/ClassInfoRegistry.java | 103 ++++++ .../jme3/network/service/rmi/MethodInfo.java | 136 +++++++ .../service/rmi/RemoteObjectHandler.java | 87 +++++ .../network/service/rmi/RmiClientService.java | 147 ++++++++ .../jme3/network/service/rmi/RmiContext.java | 56 +++ .../network/service/rmi/RmiHostedService.java | 203 +++++++++++ .../jme3/network/service/rmi/RmiRegistry.java | 344 ++++++++++++++++++ 10 files changed, 1284 insertions(+) create mode 100644 jme3-networking/src/main/java/com/jme3/network/service/rmi/Asynchronous.java create mode 100644 jme3-networking/src/main/java/com/jme3/network/service/rmi/CallType.java create mode 100644 jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfo.java create mode 100644 jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfoRegistry.java create mode 100644 jme3-networking/src/main/java/com/jme3/network/service/rmi/MethodInfo.java create mode 100644 jme3-networking/src/main/java/com/jme3/network/service/rmi/RemoteObjectHandler.java create mode 100644 jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiClientService.java create mode 100644 jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiContext.java create mode 100644 jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiHostedService.java create mode 100644 jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/Asynchronous.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/Asynchronous.java new file mode 100644 index 000000000..8deada70b --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/Asynchronous.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.network.service.rmi; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.*; + + +/** + * Indicates that a given method should be executed asynchronously + * through the RMI service. This must annotate the method on the + * shared interface for it to have an effect. + * + * @author Paul Speed + */ +@Retention(value=RUNTIME) +@Target(value=METHOD) +public @interface Asynchronous { + boolean reliable() default true; +} + + diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/CallType.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/CallType.java new file mode 100644 index 000000000..6b218891d --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/CallType.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.network.service.rmi; + + +/** + * + * + * @author Paul Speed + */ +public enum CallType { + Synchronous, Asynchronous, Unreliable +} diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfo.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfo.java new file mode 100644 index 000000000..88d91a686 --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfo.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.network.service.rmi; + +import com.jme3.network.serializing.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + + +/** + * + * + * @author Paul Speed + */ +@Serializable +public final class ClassInfo { + private String name; + private short typeId; + private MethodInfo[] methods; + + /** + * For serialization only. + */ + public ClassInfo() { + } + + public ClassInfo( short typeId, Class type ) { + this.typeId = typeId; + this.name = type.getName(); + this.methods = toMethodInfo(type, type.getMethods()); + } + + public String getName() { + return name; + } + + public Class getType() { + try { + return Class.forName(name); + } catch( ClassNotFoundException e ) { + throw new RuntimeException("Error finding class for:" + this, e); + } + } + + public short getId() { + return typeId; + } + + public MethodInfo getMethod( short id ) { + return methods[id]; + } + + public MethodInfo getMethod( Method m ) { + for( MethodInfo mi : methods ) { + if( mi.matches(m) ) { + return mi; + } + } + return null; + } + + private MethodInfo[] toMethodInfo( Class type, Method[] methods ) { + List result = new ArrayList(); + short methodId = 0; + for( Method m : methods ) { + // Simple... add all methods exposed through the interface + result.add(new MethodInfo(methodId++, m)); + } + return result.toArray(new MethodInfo[result.size()]); + } + + public MethodInfo[] getMethods() { + return methods; + } + + @Override + public String toString() { + return "ClassInfo[" + name + "]"; + } +} diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfoRegistry.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfoRegistry.java new file mode 100644 index 000000000..557948042 --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfoRegistry.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.network.service.rmi; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + + +/** + * + * + * @author Paul Speed + */ +public class ClassInfoRegistry { + + //private final LoadingCache cache; // Guava version + private final Map cache = new HashMap(); + private final AtomicInteger nextClassId = new AtomicInteger(); + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + public ClassInfoRegistry() { + //this.cache = CacheBuilder.newBuilder().build(new ClassInfoLoader()); // Guava version + } + + public ClassInfo getClassInfo( Class type ) { + //return cache.getUnchecked(type); // Guava version + + // More complicated without guava + lock.readLock().lock(); + try { + ClassInfo result = cache.get(type); + if( result != null ) { + return result; + } + // Else we need to create it and store it... so grab the write + // lock + lock.readLock().unlock(); + lock.writeLock().lock(); + try { + // Note: it's technically possible that a race with another thread + // asking for the same class already created one between our read unlock + // and our write lock. No matter as it's cheap to create one and does + // no harm. Code is simpler without the double-check. + result = new ClassInfo((short)nextClassId.getAndIncrement(), type); + cache.put(type, result); + + // Regrab the read lock before leaving... kind of unnecessary but + // it makes the method cleaner and widens the gap of lock races. + // Downgrading a write lock to read is ok. + lock.readLock().lock(); + + return result; + } finally { + // Unlock the write lock while still holding onto read + lock.writeLock().unlock(); + } + } finally { + lock.readLock().unlock(); + } + } + + /* + would be more straight-forward with guava Guava version + private class ClassInfoLoader extends CacheLoader { + @Override + public ClassInfo load( Class type ) { + return new ClassInfo((short)nextClassId.getAndIncrement(), type); + } + }*/ +} diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/MethodInfo.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/MethodInfo.java new file mode 100644 index 000000000..a0138556d --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/MethodInfo.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.network.service.rmi; + +import com.jme3.network.serializing.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import javax.jws.Oneway; + + +/** + * + * + * @author Paul Speed + */ +@Serializable +public final class MethodInfo { + + public static final MethodInfo NULL_INFO = new MethodInfo(); + + private String representation; + private short id; + private CallType callType; + private transient Method method; + + /** + * For serialization only. + */ + public MethodInfo() { + } + + public MethodInfo( short id, Method m ) { + this.id = id; + this.method = m; + this.representation = methodToString(m); + this.callType = getCallType(m); + } + + public Object invoke( Object target, Object... parms ) { + try { + return method.invoke(target, parms); + } catch (IllegalAccessException e) { + throw new RuntimeException("Error invoking:" + method + " on:" + target, e); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Error invoking:" + method + " on:" + target, e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Error invoking:" + method + " on:" + target, e); + } + } + + public short getId() { + return id; + } + + public CallType getCallType() { + return callType; + } + + public boolean matches( Method m ) { + return representation.equals(methodToString(m)); + } + + public static String methodToString( Method m ) { + StringBuilder sb = new StringBuilder(); + for( Class t : m.getParameterTypes() ) { + if( sb.length() > 0 ) + sb.append(", "); + sb.append(t.getName()); + } + return m.getReturnType().getName() + " " + m.getName() + "(" + sb + ")"; + } + + public static CallType getCallType( Method m ) { + if( m.getReturnType() != Void.TYPE ) + return CallType.Synchronous; + if( m.getAnnotation(Oneway.class) != null ) + return CallType.Asynchronous; + if( m.getAnnotation(Asynchronous.class) == null ) + return CallType.Synchronous; + + Asynchronous async = m.getAnnotation(Asynchronous.class); + return async.reliable() ? CallType.Asynchronous : CallType.Unreliable; + } + + @Override + public int hashCode() { + return representation.hashCode(); + } + + @Override + public boolean equals( Object o ) { + if( o == this ) { + return true; + } + if( o == null || o.getClass() != getClass() ) { + return false; + } + MethodInfo other = (MethodInfo)o; + return representation.equals(other.representation); + } + + @Override + public String toString() { + return "MethodInfo[#" + getId() + ", callType=" + callType + ", " + representation + "]"; + } +} diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RemoteObjectHandler.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RemoteObjectHandler.java new file mode 100644 index 000000000..03309a4fa --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RemoteObjectHandler.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.network.service.rmi; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * + * + * @author Paul Speed + */ +public class RemoteObjectHandler implements InvocationHandler { + + private final RmiRegistry rmi; + private final byte channel; + private final short objectId; + private final ClassInfo typeInfo; + private final Map methodIndex = new ConcurrentHashMap(); + + public RemoteObjectHandler( RmiRegistry rmi, byte channel, short objectId, ClassInfo typeInfo ) { + this.rmi = rmi; + this.channel = channel; + this.objectId = objectId; + this.typeInfo = typeInfo; + } + + protected MethodInfo getMethodInfo( Method method ) { + MethodInfo mi = methodIndex.get(method); + if( mi == null ) { + mi = typeInfo.getMethod(method); + if( mi == null ) { + mi = MethodInfo.NULL_INFO; + } + methodIndex.put(method, mi); + } + return mi == MethodInfo.NULL_INFO ? null : mi; + } + + @Override + public Object invoke(Object o, Method method, Object[] os) throws Throwable { + MethodInfo mi = getMethodInfo(method); + if( mi == null ) { + // Try to invoke locally + return method.invoke(this, os); + } + return rmi.invokeRemote(channel, objectId, mi.getId(), mi.getCallType(), os); + } + + @Override + public String toString() { + return "RemoteObject[#" + objectId + ", " + typeInfo.getName() + "]"; + } +} diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiClientService.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiClientService.java new file mode 100644 index 000000000..437a697f2 --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiClientService.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.network.service.rmi; + +import com.jme3.network.MessageConnection; +import com.jme3.network.service.AbstractClientService; +import com.jme3.network.service.ClientServiceManager; +import com.jme3.network.service.rpc.RpcClientService; +import java.util.ArrayList; +import java.util.List; + + +/** + * + * + * @author Paul Speed + */ +public class RmiClientService extends AbstractClientService { + + private RpcClientService rpc; + private byte defaultChannel; + private short rmiObjectId; + private RmiRegistry rmi; + private volatile boolean isStarted = false; + + private final List pending = new ArrayList(); + + public RmiClientService() { + this((short)-1, (byte)MessageConnection.CHANNEL_DEFAULT_RELIABLE); + } + + public RmiClientService( short rmiObjectId, byte defaultChannel ) { + this.defaultChannel = defaultChannel; + this.rmiObjectId = rmiObjectId; + } + + public void share( T object, Class type ) { + share(defaultChannel, object, type); + } + + public void share( byte channel, T object, Class type ) { + share(channel, type.getName(), object, type); + } + + public void share( String name, T object, Class type ) { + share(defaultChannel, name, object, type); + } + + public void share( byte channel, String name, T object, Class type ) { + if( !isStarted ) { + synchronized(pending) { + if( !isStarted ) { + pending.add(new ObjectInfo(channel, name, object, type)); + return; + } + } + } + + // Else we can add it directly. + rmi.share(channel, name, object, type); + } + + public T getRemoteObject( Class type ) { + return rmi.getRemoteObject(type); + } + + public T getRemoteObject( String name, Class type ) { + return rmi.getRemoteObject(name, type); + } + + @Override + protected void onInitialize( ClientServiceManager s ) { + rpc = getService(RpcClientService.class); + if( rpc == null ) { + throw new RuntimeException("RmiClientService requires RpcClientService"); + } + + // Register it now so that it is available when the + // server starts to send us stuff. Waiting until start() + // is too late in this case. + rmi = new RmiRegistry(rpc.getRpcConnection(), rmiObjectId, defaultChannel); + } + + @Override + public void start() { + super.start(); + + // Register all of the classes that have been waiting. + synchronized(pending) { + for( ObjectInfo info : pending ) { + rmi.share(info.channel, info.name, info.object, info.type); + } + pending.clear(); + isStarted = true; + } + } + + private class ObjectInfo { + byte channel; + String name; + Object object; + Class type; + + public ObjectInfo( byte channel, String name, Object object, Class type ) { + this.channel = channel; + this.name = name; + this.object = object; + this.type = type; + } + + @Override + public String toString() { + return "ObjectInfo[" + channel + ", " + name + ", " + object + ", " + type + "]"; + } + } +} + diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiContext.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiContext.java new file mode 100644 index 000000000..47d115d5b --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiContext.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.network.service.rmi; + +import com.jme3.network.HostedConnection; + + +/** + * Keeps track of the current connection performing a particular + * RMI call. RMI-based services can use this to find out which + * connection is calling a particular method without having to + * pass additional problematic data on the method calls. + * + * @author Paul Speed + */ +public class RmiContext { + private static final ThreadLocal connection = new ThreadLocal(); + + public static HostedConnection getRmiConnection() { + return connection.get(); + } + + static void setRmiConnection( HostedConnection conn ) { + connection.set(conn); + } +} diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiHostedService.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiHostedService.java new file mode 100644 index 000000000..647fe4fb6 --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiHostedService.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.network.service.rmi; + +import com.jme3.network.HostedConnection; +import com.jme3.network.MessageConnection; +import com.jme3.network.Server; +import com.jme3.network.serializing.Serializer; +import com.jme3.network.service.AbstractHostedService; +import com.jme3.network.service.HostedServiceManager; +import com.jme3.network.service.rpc.RpcHostedService; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * + * + * @author Paul Speed + */ +public class RmiHostedService extends AbstractHostedService { + + static final Logger log = Logger.getLogger(RpcHostedService.class.getName()); + + public static final String ATTRIBUTE_NAME = "rmi"; + + private RpcHostedService rpcService; + private short rmiId; + private byte defaultChannel; + private boolean autoHost; + private final Map globalShares = new ConcurrentHashMap(); + + public RmiHostedService() { + this((short)-1, (byte)MessageConnection.CHANNEL_DEFAULT_RELIABLE, true); + } + + public RmiHostedService( short rmiId, byte defaultChannel, boolean autoHost ) { + this.rmiId = rmiId; + this.defaultChannel = defaultChannel; + this.autoHost = autoHost; + + Serializer.registerClasses(ClassInfo.class, MethodInfo.class); + } + + public void shareGlobal( T object, Class type ) { + shareGlobal(defaultChannel, type.getName(), object, type); + } + + public void shareGlobal( String name, T object, Class type ) { + shareGlobal(defaultChannel, name, object, type); + } + + public void shareGlobal( byte channel, String name, T object, Class type ) { + GlobalShare share = new GlobalShare(channel, object, type); + GlobalShare existing = globalShares.put(name, share); + if( existing != null ) { + // Shouldn't need to do anything actually. + } + + // Go through all of the children + for( HostedConnection conn : getServer().getConnections() ) { + RmiRegistry child = getRmiRegistry(conn); + if( child == null ) { + continue; + } + child.share(channel, name, object, type); + } + } + + public void setAutoHost( boolean b ) { + this.autoHost = b; + } + + public boolean getAutoHost() { + return autoHost; + } + + public RmiRegistry getRmiRegistry( HostedConnection hc ) { + return hc.getAttribute(ATTRIBUTE_NAME); + } + + /** + * Sets up RMI hosting services for the hosted connection allowing + * getRmiRegistry() to return a valid RmiRegistry object. + * This method is called automatically for all new connections if + * autohost is set to true. + */ + public void startHostingOnConnection( HostedConnection hc ) { + if( log.isLoggable(Level.FINEST) ) { + log.log(Level.FINEST, "startHostingOnConnection:{0}", hc); + } + RmiRegistry rmi = new RmiRegistry(hc, rpcService.getRpcConnection(hc), + rmiId, defaultChannel); + hc.setAttribute(ATTRIBUTE_NAME, rmi); + + // Register any global shares + for( Map.Entry e : globalShares.entrySet() ) { + GlobalShare share = e.getValue(); + rmi.share(share.channel, e.getKey(), share.object, share.type); + } + } + + /** + * Removes any RMI hosting services associated with the specified + * connection. Calls to getRmiRegistry() will return null for + * this connection. + * This method is called automatically for all leaving connections if + * autohost is set to true. + */ + public void stopHostingOnConnection( HostedConnection hc ) { + RmiRegistry rmi = hc.getAttribute(ATTRIBUTE_NAME); + if( rmi == null ) { + return; + } + if( log.isLoggable(Level.FINEST) ) { + log.log(Level.FINEST, "stopHostingOnConnection:{0}", hc); + } + hc.setAttribute(ATTRIBUTE_NAME, null); + //rpc.close(); + } + + @Override + protected void onInitialize( HostedServiceManager s ) { + this.rpcService = getService(RpcHostedService.class); + if( rpcService == null ) { + throw new RuntimeException("RmiHostedService requires RpcHostedService"); + } + } + + /** + * Called internally when a new connection is detected for + * the server. If the current autoHost property is true then + * startHostingOnConnection(hc) is called. + */ + @Override + public void connectionAdded(Server server, HostedConnection hc) { + if( log.isLoggable(Level.FINEST) ) { + log.log(Level.FINEST, "connectionAdded({0}, {1})", new Object[]{server, hc}); + } + if( autoHost ) { + startHostingOnConnection(hc); + } + } + + /** + * Called internally when an existing connection is leaving + * the server. If the current autoHost property is true then + * stopHostingOnConnection(hc) is called. + */ + @Override + public void connectionRemoved(Server server, HostedConnection hc) { + if( log.isLoggable(Level.FINEST) ) { + log.log(Level.FINEST, "connectionRemoved({0}, {1})", new Object[]{server, hc}); + } + if( autoHost ) { + stopHostingOnConnection(hc); + } + } + + private class GlobalShare { + byte channel; + Object object; + Class type; + + public GlobalShare( byte channel, Object object, Class type ) { + this.channel = channel; + this.object = object; + this.type = type; + } + } +} diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java new file mode 100644 index 000000000..6d1722307 --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jme3.network.service.rmi; + +import com.jme3.network.HostedConnection; +import com.jme3.network.MessageConnection; +import com.jme3.network.service.rpc.RpcConnection; +import com.jme3.network.service.rpc.RpcHandler; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * + * + * @author Paul Speed + */ +public class RmiRegistry { + + static final Logger log = Logger.getLogger(RmiRegistry.class.getName()); + + // RPC IDs for calling our remote endpoint + private static final short NEW_CLASS = 0; + private static final short ADD_OBJECT = 1; + private static final short REMOVE_OBJECT = 2; + + private RpcConnection rpc; + private short rmiId; + private byte defaultChannel; + private final RmiHandler rmiHandler = new RmiHandler(); + private final ClassInfoRegistry classCache = new ClassInfoRegistry(); + private final AtomicInteger nextObjectId = new AtomicInteger(); + + private final ObjectIndex local = new ObjectIndex(); + private final ObjectIndex remote = new ObjectIndex(); + + // Only used on the server to provide thread-local context for + // local RMI calls. + private HostedConnection context; + + public RmiRegistry( RpcConnection rpc, short rmiId, byte defaultChannel ) { + this(null, rpc, rmiId, defaultChannel); + } + + public RmiRegistry( HostedConnection context, RpcConnection rpc, short rmiId, byte defaultChannel ) { + this.context = context; + this.rpc = rpc; + this.rmiId = rmiId; + this.defaultChannel = defaultChannel; + rpc.registerHandler(rmiId, rmiHandler); + } + + public void share( T object, Class type ) { + share(defaultChannel, object, type); + } + + public void share( byte channel, T object, Class type ) { + share(channel, type.getName(), object, type); + } + + public void share( String name, T object, Class type ) { + share(defaultChannel, name, object, type); + } + + public void share( byte channel, String name, T object, Class type ) { + + ClassInfo typeInfo = classCache.getClassInfo(type); + + local.lock.writeLock().lock(); + try { + + // First see if we've told the remote end about this class + // before + if( local.classes.put(typeInfo.getId(), typeInfo) == null ) { + // It's new + rpc.callAsync(defaultChannel, rmiId, NEW_CLASS, typeInfo); + + // Because type info IDs are global to the class cache, + // we could in theory keep a global index that we broadcast + // on first connection setup... we need only prepopulate + // the index in that case. + } + + // See if we already shared an object under that name + SharedObject existing = local.byName.remove(name); + if( existing != null ) { + local.byId.remove(existing.objectId); + rpc.removeHandler(existing.objectId, rmiHandler); + + // Need to delete the old one from the remote end + rpc.callAsync(defaultChannel, rmiId, REMOVE_OBJECT, existing.objectId); + + // We don't reuse the ID because it's kind of dangerous. + // Churning through a new ID is our safety net for accidents. + } + + SharedObject newShare = new SharedObject(name, object, type, typeInfo); + local.byName.put(name, newShare); + local.byId.put(newShare.objectId, newShare); + + // Make sure we are setup to receive the remote method calls through + // the RPC service + rpc.registerHandler(newShare.objectId, rmiHandler); + + // Let the other end know + rpc.callAsync(defaultChannel, rmiId, ADD_OBJECT, channel, newShare.objectId, name, typeInfo.getId()); + + // We send the ADD_OBJECT to the other end before releasing the + // lock to avoid a potential inconsistency if two threads try to + // jam the same name at the same time. Otherwise, if the timing were + // right, the remove for one object could get there before its add. + + } finally { + local.lock.writeLock().unlock(); + } + } + + /** + * Returns an object that was previously registered with share(). + */ + public T getSharedObject( Class type ) { + return getSharedObject(type.getName(), type); + } + + /** + * Returns an object that was previously registered with share(). + */ + public T getSharedObject( String name, Class type ) { + local.lock.readLock().lock(); + try { + return type.cast(local.byName.get(name)); + } finally { + local.lock.readLock().unlock(); + } + } + + public T getRemoteObject( Class type ) { + return getRemoteObject(type.getName(), type); + } + + public T getRemoteObject( String name, Class type ) { + remote.lock.readLock().lock(); + try { + return type.cast(remote.byName.get(name)); + } finally { + remote.lock.readLock().unlock(); + } + } + + protected void addRemoteClass( ClassInfo info ) { + if( remote.classes.put(info.getId(), info) != null ) { + throw new RuntimeException("Error class already exists for ID:" + info.getId()); + } + } + + protected void removeRemoteObject( short objectId ) { + if( log.isLoggable(Level.FINEST) ) { + log.log(Level.FINEST, "removeRemoteObject({0})", objectId); + } + throw new UnsupportedOperationException("Removal not yet implemented."); + } + + protected void addRemoteObject( byte channel, short objectId, String name, ClassInfo typeInfo ) { + if( log.isLoggable(Level.FINEST) ) { + log.finest("addRemoveObject(" + objectId + ", " + name + ", " + typeInfo + ")"); + } + remote.lock.writeLock().lock(); + try { + Object existing = remote.byName.get(name); + if( existing != null ) { + throw new RuntimeException("Object already registered for:" + name); + } + + RemoteObjectHandler remoteHandler = new RemoteObjectHandler(this, channel, objectId, typeInfo); + + Object remoteObject = Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {typeInfo.getType()}, + remoteHandler); + + remote.byName.put(name, remoteObject); + remote.byId.put(objectId, remoteObject); + } finally { + remote.lock.writeLock().unlock(); + } + } + + protected Object invokeRemote( byte channel, short objectId, short procId, CallType callType, Object[] args ) { + if( log.isLoggable(Level.FINEST) ) { + log.finest("invokeRemote(" + channel + ", " + objectId + ", " + procId + ", " + + callType + ", " + (args == null ? "null" : Arrays.asList(args)) + ")"); + } + switch( callType ) { + case Asynchronous: + log.finest("Sending reliable asynchronous."); + rpc.callAsync(channel, objectId, procId, args); + return null; + case Unreliable: + log.finest("Sending unreliable asynchronous."); + rpc.callAsync((byte)MessageConnection.CHANNEL_DEFAULT_UNRELIABLE, objectId, procId, args); + return null; + default: + case Synchronous: + log.finest("Sending synchronous."); + Object result = rpc.callAndWait(channel, objectId, procId, args); + if( log.isLoggable(Level.FINEST) ) { + log.finest("->got:" + result); + } + return result; + } + } + + /** + * Handle remote object registry updates from the other end. + */ + protected void rmiUpdate( short procId, Object[] args ) { + if( log.isLoggable(Level.FINEST) ) { + log.finest("rmiUpdate(" + procId + ", " + Arrays.asList(args) + ")"); + } + switch( procId ) { + case NEW_CLASS: + addRemoteClass((ClassInfo)args[0]); + break; + case REMOVE_OBJECT: + removeRemoteObject((Short)args[0]); + break; + case ADD_OBJECT: + ClassInfo info = remote.classes.get((Short)args[3]); + addRemoteObject((Byte)args[0], (Short)args[1], (String)args[2], info); + break; + } + } + + /** + * Handle the actual remote object method calls. + */ + protected Object invokeLocal( short objectId, short procId, Object[] args ) { + // Actually could use a regular concurrent map for this + + // Only lock the local registry during lookup and + // not invocation. It prevents a deadlock if the invoked method + // tries to share an object. It should be safe. + SharedObject share = local.byId.get(objectId); + local.lock.readLock().lock(); + try { + share = local.byId.get(objectId); + } finally { + local.lock.readLock().unlock(); + } + + try { + RmiContext.setRmiConnection(context); + return share.invoke(procId, args); + } finally { + RmiContext.setRmiConnection(null); + } + } + + private class SharedObject { + private final short objectId; + private final String name; + private final Object object; + private final Class type; + private final ClassInfo classInfo; + + public SharedObject( String name, Object object, Class type, ClassInfo classInfo ) { + this.objectId = (short)nextObjectId.incrementAndGet(); + this.name = name; + this.object = object; + this.type = type; + this.classInfo = classInfo; + } + + public Object invoke( short procId, Object[] args ) { + return classInfo.getMethod(procId).invoke(object, args); + } + } + + private class RmiHandler implements RpcHandler { + @Override + public Object call( RpcConnection conn, short objectId, short procId, Object... args ) { + if( objectId == rmiId ) { + rmiUpdate(procId, args); + return null; + } else { + return invokeLocal(objectId, procId, args); + } + } + } + + /** + * Keeps a coincident index between short ID, name, and related class info. + * There will be one of these to track our local objects and one to track + * the remote objects and a lock that can guard them. + */ + private class ObjectIndex { + final Map byName = new HashMap(); + final Map byId = new HashMap(); + final Map classes = new HashMap(); + final ReadWriteLock lock = new ReentrantReadWriteLock(); + + public ObjectIndex() { + } + } + +} + + From 3a4624a5fe4684d8df5260c4d5320a762c65b3f7 Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Mon, 16 Nov 2015 02:38:45 -0500 Subject: [PATCH 12/16] Added a bunch of javadoc and changed the names of one of the method sets to be a little less confusing. --- .../network/service/rmi/Asynchronous.java | 4 +- .../jme3/network/service/rmi/CallType.java | 21 ++++++- .../jme3/network/service/rmi/ClassInfo.java | 3 +- .../service/rmi/ClassInfoRegistry.java | 3 +- .../jme3/network/service/rmi/MethodInfo.java | 3 +- .../service/rmi/RemoteObjectHandler.java | 2 +- .../network/service/rmi/RmiClientService.java | 48 +++++++++++++++ .../jme3/network/service/rmi/RmiContext.java | 6 +- .../network/service/rmi/RmiHostedService.java | 59 +++++++++++++++++++ .../jme3/network/service/rmi/RmiRegistry.java | 53 +++++++++++++++-- 10 files changed, 189 insertions(+), 13 deletions(-) diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/Asynchronous.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/Asynchronous.java index 8deada70b..a26d09635 100644 --- a/jme3-networking/src/main/java/com/jme3/network/service/rmi/Asynchronous.java +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/Asynchronous.java @@ -41,7 +41,9 @@ import static java.lang.annotation.RetentionPolicy.*; /** * Indicates that a given method should be executed asynchronously * through the RMI service. This must annotate the method on the - * shared interface for it to have an effect. + * shared interface for it to have an effect. If reliable=false + * is specified then remote method invocation is done over UDP + * instead of TCP, ie: unreliably... but faster. * * @author Paul Speed */ diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/CallType.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/CallType.java index 6b218891d..5167bd7eb 100644 --- a/jme3-networking/src/main/java/com/jme3/network/service/rmi/CallType.java +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/CallType.java @@ -34,10 +34,27 @@ package com.jme3.network.service.rmi; /** - * + * Internal type denoting the type of call to make when remotely + * invoking methods. * * @author Paul Speed */ public enum CallType { - Synchronous, Asynchronous, Unreliable + /** + * Caller will block until a response is received and returned. + */ + Synchronous, + + /** + * Caller does not block or wait for a response. The other end + * of the connection will also not send one. + */ + Asynchronous, + + /** + * Similar to asynchronous in that no response is expected or sent + * but differs in that the call will be sent over UDP and so may + * not make it to the other end. + */ + Unreliable } diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfo.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfo.java index 88d91a686..4f7f7de32 100644 --- a/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfo.java +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfo.java @@ -39,7 +39,8 @@ import java.util.List; /** - * + * Internal information about a shared class. This is the information + * that is sent over the wire for shared types. * * @author Paul Speed */ diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfoRegistry.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfoRegistry.java index 557948042..addc03593 100644 --- a/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfoRegistry.java +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfoRegistry.java @@ -40,7 +40,8 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; /** - * + * Internal registry of shared types and their ClassInfo and MethodInfo + * objects. * * @author Paul Speed */ diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/MethodInfo.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/MethodInfo.java index a0138556d..9b324a6c3 100644 --- a/jme3-networking/src/main/java/com/jme3/network/service/rmi/MethodInfo.java +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/MethodInfo.java @@ -39,7 +39,8 @@ import javax.jws.Oneway; /** - * + * Internal information about shared methods. This is part of the data that + * is passed over the wire when an object is shared. * * @author Paul Speed */ diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RemoteObjectHandler.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RemoteObjectHandler.java index 03309a4fa..f42ada84f 100644 --- a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RemoteObjectHandler.java +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RemoteObjectHandler.java @@ -39,7 +39,7 @@ import java.util.concurrent.ConcurrentHashMap; /** - * + * Used internally to remotely invoke methods on RMI shared objects. * * @author Paul Speed */ diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiClientService.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiClientService.java index 437a697f2..ea1506f72 100644 --- a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiClientService.java +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiClientService.java @@ -41,7 +41,23 @@ import java.util.List; /** + * A service that can be added to the client to support a simple + * shared objects protocol. * + *

Objects are shared by adding them to the RmiRegistry with one of the + * share() methods. Shared objects must have a separate interface and implementation. + * The interface is what the other end of the connection will use to interact + * with the object and that interface class must be available on both ends of + * the connection. The implementing class need only be on the sharing end.

+ * + *

Shared objects can be accessed on the other end of the connection by + * using one of the RmiRegistry's getRemoteObject() methods. These can be + * used to lookup an object by class if it is a shared singleton or by name + * if it was registered with a name.

+ * + *

Note: This RMI implementation is not as advanced as Java's regular + * RMI as it won't marshall shared references, ie: you can't pass + * a shared objects as an argument to another shared object's method.

* * @author Paul Speed */ @@ -64,18 +80,42 @@ public class RmiClientService extends AbstractClientService { this.rmiObjectId = rmiObjectId; } + /** + * Shares the specified object with the server and associates it with the + * specified type. Objects shared in this way are available in the connection-specific + * RMI registry on the server and are not available to other connections. + */ public void share( T object, Class type ) { share(defaultChannel, object, type); } + /** + * Shares the specified object with the server and associates it with the + * specified type. Objects shared in this way are available in the connection-specific + * RMI registry on the server and are not available to other connections. + * All object related communication will be done over the specified connection + * channel. + */ public void share( byte channel, T object, Class type ) { share(channel, type.getName(), object, type); } + /** + * Shares the specified object with the server and associates it with the + * specified name. Objects shared in this way are available in the connection-specific + * RMI registry on the server and are not available to other connections. + */ public void share( String name, T object, Class type ) { share(defaultChannel, name, object, type); } + /** + * Shares the specified object with the server and associates it with the + * specified name. Objects shared in this way are available in the connection-specific + * RMI registry on the server and are not available to other connections. + * All object related communication will be done over the specified connection + * channel. + */ public void share( byte channel, String name, T object, Class type ) { if( !isStarted ) { synchronized(pending) { @@ -90,10 +130,18 @@ public class RmiClientService extends AbstractClientService { rmi.share(channel, name, object, type); } + /** + * Looks up a remote object on the server by type and returns a local proxy to the + * remote object that was shared on the other end of the network connection. + */ public T getRemoteObject( Class type ) { return rmi.getRemoteObject(type); } + /** + * Looks up a remote object on the server by name and returns a local proxy to the + * remote object that was shared on the other end of the network connection. + */ public T getRemoteObject( String name, Class type ) { return rmi.getRemoteObject(name, type); } diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiContext.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiContext.java index 47d115d5b..547e83dd4 100644 --- a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiContext.java +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiContext.java @@ -45,7 +45,11 @@ import com.jme3.network.HostedConnection; */ public class RmiContext { private static final ThreadLocal connection = new ThreadLocal(); - + + /** + * Returns the HostedConnection that is responsible for any + * RMI-related calls on this thread. + */ public static HostedConnection getRmiConnection() { return connection.get(); } diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiHostedService.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiHostedService.java index 647fe4fb6..997658a77 100644 --- a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiHostedService.java +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiHostedService.java @@ -46,7 +46,30 @@ import java.util.logging.Logger; /** + * A service that can be added to the host to support a simple + * shared objects protocol. * + *

Objects are shared by adding them to the RmiRegistry with one of the + * share() methods. Shared objects must have a separate interface and implementation. + * The interface is what the other end of the connection will use to interact + * with the object and that interface class must be available on both ends of + * the connection. The implementing class need only be on the sharing end.

+ * + *

Shared objects can be accessed on the other end of the connection by + * using one of the RmiRegistry's getRemoteObject() methods. These can be + * used to lookup an object by class if it is a shared singleton or by name + * if it was registered with a name.

+ * + *

On the hosting side, a special shardGlobal() method is provided that + * will register shared objects that will automatically be provided to every + * new joining client and they will all be calling the same server-side instance. + * Normally, shared objects themselves are connection specific and handled + * at the connection layer. The shareGlobal() space is a way to have global + * resources passed directly though the need is relatively rare.

+ * + *

Note: This RMI implementation is not as advanced as Java's regular + * RMI as it won't marshall shared references, ie: you can't pass + * a shared objects as an argument to another shared object's method.

* * @author Paul Speed */ @@ -74,14 +97,34 @@ public class RmiHostedService extends AbstractHostedService { Serializer.registerClasses(ClassInfo.class, MethodInfo.class); } + /** + * Shares a server-wide object associated with the specified type. All connections + * with RMI hosting started will have access to this shared object as soon as they + * connect and they will all share the same instance. It is up to the shared object + * to handle any multithreading that might be required. + */ public void shareGlobal( T object, Class type ) { shareGlobal(defaultChannel, type.getName(), object, type); } + /** + * Shares a server-wide object associated with the specified name. All connections + * with RMI hosting started will have access to this shared object as soon as they + * connect and they will all share the same instance. It is up to the shared object + * to handle any multithreading that might be required. + */ public void shareGlobal( String name, T object, Class type ) { shareGlobal(defaultChannel, name, object, type); } + /** + * Shares a server-wide object associated with the specified name over the specified + * channel. All connections with RMI hosting started will have access to this shared + * object as soon as they connect and they will all share the same instance. It is up + * to the shared object to handle any multithreading that might be required. + * All network communcation associated with the shared object will be done over + * the specified channel. + */ public void shareGlobal( byte channel, String name, T object, Class type ) { GlobalShare share = new GlobalShare(channel, object, type); GlobalShare existing = globalShares.put(name, share); @@ -99,14 +142,30 @@ public class RmiHostedService extends AbstractHostedService { } } + /** + * Set to true if all new connections should automatically have RMI hosting started. + * Set to false if the game-specific connection setup will call startHostingOnConnection() + * after some connection setup is done (for example, logging in). Note: generally + * is is safe to autohost RMI as long as callers are careful about what they've added + * using shareGlobal(). One reasonable use-case is to shareGlobal() some kind of login + * service and nothing else. All other shared objects would then be added as connection + * specific objects during successful login processing. + */ public void setAutoHost( boolean b ) { this.autoHost = b; } + /** + * Returns true if RMI hosting is automatically started for all new connections. + */ public boolean getAutoHost() { return autoHost; } + /** + * Returns the RMI registry for the specific HostedConection. Each connection + * has its own registry with its own connection-specific shared objects. + */ public RmiRegistry getRmiRegistry( HostedConnection hc ) { return hc.getAttribute(ATTRIBUTE_NAME); } diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java index 6d1722307..c04a8c7b3 100644 --- a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java @@ -87,18 +87,45 @@ public class RmiRegistry { rpc.registerHandler(rmiId, rmiHandler); } + /** + * Exposes the specified object to the other end of the connection as + * the specified interface type. The object can be looked up by type + * on the other end. + */ public void share( T object, Class type ) { share(defaultChannel, object, type); } + /** + * Exposes, through a specific connection channel, the specified object + * to the other end of the connection as the specified interface type. + * The object can be looked up by type on the other end. + * The specified channel will be used for all network communication + * specific to this object. + */ public void share( byte channel, T object, Class type ) { share(channel, type.getName(), object, type); } + /** + * Exposes the specified object to the other end of the connection as + * the specified interface type and associates it with the specified name. + * The object can be looked up by the associated name on the other end of + * the connection. + */ public void share( String name, T object, Class type ) { share(defaultChannel, name, object, type); } + /** + * Exposes, through a specific connection channel, the specified object to + * the other end of the connection as the specified interface type and associates + * it with the specified name. + * The object can be looked up by the associated name on the other end of + * the connection. + * The specified channel will be used for all network communication + * specific to this object. + */ public void share( byte channel, String name, T object, Class type ) { ClassInfo typeInfo = classCache.getClassInfo(type); @@ -153,16 +180,18 @@ public class RmiRegistry { } /** - * Returns an object that was previously registered with share(). + * Returns a local object that was previously registered with share() using + * just type registration. */ - public T getSharedObject( Class type ) { - return getSharedObject(type.getName(), type); + public T getLocalObject( Class type ) { + return getLocalObject(type.getName(), type); } /** - * Returns an object that was previously registered with share(). + * Returns a local object that was previously registered with share() using + * name registration. */ - public T getSharedObject( String name, Class type ) { + public T getLocalObject( String name, Class type ) { local.lock.readLock().lock(); try { return type.cast(local.byName.get(name)); @@ -171,10 +200,24 @@ public class RmiRegistry { } } + /** + * Looks up a remote object by type and returns a local proxy to the remote object + * that was shared on the other end of the network connection. If this is called + * from a client then it is accessing a shared object registered on the server. + * If this is called from the server then it is accessing a shared object registered + * on the client. + */ public T getRemoteObject( Class type ) { return getRemoteObject(type.getName(), type); } + /** + * Looks up a remote object by name and returns a local proxy to the remote object + * that was shared on the other end of the network connection. If this is called + * from a client then it is accessing a shared object registered on the server. + * If this is called from the server then it is accessing a shared object registered + * on the client. + */ public T getRemoteObject( String name, Class type ) { remote.lock.readLock().lock(); try { From 31cab674b3cf6c8e0ad6c8e47560f2150167ecff Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Mon, 16 Nov 2015 03:03:53 -0500 Subject: [PATCH 13/16] Removing the dodgy 'optimization' that broke some people and caused other 'makeup' changes to better support the dodginess. (And I do realize I have the benefit of analyzing the aftermath, hindsight is 20/20, etc.) Included a big long comment about the right way to implement this optimization. --- .../src/main/java/com/jme3/scene/Node.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/jme3-core/src/main/java/com/jme3/scene/Node.java b/jme3-core/src/main/java/com/jme3/scene/Node.java index 5edfa021b..b388a1e5a 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Node.java +++ b/jme3-core/src/main/java/com/jme3/scene/Node.java @@ -570,6 +570,35 @@ public class Node extends Spatial { // optimization: try collideWith BoundingVolume to avoid possibly redundant tests on children // number 4 in condition is somewhat arbitrary. When there is only one child, the boundingVolume test is redundant at all. // The idea is when there are few children, it can be too expensive to test boundingVolume first. + /* + I'm removing this change until some issues can be addressed and I really + think it needs to be implemented a better way anyway. + + First, it causes issues for anyone doing collideWith() with BoundingVolumes + and expecting it to trickle down to the children. For example, children + with BoundingSphere bounding volumes and collideWith(BoundingSphere). Doing + a collision check at the parent level then has to do a BoundingSphere to BoundingBox + collision which isn't resolved. (Having to come up with a collision point in that + case is tricky and the first sign that this is the wrong approach.) + + Second, the rippling changes this caused to 'optimize' collideWith() for this + special use-case are another sign that this approach was a bit dodgy. The whole + idea of calculating a full collision just to see if the two shapes collide at all + is very wasteful. + + A proper implementation should support a simpler boolean check that doesn't do + all of that calculation. For example, if 'other' is also a BoundingVolume (ie: 99.9% + of all non-Ray cases) then a direct BV to BV intersects() test can be done. So much + faster. And if 'other' _is_ a Ray then the BV.intersects(Ray) call can be done. + + I don't have time to do it right now but I'll at least un-break a bunch of peoples' + code until it can be 'optimized' properly. Hopefully it's not too late to back out + the other dodgy ripples this caused. -pspeed (hindsight-expert ;)) + + Note: the code itself is relatively simple to implement but I don't have time to + a) test it, and b) see if '> 4' is still a decent check for it. Could be it's fast + enough to do all the time for > 1. + if (children.size() > 4) { BoundingVolume bv = this.getWorldBound(); @@ -578,6 +607,7 @@ public class Node extends Spatial { // collideWith without CollisionResults parameter used to avoid allocation when possible if (bv.collideWith(other) == 0) return 0; } + */ for (Spatial child : children.getArray()){ total += child.collideWith(other, results); } From cde35a005afa4a14a7833874d1e77d649631a7ff Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Tue, 17 Nov 2015 01:46:38 -0500 Subject: [PATCH 14/16] Just fixing an indent. --- jme3-core/src/main/java/com/jme3/scene/Node.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/scene/Node.java b/jme3-core/src/main/java/com/jme3/scene/Node.java index b388a1e5a..74b14188d 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Node.java +++ b/jme3-core/src/main/java/com/jme3/scene/Node.java @@ -195,7 +195,7 @@ public class Node extends Spatial { void invalidateUpdateList() { updateListValid = false; if ( parent != null ) { - parent.invalidateUpdateList(); + parent.invalidateUpdateList(); } } From d57147e3929f35c49fcefcc29cab08263c761a33 Mon Sep 17 00:00:00 2001 From: Paul Speed Date: Tue, 17 Nov 2015 02:39:46 -0500 Subject: [PATCH 15/16] A small optimization. The BitmapTextPage does not require custom updates so it now signifies that. --- jme3-core/src/main/java/com/jme3/font/BitmapTextPage.java | 1 + 1 file changed, 1 insertion(+) diff --git a/jme3-core/src/main/java/com/jme3/font/BitmapTextPage.java b/jme3-core/src/main/java/com/jme3/font/BitmapTextPage.java index 1edac967c..58dc87550 100644 --- a/jme3-core/src/main/java/com/jme3/font/BitmapTextPage.java +++ b/jme3-core/src/main/java/com/jme3/font/BitmapTextPage.java @@ -59,6 +59,7 @@ class BitmapTextPage extends Geometry { BitmapTextPage(BitmapFont font, boolean arrayBased, int page) { super("BitmapFont", new Mesh()); + setRequiresUpdates(false); setBatchHint(BatchHint.Never); if (font == null) { throw new IllegalArgumentException("font cannot be null."); From ad4634ce04cab3c41f88ed6c61efc62f4a2fe277 Mon Sep 17 00:00:00 2001 From: Nehon Date: Tue, 17 Nov 2015 14:54:59 +0100 Subject: [PATCH 16/16] .hdr files are now loaded in sRGB color space as there is no reason it should be loaded in linear space. --- .../src/plugins/java/com/jme3/texture/plugins/HDRLoader.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/jme3-core/src/plugins/java/com/jme3/texture/plugins/HDRLoader.java b/jme3-core/src/plugins/java/com/jme3/texture/plugins/HDRLoader.java index 611e208d4..8060197b2 100644 --- a/jme3-core/src/plugins/java/com/jme3/texture/plugins/HDRLoader.java +++ b/jme3-core/src/plugins/java/com/jme3/texture/plugins/HDRLoader.java @@ -308,9 +308,8 @@ public class HDRLoader implements AssetLoader { } in.close(); - dataStore.rewind(); - //TODO, HDR color space? considered linear here - return new Image(pixelFormat, width, height, dataStore, ColorSpace.Linear); + dataStore.rewind(); + return new Image(pixelFormat, width, height, dataStore, ColorSpace.sRGB); } public Object load(AssetInfo info) throws IOException {