Jelajahi Sumber

Merge branch 'master' into PBRisComing

Nehon 9 tahun lalu
induk
melakukan
aa067ef60d
21 mengubah file dengan 1788 tambahan dan 32 penghapusan
  1. 4 0
      jme3-android/src/main/java/com/jme3/system/android/JmeAndroidSystem.java
  2. 1 0
      jme3-core/src/main/java/com/jme3/font/BitmapTextPage.java
  3. 31 1
      jme3-core/src/main/java/com/jme3/scene/Node.java
  4. 4 0
      jme3-core/src/main/java/com/jme3/system/JmeSystemDelegate.java
  5. 5 0
      jme3-core/src/main/java/com/jme3/system/Platform.java
  6. 2 3
      jme3-core/src/plugins/java/com/jme3/texture/plugins/HDRLoader.java
  7. 73 7
      jme3-examples/src/main/java/jme3test/network/TestChatClient.java
  8. 74 0
      jme3-examples/src/main/java/jme3test/network/TestChatClientAndServer.java
  9. 99 14
      jme3-examples/src/main/java/jme3test/network/TestChatServer.java
  10. 20 2
      jme3-networking/src/main/java/com/jme3/network/message/SerializerRegistrationsMessage.java
  11. 56 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/Asynchronous.java
  12. 60 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/CallType.java
  13. 112 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfo.java
  14. 104 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfoRegistry.java
  15. 137 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/MethodInfo.java
  16. 87 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RemoteObjectHandler.java
  17. 195 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiClientService.java
  18. 60 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiContext.java
  19. 262 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiHostedService.java
  20. 387 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java
  21. 15 5
      jme3-networking/src/main/java/com/jme3/network/service/serializer/ClientSerializerRegistrationsService.java

+ 4 - 0
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;
         }

+ 1 - 0
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.");

+ 31 - 1
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();
         }
     }
 
@@ -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);
         }

+ 4 - 0
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 {

+ 5 - 0
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
      */

+ 2 - 3
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 {

+ 73 - 7
jme3-examples/src/main/java/jme3test/network/TestChatClient.java

@@ -32,6 +32,8 @@
 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;
@@ -51,11 +53,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,7 +92,20 @@ 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.addErrorListener(new ChatErrorListener());
         client.start();
+        
+        System.out.println("Started client:" + client);        
+    }
+
+    @Override
+    public void dispose() {
+        System.out.println("Chat window closing.");
+        super.dispose();
+        if( client.isConnected() ) {
+            client.close();
+        }
     }
 
     public static String getString(Component owner, String title, String message, String initialValue) {
@@ -99,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");
@@ -108,12 +128,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<Client> {
 
+        @Override
         public void messageReceived(Client source, Message m) {
             ChatMessage chat = (ChatMessage) m;
 
@@ -134,15 +165,50 @@ public class TestChatClient extends JFrame {
         }
     }
 
+    private class ChatClientStateListener implements ClientStateListener {
+
+        @Override
+        public void clientConnected(Client c) {
+            System.out.println("clientConnected(" + c + ")");
+        }
+
+        @Override
+        public void clientDisconnected(Client c, DisconnectInfo info) {
+            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<Client> {
+
+        @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);
+        }
+        
+    }
+    
     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();

+ 74 - 0
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();
+            }
+        }    
+    }
+}

+ 99 - 14
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,51 +52,134 @@ 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;
+    private boolean isRunning;
+    
+    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 boolean isRunning() {
+        return isRunning;
+    }
+    
+    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. 
+        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();
+        while( chatServer.isRunning ) {
+            synchronized (chatServer) {
+                chatServer.wait();
+            }
         }
     }
 
-    private static class ChatHandler implements MessageListener<HostedConnection> {
+    private class ChatHandler implements MessageListener<HostedConnection> {
 
         public ChatHandler() {
         }
 
+        @Override
         public void messageReceived(HostedConnection source, Message m) {
             if (m instanceof ChatMessage) {
                 // 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 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 +210,7 @@ public class TestChatServer {
             return message;
         }
 
+        @Override
         public String toString() {
             return name + ":" + message;
         }

+ 20 - 2
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();

+ 56 - 0
jme3-networking/src/main/java/com/jme3/network/service/rmi/Asynchronous.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 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.  If reliable=false
+ *  is specified then remote method invocation is done over UDP
+ *  instead of TCP, ie: unreliably... but faster.
+ *
+ *  @author    Paul Speed
+ */
+@Retention(value=RUNTIME)
+@Target(value=METHOD)
+public @interface Asynchronous {
+    boolean reliable() default true;
+}
+
+

+ 60 - 0
jme3-networking/src/main/java/com/jme3/network/service/rmi/CallType.java

@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+
+/**
+ *  Internal type denoting the type of call to make when remotely
+ *  invoking methods.
+ *
+ *  @author    Paul Speed
+ */
+public enum CallType {
+    /**
+     *  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
+}

+ 112 - 0
jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfo.java

@@ -0,0 +1,112 @@
+/*
+ * 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;
+
+
+/**
+ *  Internal information about a shared class.  This is the information
+ *  that is sent over the wire for shared types.
+ *
+ *  @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<MethodInfo> result = new ArrayList<MethodInfo>();
+        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 + "]";
+    }
+}

+ 104 - 0
jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfoRegistry.java

@@ -0,0 +1,104 @@
+/*
+ * 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;
+
+
+/**
+ *  Internal registry of shared types and their ClassInfo and MethodInfo
+ *  objects.
+ *
+ *  @author    Paul Speed
+ */
+public class ClassInfoRegistry {
+ 
+    //private final LoadingCache<Class, ClassInfo> cache;  // Guava version
+    private final Map<Class, ClassInfo> cache = new HashMap<Class, ClassInfo>();
+    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<Class, ClassInfo> {
+        @Override
+        public ClassInfo load( Class type ) {
+            return new ClassInfo((short)nextClassId.getAndIncrement(), type);
+        }
+    }*/ 
+}

+ 137 - 0
jme3-networking/src/main/java/com/jme3/network/service/rmi/MethodInfo.java

@@ -0,0 +1,137 @@
+/*
+ * 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;
+
+
+/**
+ *  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
+ */
+@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 + "]";
+    }
+}

+ 87 - 0
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;
+
+
+/**
+ *  Used internally to remotely invoke methods on RMI shared objects.
+ *
+ *  @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<Method, MethodInfo> methodIndex = new ConcurrentHashMap<Method, MethodInfo>();
+
+    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() + "]";
+    }   
+}

+ 195 - 0
jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiClientService.java

@@ -0,0 +1,195 @@
+/*
+ * 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;
+
+
+/**
+ *  A service that can be added to the client to support a simple
+ *  shared objects protocol.
+ *
+ *  <p>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.</p>
+ *
+ *  <p>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.</p>
+ * 
+ *  <p>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.</p>
+ *
+ *  @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<ObjectInfo> pending = new ArrayList<ObjectInfo>();   
+    
+    public RmiClientService() {
+        this((short)-1, (byte)MessageConnection.CHANNEL_DEFAULT_RELIABLE);
+    }
+    
+    public RmiClientService( short rmiObjectId, byte defaultChannel ) {
+        this.defaultChannel = defaultChannel;
+        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 <T> void share( T object, Class<? super T> 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 <T> void share( byte channel, T object, Class<? super T> 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 <T> void share( String name, T object, Class<? super T> 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 <T> void share( byte channel, String name, T object, Class<? super T> 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);
+    }
+
+    /**
+     *  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> T getRemoteObject( Class<T> 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> T getRemoteObject( String name, Class<T> 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 + "]";
+        }
+    }   
+}
+

+ 60 - 0
jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiContext.java

@@ -0,0 +1,60 @@
+/*
+ * 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<HostedConnection> connection = new ThreadLocal<HostedConnection>();
+ 
+    /**
+     *  Returns the HostedConnection that is responsible for any
+     *  RMI-related calls on this thread.
+     */   
+    public static HostedConnection getRmiConnection() {
+        return connection.get();
+    }
+    
+    static void setRmiConnection( HostedConnection conn ) {
+        connection.set(conn);
+    }
+}

+ 262 - 0
jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiHostedService.java

@@ -0,0 +1,262 @@
+/*
+ * 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;
+
+
+/**
+ *  A service that can be added to the host to support a simple
+ *  shared objects protocol.
+ *
+ *  <p>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.</p>
+ *
+ *  <p>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.</p>
+ *
+ *  <p>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.</p>
+ * 
+ *  <p>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.</p>
+ *
+ *  @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<String, GlobalShare> globalShares = new ConcurrentHashMap<String, GlobalShare>();
+
+    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);
+    }
+
+    /**
+     *  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 <T> void shareGlobal( T object, Class<? super T> 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 <T> void shareGlobal( String name, T object, Class<? super T> 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 <T> void shareGlobal( byte channel, String name, T object, Class<? super T> 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);
+        }
+    } 
+
+    /**
+     *  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);
+    }
+
+    /**
+     *  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<String, GlobalShare> 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;
+        }
+    }
+}

+ 387 - 0
jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java

@@ -0,0 +1,387 @@
+/*
+ * 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<SharedObject> local = new ObjectIndex<SharedObject>();
+    private final ObjectIndex<Object> remote = new ObjectIndex<Object>();
+
+    // 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);
+    }
+    
+    /**
+     *  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 <T> void share( T object, Class<? super T> 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 <T> void share( byte channel, T object, Class<? super T> 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 <T> void share( String name, T object, Class<? super T> 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 <T> void share( byte channel, String name, T object, Class<? super T> 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 a local object that was previously registered with share() using
+     *  just type registration.
+     */
+    public <T> T getLocalObject( Class<T> type ) {
+        return getLocalObject(type.getName(), type);
+    }
+    
+    /**
+     *  Returns a local object that was previously registered with share() using
+     *  name registration.
+     */
+    public <T> T getLocalObject( String name, Class<T> type ) {
+        local.lock.readLock().lock();
+        try {
+            return type.cast(local.byName.get(name));
+        } finally {
+            local.lock.readLock().unlock();
+        }
+    }   
+    
+    /**
+     *  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> T getRemoteObject( Class<T> 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> T getRemoteObject( String name, Class<T> 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<T> {
+        final Map<String, T> byName = new HashMap<String, T>();
+        final Map<Short, T> byId = new HashMap<Short, T>(); 
+        final Map<Short, ClassInfo> classes = new HashMap<Short, ClassInfo>();  
+        final ReadWriteLock lock = new ReentrantReadWriteLock();
+        
+        public ObjectIndex() {
+        }
+    }    
+    
+}
+
+

+ 15 - 5
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,19 +50,27 @@ import com.jme3.network.service.ClientServiceManager;
  */
 public class ClientSerializerRegistrationsService extends AbstractClientService 
                                                   implements MessageListener<Client> {
+                                                  
+    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.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);
+            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); 
     }
 
+    @Override
     public void messageReceived( Client source, Message m ) {
         // We only wait for one kind of message...
         SerializerRegistrationsMessage msg = (SerializerRegistrationsMessage)m;