Parcourir la source

Merge branch 'master' into expermiental

Kirill Vainer il y a 9 ans
Parent
commit
15465a020f
64 fichiers modifiés avec 2668 ajouts et 587 suppressions
  1. 1 0
      .gitignore
  2. 4 0
      jme3-android/src/main/java/com/jme3/system/android/JmeAndroidSystem.java
  3. 4 0
      jme3-blender/src/main/java/com/jme3/asset/BlenderKey.java
  4. 9 78
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/AbstractBlenderHelper.java
  5. 122 15
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java
  6. 69 17
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderLoader.java
  7. 1 0
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/landscape/LandscapeHelper.java
  8. 8 2
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/TemporalMesh.java
  9. 2 2
      jme3-bullet/src/main/java/com/jme3/bullet/objects/PhysicsRigidBody.java
  10. 1 0
      jme3-core/src/main/java/com/jme3/font/BitmapTextPage.java
  11. 31 1
      jme3-core/src/main/java/com/jme3/scene/Node.java
  12. 11 2
      jme3-core/src/main/java/com/jme3/system/AppSettings.java
  13. 4 0
      jme3-core/src/main/java/com/jme3/system/JmeSystemDelegate.java
  14. 5 0
      jme3-core/src/main/java/com/jme3/system/Platform.java
  15. 2 0
      jme3-core/src/main/java/com/jme3/texture/Image.java
  16. 2 0
      jme3-core/src/main/resources/Common/MatDefs/Gui/Gui.frag
  17. 13 2
      jme3-core/src/main/resources/Common/MatDefs/Gui/Gui.j3md
  18. 2 0
      jme3-core/src/main/resources/Common/MatDefs/Gui/Gui.vert
  19. 44 0
      jme3-core/src/main/resources/joystick-mapping.properties
  20. 2 2
      jme3-core/src/plugins/java/com/jme3/texture/plugins/HDRLoader.java
  21. 1 1
      jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java
  22. 73 7
      jme3-examples/src/main/java/jme3test/network/TestChatClient.java
  23. 74 0
      jme3-examples/src/main/java/jme3test/network/TestChatClientAndServer.java
  24. 99 14
      jme3-examples/src/main/java/jme3test/network/TestChatServer.java
  25. 3 3
      jme3-jogl/build.gradle
  26. 61 28
      jme3-jogl/src/main/java/com/jme3/input/jogl/NewtMouseInput.java
  27. 9 0
      jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGL.java
  28. 8 3
      jme3-jogl/src/main/java/com/jme3/system/jogl/JoglAbstractDisplay.java
  29. 1 1
      jme3-jogl/src/main/java/com/jme3/system/jogl/JoglContext.java
  30. 7 4
      jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtAbstractDisplay.java
  31. 4 2
      jme3-jogl/src/main/java/com/jme3/system/jogl/JoglOffscreenBuffer.java
  32. 39 10
      jme3-networking/src/main/java/com/jme3/network/base/DefaultClient.java
  33. 58 13
      jme3-networking/src/main/java/com/jme3/network/base/DefaultServer.java
  34. 1 1
      jme3-networking/src/main/java/com/jme3/network/base/MessageProtocol.java
  35. 20 2
      jme3-networking/src/main/java/com/jme3/network/message/SerializerRegistrationsMessage.java
  36. 5 0
      jme3-networking/src/main/java/com/jme3/network/serializing/Serializer.java
  37. 1 1
      jme3-networking/src/main/java/com/jme3/network/service/AbstractService.java
  38. 29 1
      jme3-networking/src/main/java/com/jme3/network/service/ServiceManager.java
  39. 56 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/Asynchronous.java
  40. 60 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/CallType.java
  41. 112 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfo.java
  42. 104 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfoRegistry.java
  43. 137 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/MethodInfo.java
  44. 87 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RemoteObjectHandler.java
  45. 195 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiClientService.java
  46. 60 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiContext.java
  47. 262 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiHostedService.java
  48. 387 0
      jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java
  49. 15 5
      jme3-networking/src/main/java/com/jme3/network/service/serializer/ClientSerializerRegistrationsService.java
  50. 1 0
      sdk/jme3-core/src/com/jme3/gde/core/properties/TextureBrowser.form
  51. 19 24
      sdk/jme3-core/src/com/jme3/gde/core/properties/TextureBrowser.java
  52. 7 0
      sdk/jme3-core/src/com/jme3/gde/core/properties/TexturePropertyEditor.java
  53. 0 140
      sdk/jme3-core/src/com/jme3/gde/core/properties/preview/DDSPreview.java
  54. 179 0
      sdk/jme3-core/src/com/jme3/gde/core/properties/preview/TexturePreview.java
  55. 4 5
      sdk/jme3-core/src/com/jme3/gde/core/properties/preview/tex3DThumb.frag
  56. 2 2
      sdk/jme3-core/src/com/jme3/gde/core/properties/preview/tex3DThumb.vert
  57. 8 17
      sdk/jme3-materialeditor/src/com/jme3/gde/materials/multiview/widgets/TexturePanel.java
  58. 22 40
      sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/TerrainEditorTopComponent.java
  59. 1 2
      sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/TerrainToolController.java
  60. 5 3
      sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/sky/AddSkyboxAction.java
  61. 1 1
      sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/sky/Bundle.properties
  62. 37 17
      sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/sky/SkyboxVisualPanel2.form
  63. 75 118
      sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/sky/SkyboxVisualPanel2.java
  64. 2 1
      sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/sky/SkyboxWizardPanel2.java

+ 1 - 0
.gitignore

@@ -146,3 +146,4 @@
 /sdk/nbi/stub/ext/components/products/jdk/build/
 /sdk/nbi/stub/ext/components/products/jdk/dist/
 /sdk/jme3-dark-laf/nbproject/private/
+jme3-lwjgl3/build/

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

+ 4 - 0
jme3-blender/src/main/java/com/jme3/asset/BlenderKey.java

@@ -230,18 +230,22 @@ public class BlenderKey extends ModelKey {
     }
 
     /**
+     * Not used any more.
      * This method sets the asset root path.
      * @param assetRootPath
      *            the assets root path
      */
+    @Deprecated
     public void setAssetRootPath(String assetRootPath) {
         this.assetRootPath = assetRootPath;
     }
 
     /**
+     * Not used any more.
      * This method returns the asset root path.
      * @return the asset root path
      */
+    @Deprecated
     public String getAssetRootPath() {
         return assetRootPath;
     }

+ 9 - 78
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/AbstractBlenderHelper.java

@@ -31,8 +31,6 @@
  */
 package com.jme3.scene.plugins.blender;
 
-import java.io.File;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -40,25 +38,17 @@ import java.util.Map.Entry;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
-import com.jme3.animation.Animation;
 import com.jme3.asset.AssetNotFoundException;
 import com.jme3.asset.BlenderKey;
 import com.jme3.export.Savable;
-import com.jme3.light.Light;
 import com.jme3.math.FastMath;
 import com.jme3.math.Quaternion;
-import com.jme3.post.Filter;
-import com.jme3.renderer.Camera;
-import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
 import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
 import com.jme3.scene.plugins.blender.file.BlenderFileException;
 import com.jme3.scene.plugins.blender.file.Pointer;
 import com.jme3.scene.plugins.blender.file.Structure;
-import com.jme3.scene.plugins.blender.materials.MaterialContext;
-import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
 import com.jme3.scene.plugins.blender.objects.Properties;
-import com.jme3.texture.Texture;
 
 /**
  * A purpose of the helper class is to split calculation code into several classes. Each helper after use should be cleared because it can
@@ -157,7 +147,6 @@ public abstract class AbstractBlenderHelper {
      * @throws BlenderFileException
      *             and exception is throw when problems with reading a blend file occur
      */
-    @SuppressWarnings("unchecked")
     protected Object loadLibrary(Structure id) throws BlenderFileException {
         Pointer pLib = (Pointer) id.getFieldValue("lib");
         if (pLib.isNotNull()) {
@@ -167,79 +156,21 @@ public abstract class AbstractBlenderHelper {
             String path = library.getFieldValue("filepath").toString();
 
             if (!blenderContext.getLinkedFeatures().keySet().contains(path)) {
-                File file = new File(path);
-                List<String> pathsToCheck = new ArrayList<String>();
-                String currentPath = file.getName();
-                do {
-                    pathsToCheck.add(currentPath);
-                    file = file.getParentFile();
-                    if (file != null) {
-                        currentPath = file.getName() + '/' + currentPath;
-                    }
-                } while (file != null);
-
                 Spatial loadedAsset = null;
-                BlenderKey blenderKey = null;
-                for (String p : pathsToCheck) {
-                    blenderKey = new BlenderKey(p);
-                    blenderKey.setLoadUnlinkedAssets(true);
-                    try {
-                        loadedAsset = blenderContext.getAssetManager().loadAsset(blenderKey);
-                        break;// break if no exception was thrown
-                    } catch (AssetNotFoundException e) {
-                        LOGGER.log(Level.FINEST, "Cannot locate linked resource at path: {0}.", p);
-                    }
+                BlenderKey blenderKey = new BlenderKey(path);
+                blenderKey.setLoadUnlinkedAssets(true);
+                try {
+                    loadedAsset = blenderContext.getAssetManager().loadAsset(blenderKey);
+                } catch (AssetNotFoundException e) {
+                    LOGGER.log(Level.FINEST, "Cannot locate linked resource at path: {0}.", path);
                 }
-
+                
                 if (loadedAsset != null) {
                     Map<String, Map<String, Object>> linkedData = loadedAsset.getUserData("linkedData");
+                    
                     for (Entry<String, Map<String, Object>> entry : linkedData.entrySet()) {
                         String linkedDataFilePath = "this".equals(entry.getKey()) ? path : entry.getKey();
-
-                        List<Node> scenes = (List<Node>) entry.getValue().get("scenes");
-                        for (Node scene : scenes) {
-                            blenderContext.addLinkedFeature(linkedDataFilePath, "SC" + scene.getName(), scene);
-                        }
-                        List<Node> objects = (List<Node>) entry.getValue().get("objects");
-                        for (Node object : objects) {
-                            blenderContext.addLinkedFeature(linkedDataFilePath, "OB" + object.getName(), object);
-                        }
-                        List<TemporalMesh> meshes = (List<TemporalMesh>) entry.getValue().get("meshes");
-                        for (TemporalMesh mesh : meshes) {
-                            blenderContext.addLinkedFeature(linkedDataFilePath, "ME" + mesh.getName(), mesh);
-                        }
-                        List<MaterialContext> materials = (List<MaterialContext>) entry.getValue().get("materials");
-                        for (MaterialContext materialContext : materials) {
-                            blenderContext.addLinkedFeature(linkedDataFilePath, "MA" + materialContext.getName(), materialContext);
-                        }
-                        List<Texture> textures = (List<Texture>) entry.getValue().get("textures");
-                        for (Texture texture : textures) {
-                            blenderContext.addLinkedFeature(linkedDataFilePath, "TE" + texture.getName(), texture);
-                        }
-                        List<Texture> images = (List<Texture>) entry.getValue().get("images");
-                        for (Texture image : images) {
-                            blenderContext.addLinkedFeature(linkedDataFilePath, "IM" + image.getName(), image);
-                        }
-                        List<Animation> animations = (List<Animation>) entry.getValue().get("animations");
-                        for (Animation animation : animations) {
-                            blenderContext.addLinkedFeature(linkedDataFilePath, "AC" + animation.getName(), animation);
-                        }
-                        List<Camera> cameras = (List<Camera>) entry.getValue().get("cameras");
-                        for (Camera camera : cameras) {
-                            blenderContext.addLinkedFeature(linkedDataFilePath, "CA" + camera.getName(), camera);
-                        }
-                        List<Light> lights = (List<Light>) entry.getValue().get("lights");
-                        for (Light light : lights) {
-                            blenderContext.addLinkedFeature(linkedDataFilePath, "LA" + light.getName(), light);
-                        }
-                        Spatial sky = (Spatial) entry.getValue().get("sky");
-                        if (sky != null) {
-                            blenderContext.addLinkedFeature(linkedDataFilePath, sky.getName(), sky);
-                        }
-                        List<Filter> filters = (List<Filter>) entry.getValue().get("filters");
-                        for (Filter filter : filters) {
-                            blenderContext.addLinkedFeature(linkedDataFilePath, filter.getName(), filter);
-                        }
+                        blenderContext.getLinkedFeatures().put(linkedDataFilePath, entry.getValue());
                     }
                 } else {
                     LOGGER.log(Level.WARNING, "No features loaded from path: {0}.", path);

+ 122 - 15
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java

@@ -44,8 +44,11 @@ import com.jme3.animation.Bone;
 import com.jme3.animation.Skeleton;
 import com.jme3.asset.AssetManager;
 import com.jme3.asset.BlenderKey;
+import com.jme3.light.Light;
 import com.jme3.material.Material;
 import com.jme3.math.ColorRGBA;
+import com.jme3.post.Filter;
+import com.jme3.renderer.Camera;
 import com.jme3.scene.Node;
 import com.jme3.scene.plugins.blender.animations.BlenderAction;
 import com.jme3.scene.plugins.blender.animations.BoneContext;
@@ -55,6 +58,8 @@ import com.jme3.scene.plugins.blender.file.DnaBlockData;
 import com.jme3.scene.plugins.blender.file.FileBlockHeader;
 import com.jme3.scene.plugins.blender.file.FileBlockHeader.BlockCode;
 import com.jme3.scene.plugins.blender.file.Structure;
+import com.jme3.scene.plugins.blender.materials.MaterialContext;
+import com.jme3.texture.Texture;
 
 /**
  * The class that stores temporary data and manages it during loading the belnd
@@ -77,7 +82,7 @@ public class BlenderContext {
     /** The asset manager. */
     private AssetManager                           assetManager;
     /** The blocks read from the file. */
-    protected List<FileBlockHeader>                blocks;
+    protected List<FileBlockHeader>                blocks                 = new ArrayList<FileBlockHeader>();
     /**
      * A map containing the file block headers. The key is the old memory address.
      */
@@ -233,6 +238,7 @@ public class BlenderContext {
      *            the block header to store
      */
     public void addFileBlockHeader(Long oldMemoryAddress, FileBlockHeader fileBlockHeader) {
+        blocks.add(fileBlockHeader);
         fileBlockHeadersByOma.put(oldMemoryAddress, fileBlockHeader);
         List<FileBlockHeader> headers = fileBlockHeadersByCode.get(fileBlockHeader.getCode());
         if (headers == null) {
@@ -242,6 +248,13 @@ public class BlenderContext {
         headers.add(fileBlockHeader);
     }
 
+    /**
+     * @return the block headers
+     */
+    public List<FileBlockHeader> getBlocks() {
+        return blocks;
+    }
+
     /**
      * This method returns the block header of a given memory address. If the
      * header is not present then null is returned.
@@ -332,22 +345,14 @@ public class BlenderContext {
      * The method adds linked content to the blender context.
      * @param blenderFilePath
      *            the path of linked blender file
-     * @param featureName
-     *            the linked feature name
+     * @param featureGroup
+     *            the linked feature group (ie. scenes, materials, meshes, etc.)
      * @param feature
      *            the linked feature
      */
-    public void addLinkedFeature(String blenderFilePath, String featureName, Object feature) {
-        if (feature != null) {
-            Map<String, Object> linkedFeatures = this.linkedFeatures.get(blenderFilePath);
-            if (linkedFeatures == null) {
-                linkedFeatures = new HashMap<String, Object>();
-                this.linkedFeatures.put(blenderFilePath, linkedFeatures);
-            }
-            if (!linkedFeatures.containsKey(featureName)) {
-                linkedFeatures.put(featureName, feature);
-            }
-        }
+    @Deprecated
+    public void addLinkedFeature(String blenderFilePath, String featureGroup, Object feature) {
+        // the method is deprecated and empty at the moment
     }
 
     /**
@@ -358,9 +363,106 @@ public class BlenderContext {
      *            the feature name we want to get
      * @return linked feature or null if none was found
      */
+    @SuppressWarnings("unchecked")
     public Object getLinkedFeature(String blenderFilePath, String featureName) {
         Map<String, Object> linkedFeatures = this.linkedFeatures.get(blenderFilePath);
-        return linkedFeatures != null ? linkedFeatures.get(featureName) : null;
+        if(linkedFeatures != null) {
+            String namePrefix = (featureName.charAt(0) + "" + featureName.charAt(1)).toUpperCase();
+            featureName = featureName.substring(2);
+            
+            if("SC".equals(namePrefix)) {
+                List<Node> scenes = (List<Node>) linkedFeatures.get("scenes");
+                if(scenes != null) {
+                    for(Node scene : scenes) {
+                        if(featureName.equals(scene.getName())) {
+                            return scene;
+                        }
+                    }
+                }
+            } else if("OB".equals(namePrefix)) {
+                List<Node> features = (List<Node>) linkedFeatures.get("objects");
+                if(features != null) {
+                    for(Node feature : features) {
+                        if(featureName.equals(feature.getName())) {
+                            return feature;
+                        }
+                    }
+                }
+            } else if("ME".equals(namePrefix)) {
+                List<Node> features = (List<Node>) linkedFeatures.get("meshes");
+                if(features != null) {
+                    for(Node feature : features) {
+                        if(featureName.equals(feature.getName())) {
+                            return feature;
+                        }
+                    }
+                }
+            } else if("MA".equals(namePrefix)) {
+                List<MaterialContext> features = (List<MaterialContext>) linkedFeatures.get("materials");
+                if(features != null) {
+                    for(MaterialContext feature : features) {
+                        if(featureName.equals(feature.getName())) {
+                            return feature;
+                        }
+                    }
+                }
+            } else if("TX".equals(namePrefix)) {
+                List<Texture> features = (List<Texture>) linkedFeatures.get("textures");
+                if(features != null) {
+                    for(Texture feature : features) {
+                        if(featureName.equals(feature.getName())) {
+                            return feature;
+                        }
+                    }
+                }
+            } else if("IM".equals(namePrefix)) {
+                List<Texture> features = (List<Texture>) linkedFeatures.get("images");
+                if(features != null) {
+                    for(Texture feature : features) {
+                        if(featureName.equals(feature.getName())) {
+                            return feature;
+                        }
+                    }
+                }
+            } else if("AC".equals(namePrefix)) {
+                List<Animation> features = (List<Animation>) linkedFeatures.get("animations");
+                if(features != null) {
+                    for(Animation feature : features) {
+                        if(featureName.equals(feature.getName())) {
+                            return feature;
+                        }
+                    }
+                }
+            } else if("CA".equals(namePrefix)) {
+                List<Camera> features = (List<Camera>) linkedFeatures.get("cameras");
+                if(features != null) {
+                    for(Camera feature : features) {
+                        if(featureName.equals(feature.getName())) {
+                            return feature;
+                        }
+                    }
+                }
+            } else if("LA".equals(namePrefix)) {
+                List<Light> features = (List<Light>) linkedFeatures.get("lights");
+                if(features != null) {
+                    for(Light feature : features) {
+                        if(featureName.equals(feature.getName())) {
+                            return feature;
+                        }
+                    }
+                }
+            } else if("FI".equals(featureName)) {
+                List<Filter> features = (List<Filter>) linkedFeatures.get("lights");
+                if(features != null) {
+                    for(Filter feature : features) {
+                        if(featureName.equals(feature.getName())) {
+                            return feature;
+                        }
+                    }
+                }
+            }
+        }
+        return null;
     }
 
     /**
@@ -659,4 +761,9 @@ public class BlenderContext {
     public static enum LoadedDataType {
         STRUCTURE, FEATURE, TEMPORAL_MESH;
     }
+    
+    @Override
+    public String toString() {
+        return blenderKey == null ? "BlenderContext [key = null]" : "BlenderContext [ key = " + blenderKey.toString() + " ]";
+    }
 }

+ 69 - 17
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderLoader.java

@@ -31,6 +31,9 @@
  */
 package com.jme3.scene.plugins.blender;
 
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -41,9 +44,13 @@ import java.util.logging.Logger;
 
 import com.jme3.animation.Animation;
 import com.jme3.asset.AssetInfo;
+import com.jme3.asset.AssetKey;
 import com.jme3.asset.AssetLoader;
+import com.jme3.asset.AssetLocator;
+import com.jme3.asset.AssetManager;
 import com.jme3.asset.BlenderKey;
 import com.jme3.asset.ModelKey;
+import com.jme3.asset.StreamAssetInfo;
 import com.jme3.light.Light;
 import com.jme3.math.ColorRGBA;
 import com.jme3.post.Filter;
@@ -81,22 +88,17 @@ import com.jme3.texture.Texture;
 public class BlenderLoader implements AssetLoader {
     private static final Logger     LOGGER = Logger.getLogger(BlenderLoader.class.getName());
 
-    /** The blocks read from the file. */
-    protected List<FileBlockHeader> blocks;
-    /** The blender context. */
-    protected BlenderContext        blenderContext;
-
     @Override
     public Spatial load(AssetInfo assetInfo) throws IOException {
         try {
-            this.setup(assetInfo);
+            BlenderContext blenderContext = this.setup(assetInfo);
 
             AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class);
             animationHelper.loadAnimations();
 
             BlenderKey blenderKey = blenderContext.getBlenderKey();
             LoadedFeatures loadedFeatures = new LoadedFeatures();
-            for (FileBlockHeader block : blocks) {
+            for (FileBlockHeader block : blenderContext.getBlocks()) {
                 switch (block.getCode()) {
                     case BLOCK_OB00:
                         ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
@@ -181,7 +183,7 @@ public class BlenderLoader implements AssetLoader {
 
             LOGGER.fine("Loading scenes and attaching them to the root object.");
             for (FileBlockHeader sceneBlock : loadedFeatures.sceneBlocks) {
-                loadedFeatures.scenes.add(this.toScene(sceneBlock.getStructure(blenderContext)));
+                loadedFeatures.scenes.add(this.toScene(sceneBlock.getStructure(blenderContext), blenderContext));
             }
 
             LOGGER.fine("Creating the root node of the model and applying loaded nodes of the scene and loaded features to it.");
@@ -220,7 +222,7 @@ public class BlenderLoader implements AssetLoader {
         } catch (Exception e) {
             throw new IOException("Unexpected importer exception occured: " + e.getLocalizedMessage(), e);
         } finally {
-            this.clear();
+            this.clear(assetInfo);
         }
     }
 
@@ -228,11 +230,12 @@ public class BlenderLoader implements AssetLoader {
      * This method converts the given structure to a scene node.
      * @param structure
      *            structure of a scene
+     *            @param blenderContext the blender context
      * @return scene's node
      * @throws BlenderFileException
      *             an exception throw when problems with blender file occur
      */
-    private Node toScene(Structure structure) throws BlenderFileException {
+    private Node toScene(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
         ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
         Node result = new Node(structure.getName());
         List<Structure> base = ((Structure) structure.getFieldValue("base")).evaluateListBase();
@@ -265,7 +268,7 @@ public class BlenderLoader implements AssetLoader {
      * @throws BlenderFileException
      *             an exception is throw when something wrong happens with blender file
      */
-    protected void setup(AssetInfo assetInfo) throws BlenderFileException {
+    protected BlenderContext setup(AssetInfo assetInfo) throws BlenderFileException {
         // registering loaders
         ModelKey modelKey = (ModelKey) assetInfo.getKey();
         BlenderKey blenderKey;
@@ -273,16 +276,15 @@ public class BlenderLoader implements AssetLoader {
             blenderKey = (BlenderKey) modelKey;
         } else {
             blenderKey = new BlenderKey(modelKey.getName());
-            blenderKey.setAssetRootPath(modelKey.getFolder());
         }
 
         // opening stream
         BlenderInputStream inputStream = new BlenderInputStream(assetInfo.openStream());
 
         // reading blocks
-        blocks = new ArrayList<FileBlockHeader>();
+        List<FileBlockHeader> blocks = new ArrayList<FileBlockHeader>();
         FileBlockHeader fileBlock;
-        blenderContext = new BlenderContext();
+        BlenderContext blenderContext = new BlenderContext();
         blenderContext.setBlenderVersion(inputStream.getVersionNumber());
         blenderContext.setAssetManager(assetInfo.getManager());
         blenderContext.setInputStream(inputStream);
@@ -317,15 +319,19 @@ public class BlenderLoader implements AssetLoader {
         if (sceneFileBlock != null) {
             blenderContext.setSceneStructure(sceneFileBlock.getStructure(blenderContext));
         }
+        
+        // adding locator for linked content
+        assetInfo.getManager().registerLocator(assetInfo.getKey().getName(), LinkedContentLocator.class);
+        
+        return blenderContext;
     }
 
     /**
      * The internal data is only needed during loading so make it unreachable so that the GC can release
      * that memory (which can be quite large amount).
      */
-    protected void clear() {
-        blenderContext = null;
-        blocks = null;
+    protected void clear(AssetInfo assetInfo) {
+        assetInfo.getManager().unregisterLocator(assetInfo.getKey().getName(), LinkedContentLocator.class);
     }
 
     /**
@@ -362,4 +368,50 @@ public class BlenderLoader implements AssetLoader {
          */
         private ColorRGBA             backgroundColor = ColorRGBA.Gray;
     }
+    
+    public static class LinkedContentLocator implements AssetLocator {
+        private File rootFolder;
+        
+        @Override
+        public void setRootPath(String rootPath) {
+            rootFolder = new File(rootPath);
+            if(rootFolder.isFile()) {
+                rootFolder = rootFolder.getParentFile();
+            }
+        }
+
+        @SuppressWarnings("rawtypes")
+        @Override
+        public AssetInfo locate(AssetManager manager, AssetKey key) {
+            if(key instanceof BlenderKey) {
+                File linkedAbsoluteFile = new File(key.getName());
+                if(linkedAbsoluteFile.exists() && linkedAbsoluteFile.isFile()) {
+                    try {
+                        return new StreamAssetInfo(manager, key, new FileInputStream(linkedAbsoluteFile));
+                    } catch (FileNotFoundException e) {
+                        return null;
+                    }
+                }
+                
+                File linkedFileInCurrentAssetFolder = new File(rootFolder, linkedAbsoluteFile.getName());
+                if(linkedFileInCurrentAssetFolder.exists() && linkedFileInCurrentAssetFolder.isFile()) {
+                    try {
+                        return new StreamAssetInfo(manager, key, new FileInputStream(linkedFileInCurrentAssetFolder));
+                    } catch (FileNotFoundException e) {
+                        return null;
+                    }
+                }
+                
+                File linkedFileInCurrentFolder = new File(".", linkedAbsoluteFile.getName());
+                if(linkedFileInCurrentFolder.exists() && linkedFileInCurrentFolder.isFile()) {
+                    try {
+                        return new StreamAssetInfo(manager, key, new FileInputStream(linkedFileInCurrentFolder));
+                    } catch (FileNotFoundException e) {
+                        return null;
+                    }
+                }
+            }
+            return null;
+        }
+    }
 }

+ 1 - 0
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/landscape/LandscapeHelper.java

@@ -81,6 +81,7 @@ public class LandscapeHelper extends AbstractBlenderHelper {
         if ((mode & MODE_MIST) != 0) {
             LOGGER.fine("Loading fog.");
             result = new FogFilter();
+            result.setName("FIfog");
             result.setFogColor(this.toBackgroundColor(worldStructure));
         }
         return result;

+ 8 - 2
jme3-blender/src/main/java/com/jme3/scene/plugins/blender/meshes/TemporalMesh.java

@@ -46,7 +46,9 @@ import com.jme3.scene.plugins.blender.objects.Properties;
  */
 public class TemporalMesh extends Geometry {
     private static final Logger        LOGGER                    = Logger.getLogger(TemporalMesh.class.getName());
-
+    /** A minimum weight value. */
+    private static final double 	   MINIMUM_BONE_WEIGHT 		 = FastMath.DBL_EPSILON;
+    
     /** The blender context. */
     protected final BlenderContext     blenderContext;
 
@@ -530,7 +532,11 @@ public class TemporalMesh extends Geometry {
                         for (Entry<String, Integer> entry : boneIndexes.entrySet()) {
                             if (vertexGroupsForVertex.containsKey(entry.getKey())) {
                                 float weight = vertexGroupsForVertex.get(entry.getKey());
-                                if (weight > 0) {// no need to use such weights
+                                if (weight > MINIMUM_BONE_WEIGHT) {
+                                	// only values of weight greater than MINIMUM_BONE_WEIGHT are used
+                                	// if all non zero weights were used, and they were samm enough, problems with normalisation would occur
+                                	// because adding a very small value to 1.0 will give 1.0
+                                	// so in order to avoid such errors, which can cause severe animation artifacts we need to use some minimum weight value
                                     boneBuffersForVertex.put(weight, entry.getValue());
                                 }
                             }

+ 2 - 2
jme3-bullet/src/main/java/com/jme3/bullet/objects/PhysicsRigidBody.java

@@ -737,8 +737,8 @@ public class PhysicsRigidBody extends PhysicsCollisionObject {
         setKinematic(capsule.readBoolean("kinematic", false));
 
         setRestitution(capsule.readFloat("restitution", 0));
-        Vector3f angularFactor = (Vector3f) capsule.readSavable("angularFactor", Vector3f.NAN.clone());
-        if(angularFactor == Vector3f.NAN) {
+        Vector3f angularFactor = (Vector3f) capsule.readSavable("angularFactor", null);
+        if(angularFactor == null) {
             setAngularFactor(capsule.readFloat("angularFactor", 1));
         } else {
             setAngularFactor(angularFactor);

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

+ 11 - 2
jme3-core/src/main/java/com/jme3/system/AppSettings.java

@@ -112,13 +112,22 @@ public final class AppSettings extends HashMap<String, Object> {
     public static final String ANDROID_OPENAL_SOFT = "OpenAL_SOFT";
     
     /**
-     * Use JogAmp's JOGL as the display system
+     * Use JogAmp's JOGL as the display system, with the OpenGL forward compatible profile
      * <p>
      * N.B: This backend is EXPERIMENTAL
      *
      * @see AppSettings#setRenderer(java.lang.String)
      */
-    public static final String JOGL = "JOGL";
+    public static final String JOGL_OPENGL_FORWARD_COMPATIBLE = "JOGL_OPENGL_FORWARD_COMPATIBLE";
+    
+    /**
+     * Use JogAmp's JOGL as the display system with the backward compatible profile
+     * <p>
+     * N.B: This backend is EXPERIMENTAL
+     *
+     * @see AppSettings#setRenderer(java.lang.String)
+     */
+    public static final String JOGL_OPENGL_BACKWARD_COMPATIBLE = "JOGL_OPENGL_BACKWARD_COMPATIBLE";
     
     /**
      * Use JogAmp's JOAL as the display system

+ 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 - 0
jme3-core/src/main/java/com/jme3/texture/Image.java

@@ -1041,6 +1041,7 @@ public class Image extends NativeObject implements Savable /*, Cloneable*/ {
         capsule.write(mipMapSizes, "mipMapSizes", null);
         capsule.write(multiSamples, "multiSamples", 1);
         capsule.writeByteBufferArrayList(data, "data", null);
+        capsule.write(colorSpace, "colorSpace", null);
     }
 
     public void read(JmeImporter e) throws IOException {
@@ -1052,6 +1053,7 @@ public class Image extends NativeObject implements Savable /*, Cloneable*/ {
         mipMapSizes = capsule.readIntArray("mipMapSizes", null);
         multiSamples = capsule.readInt("multiSamples", 1);
         data = (ArrayList<ByteBuffer>) capsule.readByteBufferArrayList("data", null);
+        colorSpace = capsule.readEnum("colorSpace", ColorSpace.class, null);
 
         if (mipMapSizes != null) {
             needGeneratedMips = false;

+ 2 - 0
jme3-core/src/main/resources/Common/MatDefs/Gui/Gui.frag

@@ -1,3 +1,5 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
 #ifdef TEXTURE
     uniform sampler2D m_Texture;
     varying vec2 texCoord;

+ 13 - 2
jme3-core/src/main/resources/Common/MatDefs/Gui/Gui.j3md

@@ -7,8 +7,8 @@ MaterialDef Default GUI {
     }
 
     Technique {
-        VertexShader GLSL100:   Common/MatDefs/Gui/Gui.vert
-        FragmentShader GLSL100: Common/MatDefs/Gui/Gui.frag
+        VertexShader GLSL150:   Common/MatDefs/Gui/Gui.vert
+        FragmentShader GLSL150: Common/MatDefs/Gui/Gui.frag
 
         WorldParameters {
             WorldViewProjectionMatrix
@@ -21,6 +21,17 @@ MaterialDef Default GUI {
     }
 
     Technique {
+        VertexShader GLSL100:   Common/MatDefs/Gui/Gui.vert
+        FragmentShader GLSL100: Common/MatDefs/Gui/Gui.frag
+
+        WorldParameters {
+            WorldViewProjectionMatrix
+        }
+
+        Defines {
+            TEXTURE : Texture
+            VERTEX_COLOR : VertexColor
+        }
     }
 
 }

+ 2 - 0
jme3-core/src/main/resources/Common/MatDefs/Gui/Gui.vert

@@ -1,3 +1,5 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
 uniform mat4 g_WorldViewProjectionMatrix;
 uniform vec4 m_Color;
 

+ 44 - 0
jme3-core/src/main/resources/joystick-mapping.properties

@@ -69,3 +69,47 @@ Microsoft\ PC-joystick\ driver.12=POV +Y
 Microsoft\ PC-joystick\ driver.13=POV +X
 Microsoft\ PC-joystick\ driver.14=POV -Y
 Microsoft\ PC-joystick\ driver.15=POV -X
+
+# Logitech F310 gamepad with dip switch DirectInput
+Logitech\ Dual\ Action.1=2
+Logitech\ Dual\ Action.2=1
+Logitech\ Dual\ Action.3=0
+Logitech\ Dual\ Action.0=3
+
+# Logitech F310 gamepad with dip switch XInput
+Gamepad\ F310\ (Controller).0=2
+Gamepad\ F310\ (Controller).1=1
+Gamepad\ F310\ (Controller).2=3
+Gamepad\ F310\ (Controller).3=0
+
+Gamepad\ F310\ (Controller).6=8
+Gamepad\ F310\ (Controller).7=9
+
+Gamepad\ F310\ (Controller).8=10
+Gamepad\ F310\ (Controller).9=11
+
+Gamepad\ F310\ (Controller).rx=z
+Gamepad\ F310\ (Controller).ry=rz
+
+# requires custom code to support trigger buttons but this
+# keeps it from confusing the .rx mapping.
+Gamepad\ F310\ (Controller).z=trigger
+
+# Alternate version of the XBOX 360 controller
+XBOX\ 360\ For\ Windows\ (Controller).0=2
+XBOX\ 360\ For\ Windows\ (Controller).1=1
+XBOX\ 360\ For\ Windows\ (Controller).2=3
+XBOX\ 360\ For\ Windows\ (Controller).3=0
+
+XBOX\ 360\ For\ Windows\ (Controller).6=8
+XBOX\ 360\ For\ Windows\ (Controller).7=9
+
+XBOX\ 360\ For\ Windows\ (Controller).8=10
+XBOX\ 360\ For\ Windows\ (Controller).9=11
+
+XBOX\ 360\ For\ Windows\ (Controller).rx=z
+XBOX\ 360\ For\ Windows\ (Controller).ry=rz
+
+# requires custom code to support trigger buttons but this
+# keeps it from confusing the .rx mapping.
+XBOX\ 360\ For\ Windows\ (Controller).z=trigger

+ 2 - 2
jme3-core/src/plugins/java/com/jme3/texture/plugins/HDRLoader.java

@@ -308,8 +308,8 @@ public class HDRLoader implements AssetLoader {
         }
         in.close();
 
-        dataStore.rewind();
-        //TODO, HDR color space? considered linear here
+        dataStore.rewind();        
+        //HDR files color data is actually stored in linear space.
         return new Image(pixelFormat, width, height, dataStore, ColorSpace.Linear);
     }
 

+ 1 - 1
jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java

@@ -149,7 +149,7 @@ public final class NativeLibraryLoader {
         registerNativeLibrary("glfw-lwjgl3", Platform.Windows32, "native/windows/glfw32.dll");
         registerNativeLibrary("glfw-lwjgl3", Platform.Windows64, "native/windows/glfw.dll");
         registerNativeLibrary("glfw-lwjgl3", Platform.Linux32, "native/linux/libglfw32.so");
-        registerNativeLibrary("glfw-lwjgl3", Platform.Linux64, "native/linux/libglfw.dll");
+        registerNativeLibrary("glfw-lwjgl3", Platform.Linux64, "native/linux/libglfw.so");
         registerNativeLibrary("glfw-lwjgl3", Platform.MacOSX32, "native/macosx/libglfw.dylib");
         registerNativeLibrary("glfw-lwjgl3", Platform.MacOSX64, "native/macosx/libglfw.dylib");
 

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

+ 3 - 3
jme3-jogl/build.gradle

@@ -5,7 +5,7 @@ if (!hasProperty('mainClass')) {
 dependencies {
     compile project(':jme3-core')
     compile project(':jme3-desktop')
-    compile 'org.jogamp.gluegen:gluegen-rt-main:2.3.1'
-    compile 'org.jogamp.jogl:jogl-all-main:2.3.1'
-    compile 'org.jogamp.joal:joal-main:2.3.1'
+    compile 'org.jogamp.gluegen:gluegen-rt-main:2.3.2'
+    compile 'org.jogamp.jogl:jogl-all-main:2.3.2'
+    compile 'org.jogamp.joal:joal-main:2.3.2'
 }

+ 61 - 28
jme3-jogl/src/main/java/com/jme3/input/jogl/NewtMouseInput.java

@@ -50,6 +50,8 @@ import com.jogamp.nativewindow.util.DimensionImmutable;
 import com.jogamp.nativewindow.util.PixelFormat;
 import com.jogamp.nativewindow.util.PixelRectangle;
 import com.jogamp.nativewindow.util.Point;
+import com.jogamp.newt.event.WindowAdapter;
+import com.jogamp.newt.event.WindowEvent;
 
 public class NewtMouseInput  implements MouseInput, MouseListener {
     
@@ -106,40 +108,67 @@ public class NewtMouseInput  implements MouseInput, MouseListener {
 
         component = comp;
         component.addMouseListener(this);
+        component.addWindowListener(new WindowAdapter(){
+
+            @Override
+            public void windowGainedFocus(WindowEvent e) {
+                setCursorVisible(visible);
+            }
+
+            @Override
+            public void windowLostFocus(WindowEvent e) {
+                //without those lines,
+                //on Linux (OpenBox) the mouse is not restored if invisible (eg via Alt-Tab)
+                component.setPointerVisible(true);
+                component.confinePointer(false);
+            }
+            
+        });
     }
 
+    @Override
     public void initialize() {
     }
 
+    @Override
     public void destroy() {
     }
 
+    @Override
     public boolean isInitialized() {
         return true;
     }
 
+    @Override
     public void setInputListener(RawInputListener listener) {
         this.listener = listener;
     }
 
+    @Override
     public long getInputTimeNanos() {
         return System.nanoTime();
     }
 
+    @Override
     public void setCursorVisible(boolean visible) {
-        if (this.visible != visible) {
-            lastKnownLocation.setX(0);
-            lastKnownLocation.setY(0);
-
-            this.visible = visible;
-            component.setPointerVisible(visible);
-            if (!visible) {
-                recenterMouse(component);
-            }
-        }
+        lastKnownLocation.setX(0);
+        lastKnownLocation.setY(0);
+
+        this.visible = visible;
+        component.setPointerVisible(visible);
+        component.confinePointer(!visible);
+        hack_confinePointer();
     }
 
+    private void hack_confinePointer() {
+      if (component.hasFocus() && component.isPointerConfined() && !component.isPointerVisible()) {
+        recenterMouse(component);
+      }
+    }
+    
+    @Override
     public void update() {
+      if (!component.hasFocus()) return;
         if (cursorMoved) {
             int newX = location.getX();
             int newY = location.getY();
@@ -147,7 +176,7 @@ public class NewtMouseInput  implements MouseInput, MouseListener {
 
             // invert DY
             int actualX = lastKnownLocation.getX();
-            int actualY = component.getHeight() - lastKnownLocation.getY();
+            int actualY = component.getSurfaceHeight() - lastKnownLocation.getY();
             MouseMotionEvent evt = new MouseMotionEvent(actualX, actualY,
                                                         newX - lastEventX,
                                                         lastEventY - newY,
@@ -173,43 +202,46 @@ public class NewtMouseInput  implements MouseInput, MouseListener {
         }
     }
 
+    @Override
     public int getButtonCount() {
         return 3;
     }
 
+    @Override
     public void mouseClicked(MouseEvent awtEvt) {
 //        MouseButtonEvent evt = new MouseButtonEvent(getJMEButtonIndex(arg0), false);
 //        listener.onMouseButtonEvent(evt);
     }
 
+    @Override
     public void mousePressed(MouseEvent newtEvt) {
-        MouseButtonEvent evt = new MouseButtonEvent(getJMEButtonIndex(newtEvt), true, newtEvt.getX(), newtEvt.getY());
+        MouseButtonEvent evt = new MouseButtonEvent(getJMEButtonIndex(newtEvt), true, newtEvt.getX(), component.getSurfaceHeight() - newtEvt.getY());
         evt.setTime(newtEvt.getWhen());
         synchronized (eventQueue) {
             eventQueue.add(evt);
         }
     }
 
-    public void mouseReleased(MouseEvent awtEvt) {
-        MouseButtonEvent evt = new MouseButtonEvent(getJMEButtonIndex(awtEvt), false, awtEvt.getX(), awtEvt.getY());
+    @Override
+     public void mouseReleased(MouseEvent awtEvt) {
+        MouseButtonEvent evt = new MouseButtonEvent(getJMEButtonIndex(awtEvt), false, awtEvt.getX(), component.getSurfaceHeight() - awtEvt.getY());
         evt.setTime(awtEvt.getWhen());
         synchronized (eventQueue) {
             eventQueue.add(evt);
         }
     }
 
+    @Override
     public void mouseEntered(MouseEvent awtEvt) {
-        if (!visible) {
-            recenterMouse(component);
-        }
+        hack_confinePointer();
     }
 
+    @Override
     public void mouseExited(MouseEvent awtEvt) {
-        if (!visible) {
-            recenterMouse(component);
-        }
+        hack_confinePointer();
     }
 
+    @Override
     public void mouseWheelMoved(MouseEvent awtEvt) {
         //FIXME not sure this is the right way to handle this case
         // [0] should be used when the shift key is down
@@ -218,10 +250,12 @@ public class NewtMouseInput  implements MouseInput, MouseListener {
         cursorMoved = true;
     }
 
+    @Override
     public void mouseDragged(MouseEvent awtEvt) {
         mouseMoved(awtEvt);
     }
 
+    @Override
     public void mouseMoved(MouseEvent awtEvt) {
         if (isRecentering) {
             // MHenze (cylab) Fix Issue 35:
@@ -239,22 +273,20 @@ public class NewtMouseInput  implements MouseInput, MouseListener {
             int dy = awtEvt.getY() - lastKnownLocation.getY();
             location.setX(location.getX() + dx);
             location.setY(location.getY() + dy);
-            if (!visible) {
-                recenterMouse(component);
-            }
+            hack_confinePointer();
             lastKnownLocation.setX(awtEvt.getX());
             lastKnownLocation.setY(awtEvt.getY());
 
             cursorMoved = true;
         }
     }
-
+    
     // MHenze (cylab) Fix Issue 35: A method to generate recenter the mouse to allow the InputSystem to "grab" the mouse
     private void recenterMouse(final GLWindow component) {
         eventsSinceRecenter = 0;
         isRecentering = true;
-        centerLocation.setX(component.getWidth() / 2);
-        centerLocation.setY(component.getHeight() / 2);
+        centerLocation.setX(component.getSurfaceWidth() / 2);
+        centerLocation.setY(component.getSurfaceHeight() / 2);
         centerLocationOnScreen.setX(centerLocation.getX());
         centerLocationOnScreen.setY(centerLocation.getY());
         
@@ -287,12 +319,13 @@ public class NewtMouseInput  implements MouseInput, MouseListener {
         return index;
     }
 
+    @Override
     public void setNativeCursor(JmeCursor cursor) {
         final ByteBuffer pixels = Buffers.copyIntBufferAsByteBuffer(cursor.getImagesData());
         final DimensionImmutable size = new Dimension(cursor.getWidth(), cursor.getHeight());
         final PixelFormat pixFormat = PixelFormat.RGBA8888;
         final PixelRectangle.GenericPixelRect rec = new PixelRectangle.GenericPixelRect(pixFormat, size, 0, true, pixels);
-        final PointerIcon joglCursor = component.getScreen().getDisplay().createPointerIcon(rec, cursor.getXHotSpot(), cursor.getYHotSpot());
+        final PointerIcon joglCursor = component.getScreen().getDisplay().createPointerIcon(rec, cursor.getXHotSpot(), cursor.getHeight() - cursor.getYHotSpot());
         component.setPointerIcon(joglCursor);
     }
-}
+}

+ 9 - 0
jme3-jogl/src/main/java/com/jme3/renderer/jogl/JoglGL.java

@@ -532,6 +532,15 @@ public class JoglGL implements GL, GL2, GL3, GL4 {
     @Override
 	public void glShaderSource(int param1, String[] param2, IntBuffer param3) {
         checkLimit(param3);
+        
+        int param3pos = param3.position();
+        try {
+        	for (final String param2string : param2) {
+        		param3.put(Math.max(param2string.length(), param2string.getBytes().length));
+        	}
+        } finally {
+        	param3.position(param3pos);
+        }
         GLContext.getCurrentGL().getGL2ES2().glShaderSource(param1, param2.length, param2, param3);
     }
 

+ 8 - 3
jme3-jogl/src/main/java/com/jme3/system/jogl/JoglAbstractDisplay.java

@@ -37,6 +37,7 @@ import com.jme3.input.MouseInput;
 import com.jme3.input.TouchInput;
 import com.jme3.input.awt.AwtKeyInput;
 import com.jme3.input.awt.AwtMouseInput;
+import com.jme3.system.AppSettings;
 import com.jogamp.opengl.util.Animator;
 import com.jogamp.opengl.util.AnimatorBase;
 import com.jogamp.opengl.util.FPSAnimator;
@@ -80,9 +81,13 @@ public abstract class JoglAbstractDisplay extends JoglContext implements GLEvent
         
         device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
         
-        //FIXME use the settings to know whether to use the max programmable profile
-        //then call GLProfile.getMaxProgrammable(true);
-        GLCapabilities caps = new GLCapabilities(GLProfile.getMaxFixedFunc(true));
+        GLCapabilities caps;
+        if (settings.getRenderer().equals(AppSettings.JOGL_OPENGL_FORWARD_COMPATIBLE)) {
+        	caps = new GLCapabilities(GLProfile.getMaxProgrammable(true));
+        } else {
+        	caps = new GLCapabilities(GLProfile.getMaxFixedFunc(true));
+        }
+        
         caps.setHardwareAccelerated(true);
         caps.setDoubleBuffered(true);
         caps.setStencilBits(settings.getStencilBits());

+ 1 - 1
jme3-jogl/src/main/java/com/jme3/system/jogl/JoglContext.java

@@ -167,7 +167,7 @@ public abstract class JoglContext implements JmeContext {
                                         "required for jMonkeyEngine");
         }
         
-        if (settings.getRenderer().equals("JOGL")) {
+        if (settings.getRenderer().startsWith("JOGL")) {
         	com.jme3.renderer.opengl.GL gl = new JoglGL();
         	GLExt glext = new JoglGLExt();
         	GLFbo glfbo = new JoglGLFbo();

+ 7 - 4
jme3-jogl/src/main/java/com/jme3/system/jogl/JoglNewtAbstractDisplay.java

@@ -37,6 +37,7 @@ import com.jme3.input.MouseInput;
 import com.jme3.input.TouchInput;
 import com.jme3.input.jogl.NewtKeyInput;
 import com.jme3.input.jogl.NewtMouseInput;
+import com.jme3.system.AppSettings;
 import com.jogamp.newt.opengl.GLWindow;
 import com.jogamp.opengl.util.Animator;
 import com.jogamp.opengl.util.AnimatorBase;
@@ -73,10 +74,12 @@ public abstract class JoglNewtAbstractDisplay extends JoglContext implements GLE
 
     protected void initGLCanvas() {
         loadNatives();
-        //FIXME use the settings to know whether to use the max programmable profile
-        //then call GLProfile.getMaxProgrammable(true);
-        //FIXME use the default profile only on embedded devices
-        GLCapabilities caps = new GLCapabilities(GLProfile.getDefault());
+        GLCapabilities caps;
+        if (settings.getRenderer().equals(AppSettings.JOGL_OPENGL_FORWARD_COMPATIBLE)) {
+        	caps = new GLCapabilities(GLProfile.getMaxProgrammable(true));
+        } else {
+        	caps = new GLCapabilities(GLProfile.getMaxFixedFunc(true));
+        }
         caps.setHardwareAccelerated(true);
         caps.setDoubleBuffered(true);
         caps.setStencilBits(settings.getStencilBits());

+ 4 - 2
jme3-jogl/src/main/java/com/jme3/system/jogl/JoglOffscreenBuffer.java

@@ -37,16 +37,18 @@ import com.jme3.input.MouseInput;
 import com.jme3.input.TouchInput;
 import com.jme3.input.dummy.DummyKeyInput;
 import com.jme3.input.dummy.DummyMouseInput;
-import com.jogamp.newt.NewtVersion;
+
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+
 import com.jogamp.opengl.GL;
 import com.jogamp.opengl.GLCapabilities;
 import com.jogamp.opengl.GLContext;
 import com.jogamp.opengl.GLDrawableFactory;
 import com.jogamp.opengl.GLOffscreenAutoDrawable;
 import com.jogamp.opengl.GLProfile;
+import com.jogamp.opengl.JoglVersion;
 
 
 public class JoglOffscreenBuffer extends JoglContext implements Runnable {
@@ -123,7 +125,7 @@ public class JoglOffscreenBuffer extends JoglContext implements Runnable {
     @Override
 	public void run(){
         loadNatives();
-        logger.log(Level.FINE, "Using JOGL {0}", NewtVersion.getInstance().getImplementationVersion());
+        logger.log(Level.FINE, "Using JOGL {0}", JoglVersion.getInstance().getImplementationVersion());
         initInThread();
         while (!needClose.get()){
             runLoop();

+ 39 - 10
jme3-networking/src/main/java/com/jme3/network/base/DefaultClient.java

@@ -67,18 +67,18 @@ public class DefaultClient implements Client
     private static final int CH_UNRELIABLE = 1;
     private static final int CH_FIRST = 2;
         
-    private ThreadLocal<ByteBuffer> dataBuffer = new ThreadLocal<ByteBuffer>();
+    private final ThreadLocal<ByteBuffer> dataBuffer = new ThreadLocal<ByteBuffer>();
     
     private int id = -1;
     private boolean isRunning = false;
-    private CountDownLatch connecting = new CountDownLatch(1);
+    private final CountDownLatch connecting = new CountDownLatch(1);
     private String gameName;
     private int version;
-    private MessageListenerRegistry<Client> messageListeners = new MessageListenerRegistry<Client>();
-    private List<ClientStateListener> stateListeners = new CopyOnWriteArrayList<ClientStateListener>();
-    private List<ErrorListener<? super Client>> errorListeners = new CopyOnWriteArrayList<ErrorListener<? super Client>>();
-    private Redispatch dispatcher = new Redispatch();
-    private List<ConnectorAdapter> channels = new ArrayList<ConnectorAdapter>();    
+    private final MessageListenerRegistry<Client> messageListeners = new MessageListenerRegistry<Client>();
+    private final List<ClientStateListener> stateListeners = new CopyOnWriteArrayList<ClientStateListener>();
+    private final List<ErrorListener<? super Client>> errorListeners = new CopyOnWriteArrayList<ErrorListener<? super Client>>();
+    private final Redispatch dispatcher = new Redispatch();
+    private final List<ConnectorAdapter> channels = new ArrayList<ConnectorAdapter>();    
  
     private ConnectorFactory connectorFactory;
     
@@ -100,6 +100,7 @@ public class DefaultClient implements Client
     }
 
     protected void addStandardServices() {
+        log.fine("Adding standard services...");
         services.addService(new ClientSerializerRegistrationsService());
     }
 
@@ -128,6 +129,7 @@ public class DefaultClient implements Client
             throw new IllegalStateException( "Client is not started." );
     }
  
+    @Override
     public void start()
     {
         if( isRunning )
@@ -179,6 +181,7 @@ public class DefaultClient implements Client
         }
     }    
 
+    @Override
     public boolean isStarted() {
         return isRunning;
     }
@@ -195,33 +198,42 @@ public class DefaultClient implements Client
         }
     }
 
+    @Override
     public boolean isConnected()
     {
         return id != -1 && isRunning; 
     }     
 
+    @Override
     public int getId()
     {   
         return id;
     }     
  
+    @Override
     public String getGameName()
     {
         return gameName;
     }
 
+    @Override
     public int getVersion()
     {
         return version;
     }
     
+    @Override
     public ClientServiceManager getServices() 
     {
         return services;
     }
    
+    @Override
     public void send( Message message )
     {
+        if( log.isLoggable(Level.FINER) ) {
+            log.log(Level.FINER, "send({0})", message);
+        }
         if( message.isReliable() || channels.get(CH_UNRELIABLE) == null ) {
             send(CH_RELIABLE, message, true);
         } else {
@@ -229,8 +241,12 @@ public class DefaultClient implements Client
         }
     }
  
+    @Override
     public void send( int channel, Message message )
     {
+        if( log.isLoggable(Level.FINER) ) {
+            log.log(Level.FINER, "send({0}, {1})", new Object[]{channel, message});
+        }
         if( channel >= 0 ) {
             // Make sure we aren't still connecting.  Channels
             // won't be valid until we are fully connected since
@@ -275,6 +291,7 @@ public class DefaultClient implements Client
         channels.get(channel).write(buffer);
     }
  
+    @Override
     public void close()
     {
         checkRunning();
@@ -287,9 +304,11 @@ public class DefaultClient implements Client
         if( !isRunning )
             return;
 
-        // Let the services get a chance to stop before we
-        // kill the connection.
-        services.stop();
+        if( services.isStarted() ) {
+            // Let the services get a chance to stop before we
+            // kill the connection.
+            services.stop();
+        }
         
         // Send a close message
     
@@ -313,41 +332,49 @@ public class DefaultClient implements Client
         services.terminate();            
     }         
 
+    @Override
     public void addClientStateListener( ClientStateListener listener )
     {
         stateListeners.add( listener );
     } 
 
+    @Override
     public void removeClientStateListener( ClientStateListener listener )
     {
         stateListeners.remove( listener );
     } 
 
+    @Override
     public void addMessageListener( MessageListener<? super Client> listener )
     {
         messageListeners.addMessageListener( listener );
     } 
 
+    @Override
     public void addMessageListener( MessageListener<? super Client> listener, Class... classes )
     {
         messageListeners.addMessageListener( listener, classes );
     } 
 
+    @Override
     public void removeMessageListener( MessageListener<? super Client> listener )
     {
         messageListeners.removeMessageListener( listener );
     } 
 
+    @Override
     public void removeMessageListener( MessageListener<? super Client> listener, Class... classes )
     {
         messageListeners.removeMessageListener( listener, classes );
     } 
 
+    @Override
     public void addErrorListener( ErrorListener<? super Client> listener )
     {
         errorListeners.add( listener );
     } 
 
+    @Override
     public void removeErrorListener( ErrorListener<? super Client> listener )
     {
         errorListeners.remove( listener );
@@ -461,11 +488,13 @@ public class DefaultClient implements Client
  
     protected class Redispatch implements MessageListener<Object>, ErrorListener<Object>
     {
+        @Override
         public void messageReceived( Object source, Message m )
         {
             dispatch( m );
         }
         
+        @Override
         public void handleError( Object source, Throwable t )
         {
             // Only doing the DefaultClient.this to make the code

+ 58 - 13
jme3-networking/src/main/java/com/jme3/network/base/DefaultServer.java

@@ -66,26 +66,26 @@ public class DefaultServer implements Server
     private static final int CH_FIRST = 2;
     
     private boolean isRunning = false;
-    private AtomicInteger nextId = new AtomicInteger(0);
+    private final AtomicInteger nextId = new AtomicInteger(0);
     private String gameName;
     private int version;
-    private KernelFactory kernelFactory = KernelFactory.DEFAULT;
+    private final KernelFactory kernelFactory = KernelFactory.DEFAULT;
     private KernelAdapter reliableAdapter;
     private KernelAdapter fastAdapter;
-    private List<KernelAdapter> channels = new ArrayList<KernelAdapter>();
-    private List<Integer> alternatePorts = new ArrayList<Integer>();
-    private Redispatch dispatcher = new Redispatch();
-    private Map<Integer,HostedConnection> connections = new ConcurrentHashMap<Integer,HostedConnection>();
-    private Map<Endpoint,HostedConnection> endpointConnections 
+    private final List<KernelAdapter> channels = new ArrayList<KernelAdapter>();
+    private final List<Integer> alternatePorts = new ArrayList<Integer>();
+    private final Redispatch dispatcher = new Redispatch();
+    private final Map<Integer,HostedConnection> connections = new ConcurrentHashMap<Integer,HostedConnection>();
+    private final Map<Endpoint,HostedConnection> endpointConnections 
                             = new ConcurrentHashMap<Endpoint,HostedConnection>();
     
     // Keeps track of clients for whom we've only received the UDP
     // registration message
-    private Map<Long,Connection> connecting = new ConcurrentHashMap<Long,Connection>();
+    private final Map<Long,Connection> connecting = new ConcurrentHashMap<Long,Connection>();
     
-    private MessageListenerRegistry<HostedConnection> messageListeners 
+    private final MessageListenerRegistry<HostedConnection> messageListeners 
                             = new MessageListenerRegistry<HostedConnection>();                        
-    private List<ConnectionListener> connectionListeners = new CopyOnWriteArrayList<ConnectionListener>();
+    private final List<ConnectionListener> connectionListeners = new CopyOnWriteArrayList<ConnectionListener>();
     
     private HostedServiceManager services;
     
@@ -108,24 +108,29 @@ public class DefaultServer implements Server
     }   
 
     protected void addStandardServices() {
+        log.fine("Adding standard services...");
         services.addService(new ServerSerializerRegistrationsService());
     }
 
+    @Override
     public String getGameName()
     {
         return gameName;
     }
 
+    @Override
     public int getVersion()
     {
         return version;
     }
     
+    @Override
     public HostedServiceManager getServices() 
     {
         return services;
     }
 
+    @Override
     public int addChannel( int port )
     {
         if( isRunning )
@@ -164,6 +169,7 @@ public class DefaultServer implements Server
         }              
     }
 
+    @Override
     public void start()
     {
         if( isRunning )
@@ -185,11 +191,13 @@ public class DefaultServer implements Server
         services.start();             
     }
 
+    @Override
     public boolean isRunning()
     {
         return isRunning;
     }     
  
+    @Override
     public void close() 
     {
         if( !isRunning )
@@ -214,13 +222,19 @@ public class DefaultServer implements Server
         }                               
     }
  
+    @Override
     public void broadcast( Message message )
     {
         broadcast( null, message );
     }
 
+    @Override
     public void broadcast( Filter<? super HostedConnection> filter, Message message )
     {
+        if( log.isLoggable(Level.FINER) ) {
+            log.log(Level.FINER, "broadcast({0}, {1})", new Object[]{filter, message});
+        }
+        
         if( connections.isEmpty() )
             return;
  
@@ -237,8 +251,13 @@ public class DefaultServer implements Server
         }               
     }
 
+    @Override
     public void broadcast( int channel, Filter<? super HostedConnection> filter, Message message )
     {
+        if( log.isLoggable(Level.FINER) ) {
+            log.log(Level.FINER, "broadcast({0}, {1}. {2})", new Object[]{channel, filter, message});
+        }
+        
         if( connections.isEmpty() )
             return;
 
@@ -251,46 +270,55 @@ public class DefaultServer implements Server
         channels.get(channel+CH_FIRST).broadcast( adapter, buffer, true, false );               
     }
 
+    @Override
     public HostedConnection getConnection( int id )
     {
         return connections.get(id);
     }     
  
+    @Override
     public boolean hasConnections()
     {
         return !connections.isEmpty();
     }
  
+    @Override
     public Collection<HostedConnection> getConnections()
     {
         return Collections.unmodifiableCollection((Collection<HostedConnection>)connections.values());
     } 
  
+    @Override
     public void addConnectionListener( ConnectionListener listener )
     {
         connectionListeners.add(listener);   
     }
  
+    @Override
     public void removeConnectionListener( ConnectionListener listener )
     {
         connectionListeners.remove(listener);   
     }     
     
+    @Override
     public void addMessageListener( MessageListener<? super HostedConnection> listener )
     {
         messageListeners.addMessageListener( listener );
     } 
 
+    @Override
     public void addMessageListener( MessageListener<? super HostedConnection> listener, Class... classes )
     {
         messageListeners.addMessageListener( listener, classes );
     } 
 
+    @Override
     public void removeMessageListener( MessageListener<? super HostedConnection> listener )
     {
         messageListeners.removeMessageListener( listener );
     } 
 
+    @Override
     public void removeMessageListener( MessageListener<? super HostedConnection> listener, Class... classes )
     {
         messageListeners.removeMessageListener( listener, classes );
@@ -484,12 +512,12 @@ public class DefaultServer implements Server
 
     protected class Connection implements HostedConnection
     {
-        private int id;
+        private final int id;
         private boolean closed;
         private Endpoint[] channels;
         private int setChannelCount = 0; 
        
-        private Map<String,Object> sessionData = new ConcurrentHashMap<String,Object>();       
+        private final Map<String,Object> sessionData = new ConcurrentHashMap<String,Object>();       
         
         public Connection( int channelCount )
         {
@@ -523,23 +551,30 @@ public class DefaultServer implements Server
             return setChannelCount == channels.length;
         }
  
+        @Override
         public Server getServer()
         {   
             return DefaultServer.this;
         }     
        
+        @Override
         public int getId()
         {
             return id;
         }
  
+        @Override
         public String getAddress()
         {            
             return channels[CH_RELIABLE] == null ? null : channels[CH_RELIABLE].getAddress();
         }
        
+        @Override
         public void send( Message message )
         {
+            if( log.isLoggable(Level.FINER) ) {
+                log.log(Level.FINER, "send({0})", message);
+            }
             ByteBuffer buffer = MessageProtocol.messageToBuffer(message, null);
             if( message.isReliable() || channels[CH_UNRELIABLE] == null ) {
                 channels[CH_RELIABLE].send( buffer );
@@ -548,8 +583,12 @@ public class DefaultServer implements Server
             }
         }
 
+        @Override
         public void send( int channel, Message message )
         {
+            if( log.isLoggable(Level.FINER) ) {
+                log.log(Level.FINER, "send({0}, {1})", new Object[]{channel, message});
+            }
             checkChannel(channel);
             ByteBuffer buffer = MessageProtocol.messageToBuffer(message, null);
             channels[channel+CH_FIRST].send(buffer);
@@ -573,6 +612,7 @@ public class DefaultServer implements Server
             fireConnectionRemoved( this );
         }
         
+        @Override
         public void close( String reason )
         {
             // Send a reason
@@ -593,6 +633,7 @@ public class DefaultServer implements Server
             }
         }
         
+        @Override
         public Object setAttribute( String name, Object value )
         {
             if( value == null )
@@ -601,11 +642,13 @@ public class DefaultServer implements Server
         }
     
         @SuppressWarnings("unchecked")
+        @Override
         public <T> T getAttribute( String name )
         {
             return (T)sessionData.get(name);
         }             
 
+        @Override
         public Set<String> attributeNames()
         {
             return Collections.unmodifiableSet(sessionData.keySet());
@@ -621,6 +664,7 @@ public class DefaultServer implements Server
 
     protected class Redispatch implements MessageListener<HostedConnection>
     {
+        @Override
         public void messageReceived( HostedConnection source, Message m )
         {
             dispatch( source, m );
@@ -629,13 +673,14 @@ public class DefaultServer implements Server
      
     protected class FilterAdapter implements Filter<Endpoint>
     {
-        private Filter<? super HostedConnection> delegate;
+        private final Filter<? super HostedConnection> delegate;
         
         public FilterAdapter( Filter<? super HostedConnection> delegate )
         {
             this.delegate = delegate;
         }
         
+        @Override
         public boolean apply( Endpoint input )
         {
             HostedConnection conn = getConnection( input );

+ 1 - 1
jme3-networking/src/main/java/com/jme3/network/base/MessageProtocol.java

@@ -53,7 +53,7 @@ import java.util.LinkedList;
  */
 public class MessageProtocol
 {
-    private LinkedList<Message> messages = new LinkedList<Message>();
+    private final LinkedList<Message> messages = new LinkedList<Message>();
     private ByteBuffer current;
     private int size;
     private Byte carry;

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

+ 5 - 0
jme3-networking/src/main/java/com/jme3/network/serializing/Serializer.java

@@ -45,6 +45,7 @@ import java.nio.ByteBuffer;
 import java.util.*;
 import java.util.jar.Attributes;
 import java.util.logging.Level;
+import java.util.logging.LogManager;
 import java.util.logging.Logger;
 
 /**
@@ -403,6 +404,10 @@ public abstract class Serializer {
         if (reg == null) {
             throw new SerializerException( "Class not registered:" + type );
         }
+        
+        if( log.isLoggable(Level.FINER) ) {
+            log.log(Level.FINER, "writing class:{0} with ID:{1}", new Object[]{type, reg.getId()});
+        }
         buffer.putShort(reg.getId());
         return reg;
     }

+ 1 - 1
jme3-networking/src/main/java/com/jme3/network/service/AbstractService.java

@@ -106,6 +106,6 @@ public abstract class AbstractService<S extends ServiceManager> implements Servi
     
     @Override
     public String toString() {
-        return getClass().getName() + "[serviceManager.class=" + serviceManager.getClass() + "]";
+        return getClass().getName() + "[serviceManager.class=" + (serviceManager != null ? serviceManager.getClass() : "") + "]";
     }
 }

+ 29 - 1
jme3-networking/src/main/java/com/jme3/network/service/ServiceManager.java

@@ -34,6 +34,8 @@ package com.jme3.network.service;
 
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 /**
  *  The base service manager class from which the HostedServiceManager
@@ -44,7 +46,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
  */
 public abstract class ServiceManager<T> {
 
-    private List<Service<T>> services = new CopyOnWriteArrayList<Service<T>>();
+    static final Logger log = Logger.getLogger(ServiceManager.class.getName());
+    
+    private final List<Service<T>> services = new CopyOnWriteArrayList<Service<T>>();
     private volatile boolean started = false;
     
     protected ServiceManager() {
@@ -76,6 +80,9 @@ public abstract class ServiceManager<T> {
             return;
         }
         for( Service<T> s : services ) {
+            if( log.isLoggable(Level.FINE) ) {
+                log.log(Level.FINE, "Starting service:{0}", s);
+            }
             s.start();
         }
         started = true;
@@ -96,6 +103,9 @@ public abstract class ServiceManager<T> {
             throw new IllegalStateException(getClass().getSimpleName() + " not started.");
         }
         for( Service<T> s : services ) {
+            if( log.isLoggable(Level.FINE) ) {
+                log.log(Level.FINE, "Stopping service:{0}", s);
+            }
             s.stop();
         }
         started = false;
@@ -106,9 +116,18 @@ public abstract class ServiceManager<T> {
      *  has already been started then the service will also be started.
      */   
     public <S extends Service<T>> void addService( S s ) {
+        if( log.isLoggable(Level.FINE) ) {
+            log.log(Level.FINE, "addService({0})", s);
+        }
         services.add(s);
+        if( log.isLoggable(Level.FINE) ) {
+            log.log(Level.FINE, "Initializing service:{0}", s);
+        }
         s.initialize(getParent());
         if( started ) {
+            if( log.isLoggable(Level.FINE) ) {
+                log.log(Level.FINE, "Starting service:{0}", s);
+            }
             s.start();
         }
     }
@@ -120,10 +139,19 @@ public abstract class ServiceManager<T> {
      *  the service will be terminated.
      */
     public <S extends Service<T>> void removeService( S s ) {
+        if( log.isLoggable(Level.FINE) ) {
+            log.log(Level.FINE, "removeService({0})", s);
+        }
         if( started ) {
+            if( log.isLoggable(Level.FINE) ) {
+                log.log(Level.FINE, "Stopping service:{0}", s);
+            }
             s.stop();
         }
         services.remove(s);
+        if( log.isLoggable(Level.FINE) ) {
+            log.log(Level.FINE, "Terminating service:{0}", s);
+        }
         s.terminate(getParent());        
     }
  

+ 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;

+ 1 - 0
sdk/jme3-core/src/com/jme3/gde/core/properties/TextureBrowser.form

@@ -9,6 +9,7 @@
   </Properties>
   <SyntheticProperties>
     <SyntheticProperty name="formSizePolicy" type="int" value="1"/>
+    <SyntheticProperty name="generateCenter" type="boolean" value="false"/>
   </SyntheticProperties>
   <AuxValues>
     <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>

+ 19 - 24
sdk/jme3-core/src/com/jme3/gde/core/properties/TextureBrowser.java

@@ -31,9 +31,8 @@
  */
 package com.jme3.gde.core.properties;
 
-import com.jme3.asset.TextureKey;
 import com.jme3.gde.core.assets.ProjectAssetManager;
-import com.jme3.gde.core.properties.preview.DDSPreview;
+import com.jme3.gde.core.properties.preview.TexturePreview;
 import com.jme3.gde.core.util.TreeUtil;
 import com.jme3.texture.Texture;
 import java.awt.event.MouseEvent;
@@ -42,6 +41,7 @@ import java.awt.event.WindowListener;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.prefs.Preferences;
 import javax.swing.DefaultListSelectionModel;
@@ -52,8 +52,6 @@ import javax.swing.tree.DefaultMutableTreeNode;
 import javax.swing.tree.TreeNode;
 import javax.swing.tree.TreePath;
 import javax.swing.tree.TreeSelectionModel;
-import jme3tools.converters.ImageToAwt;
-import org.openide.util.ImageUtilities;
 
 /**
  * Displays all textures in the ProjectAssetManager,
@@ -68,7 +66,7 @@ public class TextureBrowser extends javax.swing.JDialog implements TreeSelection
 
     private ProjectAssetManager assetManager;
     private TexturePropertyEditor editor;
-    private DDSPreview ddsPreview;
+    private TexturePreview texPreview;
     private Preferences prefs;
     private static final String PREF_LAST_SELECTED = "lastSelectedTexture";
 
@@ -234,8 +232,8 @@ private void noTexturebuttonActionPerformed(java.awt.event.ActionEvent evt) {//G
         if (node != null && node.isLeaf()) {
             String selected = TreeUtil.getPath(node.getUserObjectPath());
             selected = selected.substring(0, selected.lastIndexOf("/"));
-            Texture tex = assetManager.loadTexture(selected);
-            editor.setValue(tex);
+//            Texture tex = assetManager.loadTexture(selected);
+//            editor.setValue(tex);
             editor.setAsText(selected);
             return true;
         }
@@ -270,7 +268,7 @@ private void noTexturebuttonActionPerformed(java.awt.event.ActionEvent evt) {//G
 
     private void setSelectedTexture(Texture texture) {
         if (texture != null) {
-            Logger.getLogger(TextureBrowser.class.getName()).finer("Looking for Texture: " + texture.getName());
+            Logger.getLogger(TextureBrowser.class.getName()).log(Level.FINER, "Looking for Texture: {0}", texture.getName());
             String[] path = ("/" + texture.getName()).split("/");
             TreePath parent = new TreePath((TreeNode) jTree1.getModel().getRoot());
             TreePath selectedTreePath = TreeUtil.buildTreePath(jTree1, parent, path, 0, true);
@@ -287,6 +285,7 @@ private void noTexturebuttonActionPerformed(java.awt.event.ActionEvent evt) {//G
         }
     }
 
+    @Override
     public void valueChanged(TreeSelectionEvent e) {
         DefaultMutableTreeNode node = (DefaultMutableTreeNode) jTree1.getLastSelectedPathComponent();
 
@@ -295,25 +294,14 @@ private void noTexturebuttonActionPerformed(java.awt.event.ActionEvent evt) {//G
             return;
         }
 
-        //Object nodeInfo = node.getUserObject();
         if (node.isLeaf()) {
             String selected = TreeUtil.getPath(node.getUserObjectPath());
             selected = selected.substring(0, selected.lastIndexOf("/"));
             Icon newicon = null;
-            if (selected.toLowerCase().endsWith(".dds")) {
-                if (ddsPreview == null) {
-                    ddsPreview = new DDSPreview(assetManager);
-                }
-                ddsPreview.requestPreview(selected, (String) node.getUserObject(), 450, 450, imagePreviewLabel, infoLabel);
-
-            } else {
-                Texture tex = assetManager.loadTexture(selected);
-                newicon = ImageUtilities.image2Icon(ImageToAwt.convert(tex.getImage(), false, true, 0));
-                assetManager.deleteFromCache(new TextureKey(selected));
-                imagePreviewLabel.setIcon(newicon);
-                infoLabel.setText(" " + node.getUserObject() + "    w : " + newicon.getIconWidth() + "    h : " + newicon.getIconHeight());
+            if (texPreview == null) {
+                texPreview = new TexturePreview(assetManager);
             }
-            
+            texPreview.requestPreview(selected, (String) node.getUserObject(), 450, 450, imagePreviewLabel, infoLabel);
             prefs.put(PREF_LAST_SELECTED, selected);
         } else {
             imagePreviewLabel.setIcon(null);
@@ -323,27 +311,34 @@ private void noTexturebuttonActionPerformed(java.awt.event.ActionEvent evt) {//G
 
     }
 
+    @Override
     public void windowOpened(WindowEvent e) {
     }
 
+    @Override
     public void windowClosing(WindowEvent e) {
-        if (ddsPreview != null) {
-            ddsPreview.cleanUp();
+        if (texPreview != null) {
+            texPreview.cleanUp();
         }
     }
 
+    @Override
     public void windowClosed(WindowEvent e) {
     }
 
+    @Override
     public void windowIconified(WindowEvent e) {
     }
 
+    @Override
     public void windowDeiconified(WindowEvent e) {
     }
 
+    @Override
     public void windowActivated(WindowEvent e) {
     }
 
+    @Override
     public void windowDeactivated(WindowEvent e) {
     }
 

+ 7 - 0
sdk/jme3-core/src/com/jme3/gde/core/properties/TexturePropertyEditor.java

@@ -80,7 +80,14 @@ public class TexturePropertyEditor implements PropertyEditor {
         }        
     }
 
+    @Override
     public Object getValue() {
+        if(texture == null && assetKey != null){
+            if (manager == null){
+                manager = SceneApplication.getApplication().getAssetManager();
+            }
+            texture = manager.loadTexture(assetKey);
+        }
         return texture;
     }
 

+ 0 - 140
sdk/jme3-core/src/com/jme3/gde/core/properties/preview/DDSPreview.java

@@ -1,140 +0,0 @@
-/*
- *  Copyright (c) 2009-2010 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.gde.core.properties.preview;
-
-import com.jme3.asset.TextureKey;
-import com.jme3.gde.core.assets.ProjectAssetManager;
-import com.jme3.gde.core.scene.PreviewRequest;
-import com.jme3.gde.core.scene.SceneApplication;
-import com.jme3.gde.core.scene.SceneListener;
-import com.jme3.gde.core.scene.SceneRequest;
-import com.jme3.material.Material;
-import com.jme3.math.Vector2f;
-import com.jme3.math.Vector3f;
-import com.jme3.scene.Geometry;
-import com.jme3.scene.Spatial;
-import com.jme3.scene.shape.Quad;
-import com.jme3.texture.Texture;
-import com.jme3.util.SkyFactory;
-import javax.swing.ImageIcon;
-import javax.swing.JButton;
-import javax.swing.JComponent;
-import javax.swing.JLabel;
-
-/**
- *
- * @author Nehon
- */
-public class DDSPreview implements SceneListener {
-
-    private final ProjectAssetManager assetManager;
-    private JComponent picPreview;
-    private final Geometry quad;
-    private final Geometry quad3D;
-    private final Material material;
-    private final Material material3D;
-
-    public DDSPreview(ProjectAssetManager assetManager) {
-        this.assetManager = assetManager;
-
-        Quad quadMesh = new Quad(4.5f, 4.5f);
-        Quad quadMesh3D = new Quad(4.5f, 4.5f);
-        quadMesh3D.scaleTextureCoordinates(new Vector2f(4, 4));
-        quad = new Geometry("previewQuad", quadMesh);
-        quad.setLocalTranslation(new Vector3f(-2.25f, -2.25f, 0));
-        quad3D = new Geometry("previewQuad", quadMesh3D);
-        quad3D.setLocalTranslation(new Vector3f(-2.25f, -2.25f, 0));
-        material3D = new Material(assetManager, "com/jme3/gde/core/properties/preview/tex3DThumb.j3md");
-        material3D.setFloat("InvDepth", 1f / 16f);
-        material3D.setInt("Rows", 4);
-        material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
-        SceneApplication.getApplication().addSceneListener(this);
-    }
-
-    public void requestPreview(String textureName, String displayName, int width, int height, JComponent picLabel, JLabel infoLabel) {
-        TextureKey key = new TextureKey(textureName);
-        picPreview = picLabel;
-        assetManager.deleteFromCache(key);
-        Texture t = assetManager.loadTexture(key);
-        Spatial geom = quad;
-        if (key.getTextureTypeHint() == Texture.Type.TwoDimensional) {
-            material.setTexture("ColorMap", t);
-            geom.setMaterial(material);
-            if (infoLabel != null) {
-                infoLabel.setText(" " + displayName + "    w : " + t.getImage().getWidth() + "    h : " + t.getImage().getHeight());
-            }
-        } else if (key.getTextureTypeHint() == Texture.Type.ThreeDimensional) {
-            geom = quad3D;
-            assetManager.deleteFromCache(key);
-            key.setTextureTypeHint(Texture.Type.ThreeDimensional);
-            t = assetManager.loadTexture(key);
-            material3D.setTexture("Texture", t);
-            geom.setMaterial(material3D);
-            if (infoLabel != null) {
-                infoLabel.setText(" " + displayName + " (Texture3D)    w : " + t.getImage().getWidth() + "    h : " + t.getImage().getHeight() + "    d : " + t.getImage().getDepth());
-            }
-        } else if (key.getTextureTypeHint() == Texture.Type.CubeMap) {
-            assetManager.deleteFromCache(key);
-            geom = SkyFactory.createSky(assetManager, textureName, SkyFactory.EnvMapType.CubeMap);
-            if (infoLabel != null) {
-                infoLabel.setText(" " + displayName + " (CubeMap)    w : " + t.getImage().getWidth() + "    h : " + t.getImage().getHeight());
-            }
-        }
-
-        PreviewRequest request = new PreviewRequest(this, geom, width, height);
-        request.getCameraRequest().setLocation(new Vector3f(0, 0, 5.3f));
-        request.getCameraRequest().setLookAt(new Vector3f(0, 0, 0), Vector3f.UNIT_Y.mult(-1));
-        SceneApplication.getApplication().createPreview(request);
-    }
-
-    public void cleanUp() {
-        SceneApplication.getApplication().removeSceneListener(this);
-    }
-
-    public void sceneOpened(SceneRequest request) {
-    }
-
-    public void sceneClosed(SceneRequest request) {
-    }
-
-    public void previewCreated(PreviewRequest request) {
-        if (request.getRequester() == this) {
-            final ImageIcon icon = new ImageIcon(request.getImage());
-            if (picPreview instanceof JLabel) {
-                ((JLabel) picPreview).setIcon(icon);
-            }
-            if (picPreview instanceof JButton) {
-                ((JButton) picPreview).setIcon(icon);
-            }
-        }
-    }
-}

+ 179 - 0
sdk/jme3-core/src/com/jme3/gde/core/properties/preview/TexturePreview.java

@@ -0,0 +1,179 @@
+/*
+ *  Copyright (c) 2009-2010 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.gde.core.properties.preview;
+
+import com.jme3.asset.TextureKey;
+import com.jme3.gde.core.assets.ProjectAssetManager;
+import com.jme3.gde.core.scene.PreviewRequest;
+import com.jme3.gde.core.scene.SceneApplication;
+import com.jme3.gde.core.scene.SceneListener;
+import com.jme3.gde.core.scene.SceneRequest;
+import com.jme3.material.Material;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.shape.Quad;
+import com.jme3.texture.Texture;
+import com.jme3.util.SkyFactory;
+import java.util.concurrent.Callable;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+
+/**
+ *
+ * @author Nehon
+ */
+public class TexturePreview implements SceneListener {
+
+    private final ProjectAssetManager assetManager;
+    private JComponent picPreview;
+    private final Geometry quad;
+    private final Geometry quad3D;
+    private final Material material;
+    private final Material material3D;
+
+    public TexturePreview(ProjectAssetManager assetManager) {
+        this.assetManager = assetManager;
+
+        Quad quadMesh = new Quad(4.5f, 4.5f);
+        Quad quadMesh3D = new Quad(4.5f, 4.5f);
+        quadMesh3D.scaleTextureCoordinates(new Vector2f(4, 4));
+        quad = new Geometry("previewQuad", quadMesh);
+        quad.setLocalTranslation(new Vector3f(-2.25f, -2.25f, 0));
+        quad3D = new Geometry("previewQuad", quadMesh3D);
+        quad3D.setLocalTranslation(new Vector3f(-2.25f, -2.25f, 0));
+        material3D = new Material(assetManager, "com/jme3/gde/core/properties/preview/tex3DThumb.j3md");
+        material3D.setFloat("InvDepth", 1f / 16f);
+        material3D.setInt("Rows", 4);
+        material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        SceneApplication.getApplication().addSceneListener(this);
+    }
+
+    public void requestPreview(final String textureName, final String displayName, final int width, final int height, final JComponent picLabel, final JLabel infoLabel) {
+
+        picPreview = picLabel;
+        clearPreview();
+        if (infoLabel != null) {
+            infoLabel.setText(" Creating preview...");
+        }
+
+        SceneApplication.getApplication().enqueue(new Callable<Void>() {
+
+            @Override
+            public Void call() throws Exception {
+                TextureKey key = new TextureKey(textureName);
+                Texture t = assetManager.loadTexture(key);
+                Spatial geom = quad;
+                if (key.getTextureTypeHint() == Texture.Type.TwoDimensional) {
+                    material.setTexture("ColorMap", t);
+                    geom.setMaterial(material);                    
+                    setLabel(infoLabel, displayName, t.getImage().getWidth(),  t.getImage().getHeight(), -1);                    
+                } else if (key.getTextureTypeHint() == Texture.Type.ThreeDimensional) {
+                    geom = quad3D;
+                    material3D.setTexture("Texture", t);
+                    geom.setMaterial(material3D);
+                    setLabel(infoLabel, displayName + " (Texture3D)", t.getImage().getWidth(),  t.getImage().getHeight(),  t.getImage().getDepth());
+                    
+                } else if (key.getTextureTypeHint() == Texture.Type.CubeMap) {
+                    geom = SkyFactory.createSky(assetManager, textureName, SkyFactory.EnvMapType.CubeMap);
+                    setLabel(infoLabel, displayName + " (CubeMap)", t.getImage().getWidth(),  t.getImage().getHeight(),  -1);                        
+                }
+
+                PreviewRequest request = new PreviewRequest(TexturePreview.this, geom, width, height);
+                request.getCameraRequest().setLocation(new Vector3f(0, 0, 5.3f));
+                request.getCameraRequest().setLookAt(new Vector3f(0, 0, 0), Vector3f.UNIT_Y.mult(-1));
+                SceneApplication.getApplication().createPreview(request);
+
+                return null;
+            }
+
+        });
+    }
+
+    public void cleanUp() {
+        SceneApplication.getApplication().removeSceneListener(this);
+    }
+
+    @Override
+    public void sceneOpened(SceneRequest request) {
+    }
+
+    @Override
+    public void sceneClosed(SceneRequest request) {
+    }
+
+    private void setLabel(final JLabel label, final String text, final int width, final int height, final int depth) {
+
+        java.awt.EventQueue.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                if (label != null) {
+                    String labText = " " + text + "    w : " + width + "    h : " + height;
+                    if (depth > 0) {
+                        labText += "    d : " + depth;
+                    }
+                    label.setText(labText);
+                }
+            }
+        });
+    }
+    
+    private void clearPreview() {
+        if (picPreview instanceof JLabel) {
+            ((JLabel) picPreview).setIcon(null);
+        }
+        if (picPreview instanceof JButton) {
+            ((JButton) picPreview).setIcon(null);
+        }
+    }
+
+    @Override
+    public void previewCreated(final PreviewRequest request) {
+        java.awt.EventQueue.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                if (request.getRequester() == TexturePreview.this) {
+                    final ImageIcon icon = new ImageIcon(request.getImage());
+                    if (picPreview instanceof JLabel) {
+                        ((JLabel) picPreview).setIcon(icon);
+                    }
+                    if (picPreview instanceof JButton) {
+                        ((JButton) picPreview).setIcon(icon);
+                    }
+                }
+            }
+        });
+    }
+}

+ 4 - 5
sdk/jme3-core/src/com/jme3/gde/core/properties/preview/tex3DThumb.frag

@@ -5,10 +5,9 @@ uniform float m_InvDepth;
 varying vec2 texCoord;
 
 void main(){
-float depthx=floor(texCoord.x);
-    float depthy=(m_Rows-1.0) - floor(texCoord.y);    
-    //vec3 texC=vec3(texCoord.x,texCoord.y ,0.7);//
+    float depthx = floor(texCoord.x);
+    float depthy = (float(m_Rows) - 1.0) - floor(texCoord.y);        
   
-        vec3 texC=vec3(fract(texCoord.x),fract(texCoord.y),(depthy*m_Rows+depthx)*m_InvDepth);//
-    gl_FragColor= texture3D(m_Texture,texC);
+    vec3 texC = vec3(fract(texCoord.x), fract(texCoord.y), (depthy * float(m_Rows) + depthx) * m_InvDepth);//
+    gl_FragColor = texture3D(m_Texture, texC);
 }

+ 2 - 2
sdk/jme3-core/src/com/jme3/gde/core/properties/preview/tex3DThumb.vert

@@ -6,6 +6,6 @@ attribute vec3 inPosition;
 varying vec2 texCoord;
 
 void main(){
-    gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition,1.0);
-    texCoord=inTexCoord;
+    gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0);
+    texCoord = inTexCoord;
 }

+ 8 - 17
sdk/jme3-materialeditor/src/com/jme3/gde/materials/multiview/widgets/TexturePanel.java

@@ -13,7 +13,7 @@ package com.jme3.gde.materials.multiview.widgets;
 import com.jme3.asset.AssetNotFoundException;
 import com.jme3.gde.core.assets.ProjectAssetManager;
 import com.jme3.gde.core.properties.TexturePropertyEditor;
-import com.jme3.gde.core.properties.preview.DDSPreview;
+import com.jme3.gde.core.properties.preview.TexturePreview;
 import com.jme3.gde.materials.MaterialProperty;
 import com.jme3.gde.materials.multiview.MaterialEditorTopComponent;
 import com.jme3.texture.Texture;
@@ -39,7 +39,7 @@ public class TexturePanel extends MaterialPropertyWidget {
     private boolean flip = false;
     private boolean repeat = false;
     private String textureName = null;
-    private DDSPreview ddsPreview;
+    private TexturePreview texPreview;
     private final ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(1);
 
     /** Creates new form SelectionPanel */
@@ -53,22 +53,13 @@ public class TexturePanel extends MaterialPropertyWidget {
         if (!"".equals(textureName)) {
             exec.execute(new Runnable() {
 
+                @Override
                 public void run() {
                     try{
-                    Texture tex = manager.loadTexture(textureName);
-                    if (textureName.toLowerCase().endsWith(".dds")) {
-                        if (ddsPreview == null) {
-                            ddsPreview = new DDSPreview(manager);
+                        if (texPreview == null) {
+                            texPreview = new TexturePreview(manager);
                         }
-                        ddsPreview.requestPreview(textureName, "", 80, 80, texturePreview, null);
-                    } else {
-                        final Icon newicon = ImageUtilities.image2Icon(resizeImage(ImageToAwt.convert(tex.getImage(), false, true, 0)));
-                        SwingUtilities.invokeLater(new Runnable() {
-                            public void run() {
-                                texturePreview.setIcon(newicon);
-                            }
-                        });
-                    }
+                        texPreview.requestPreview(textureName, "", 80, 25, texturePreview, null);
                     } catch (AssetNotFoundException a) {
                         Logger.getLogger(MaterialEditorTopComponent.class.getName()).log(Level.WARNING, "Could not load texture {0}", textureName);
                     }
@@ -318,8 +309,8 @@ public class TexturePanel extends MaterialPropertyWidget {
 
     @Override
     public void cleanUp() {
-        if (ddsPreview != null) {
-            ddsPreview.cleanUp();
+        if (texPreview != null) {
+            texPreview.cleanUp();
         }
         exec.shutdownNow();
     }

+ 22 - 40
sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/TerrainEditorTopComponent.java

@@ -34,7 +34,7 @@ package com.jme3.gde.terraineditor;
 import com.jme3.gde.core.assets.AssetDataObject;
 import com.jme3.gde.core.assets.ProjectAssetManager;
 import com.jme3.gde.core.properties.TexturePropertyEditor;
-import com.jme3.gde.core.properties.preview.DDSPreview;
+import com.jme3.gde.core.properties.preview.TexturePreview;
 import com.jme3.gde.core.scene.PreviewRequest;
 import com.jme3.gde.core.scene.SceneApplication;
 import com.jme3.gde.core.scene.SceneListener;
@@ -122,7 +122,7 @@ public final class TerrainEditorTopComponent extends TopComponent implements Sce
     //private TerrainNodeListener terrainDeletedNodeListener;
     private boolean availableNormalTextures;
     private HelpCtx ctx = new HelpCtx("sdk.terrain_editor");
-    private DDSPreview ddsPreview;
+    private TexturePreview texPreview;
     private Map<String, JButton> buttons = new HashMap<String, JButton>();
     private JPanel insideToolSettings;
     
@@ -153,35 +153,29 @@ public final class TerrainEditorTopComponent extends TopComponent implements Sce
         private float max = 0;
         private final Object lock = new Object();
 
+        @Override
         public void incrementProgress(float f) {
             progress += f;
             progressHandle.progress((int) progress);
         }
 
+        @Override
         public void setMonitorMax(float f) {
             max = f;
-//            java.awt.EventQueue.invokeLater(new Runnable() {
-//                public void run() {
-//                    synchronized(lock){
             if (progressHandle == null) {
                 progressHandle = ProgressHandleFactory.createHandle("Calculating terrain entropies...");
                 progressHandle.start((int) max);
             }
-//                    }
-//                }
-//            });
         }
 
+        @Override
         public float getMonitorMax() {
             return max;
         }
 
+        @Override
         public void progressComplete() {
-//            SwingUtilities.invokeLater(new Runnable() {
-//                public void run() {
             progressHandle.finish();
-//                }
-//            });
         }
     }
 
@@ -1631,6 +1625,13 @@ public final class TerrainEditorTopComponent extends TopComponent implements Sce
         protected abstract Texture getTextureFromModel(int index);
 
         protected abstract boolean supportsNullTexture();
+        
+        private TexturePreview getTexturePreview(){
+            if (texPreview == null) {
+                texPreview = new TexturePreview((ProjectAssetManager) SceneApplication.getApplication().getAssetManager());
+            }
+            return texPreview;
+        }
 
         private JButton getButton(Object value, final int row, final int column) {
 
@@ -1656,21 +1657,9 @@ public final class TerrainEditorTopComponent extends TopComponent implements Sce
                     }
 
                     Texture tex = getTextureFromModel(index); // delegate to sub-class
-
-                    //Texture tex = SceneApplication.getApplication().getAssetManager().loadTexture((String)value);
                     if (tex != null) {
                         String selected = tex.getKey().getName();
-
-                        if (selected.toLowerCase().endsWith(".dds")) {
-                            if (ddsPreview == null) {
-                                ddsPreview = new DDSPreview((ProjectAssetManager) SceneApplication.getApplication().getAssetManager());
-                            }
-                            ddsPreview.requestPreview(selected, "", 80, 80, lbl, null);
-
-                        } else {
-                            Icon icon = ImageUtilities.image2Icon(ImageToAwt.convert(tex.getImage(), false, true, 0));
-                            lbl.setIcon(icon);
-                        }
+                        getTexturePreview().requestPreview(selected, "", 80, 80, lbl, null);
                     }
 
                 }
@@ -1694,24 +1683,17 @@ public final class TerrainEditorTopComponent extends TopComponent implements Sce
                             TexturePropertyEditor editor = new TexturePropertyEditor(selectedTex);
                             Component view = editor.getCustomEditor();
                             view.setVisible(true);
-                            Texture tex = (Texture) editor.getValue();
-                            if (editor.getValue() != null) {
-                                String selected = tex.getKey().getName();
-
-                                if (selected.toLowerCase().endsWith(".dds")) {
-                                    if (ddsPreview == null) {
-                                        ddsPreview = new DDSPreview((ProjectAssetManager) SceneApplication.getApplication().getAssetManager());
-                                    }
-                                    ddsPreview.requestPreview(selected, "", 80, 80, lbl, null);
-
-                                } else {
-                                    Icon newicon = ImageUtilities.image2Icon(ImageToAwt.convert(tex.getImage(), false, true, 0));
-                                    lbl.setIcon(newicon);
-                                }
+                            
+                            if (editor.getAsText() != null) {
+                                
+                                String selected = editor.getAsText();
+                                getTexturePreview().requestPreview(selected, "", 80, 80, lbl, null);
+                                Texture tex = SceneApplication.getApplication().getAssetManager().loadTexture(selected);
+                                setTextureInModel(row, tex);
                             } else if (supportsNullTexture()) {
                                 lbl.setIcon(null);
                             }
-                            setTextureInModel(row, tex);
+                            
                         } finally {
                             alreadyChoosing = false;
                         }

+ 1 - 2
sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/TerrainToolController.java

@@ -260,8 +260,7 @@ public class TerrainToolController extends SceneToolController {
      * The action on the tool has ended (mouse button up), record the Undo (for painting only now)
      */
     void doTerrainEditToolActionEnded() {
-        if (terrainTool != null) {
-            System.out.println("undo tagged");
+        if (terrainTool != null) {            
             terrainTool.actionEnded(jmeRootNode, editorController.getCurrentDataObject());
         }
     }

+ 5 - 3
sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/sky/AddSkyboxAction.java

@@ -69,14 +69,16 @@ public class AddSkyboxAction extends AbstractNewSpatialWizardAction {
         } else {
             Texture textureSingle = (Texture) wiz.getProperty("textureSingle");
             Vector3f normalScale = (Vector3f) wiz.getProperty("normalScale");
-            boolean useSpheremap = (Boolean) wiz.getProperty("useSpheremap");
+            SkyFactory.EnvMapType type = (SkyFactory.EnvMapType) wiz.getProperty("envMapType");
             boolean flipY = (Boolean) wiz.getProperty("flipY");
             // reload the texture so we can use flipY
             TextureKey key = (TextureKey) textureSingle.getKey();
             TextureKey newKey = new TextureKey(key.getName(), flipY);
             newKey.setGenerateMips(true);
-            newKey.setAsCube(!useSpheremap);
-            return SkyFactory.createSky(pm, pm.loadTexture(newKey), normalScale, useSpheremap);
+            if(type == SkyFactory.EnvMapType.CubeMap){
+                newKey.setTextureTypeHint(Texture.Type.CubeMap);
+            }
+            return SkyFactory.createSky(pm, pm.loadTexture(newKey), normalScale, type); 
         }
     }
 

+ 1 - 1
sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/sky/Bundle.properties

@@ -17,7 +17,6 @@ SkyboxVisualPanel2.multipleTexWestLoadButton.text=Load
 SkyboxVisualPanel2.multipleTexTopLoadButton.text=Load
 SkyboxVisualPanel2.multipleTexBottomLoadButton.text=Load
 SkyboxVisualPanel2.jLabel9.text=Normal Scale (x,y,z):
-SkyboxVisualPanel2.spheremapCheckBox.text=Sphere map
 SkyboxVisualPanel2.normal1X.text=1
 SkyboxVisualPanel2.normal1Y.text=1
 SkyboxVisualPanel2.normal1Z.text=1
@@ -32,3 +31,4 @@ SkyboxVisualPanel2.topPic.text=
 SkyboxVisualPanel2.bottomPic.text=
 SkyboxVisualPanel2.singlePic.text=
 SkyboxVisualPanel2.flipYcheckBox.text=Flip Y
+SkyboxVisualPanel2.jLabel10.text=Map type

+ 37 - 17
sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/sky/SkyboxVisualPanel2.form

@@ -1,4 +1,4 @@
-<?xml version="1.1" encoding="UTF-8" ?>
+<?xml version="1.0" encoding="UTF-8" ?>
 
 <Form version="1.5" maxVersion="1.7" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
   <AuxValues>
@@ -127,7 +127,7 @@
                           </Group>
                       </Group>
                   </Group>
-                  <EmptySpace pref="27" max="32767" attributes="0"/>
+                  <EmptySpace pref="29" max="32767" attributes="0"/>
               </Group>
           </Group>
         </DimensionLayout>
@@ -172,7 +172,7 @@
                               <Component id="jLabel5" alignment="3" min="-2" max="-2" attributes="0"/>
                               <Component id="multipleTexTopLoadButton" alignment="3" min="-2" max="-2" attributes="0"/>
                           </Group>
-                          <EmptySpace max="-2" attributes="0"/>
+                          <EmptySpace max="32767" attributes="0"/>
                       </Group>
                       <Group type="102" alignment="0" attributes="0">
                           <Component id="westPic" min="-2" pref="20" max="-2" attributes="0"/>
@@ -376,11 +376,17 @@
               <Group type="102" attributes="0">
                   <EmptySpace max="-2" attributes="0"/>
                   <Group type="103" groupAlignment="0" attributes="0">
+                      <Component id="flipYcheckBox" alignment="0" min="-2" pref="100" max="-2" attributes="1"/>
                       <Group type="102" alignment="0" attributes="0">
-                          <Component id="jLabel8" min="-2" max="-2" attributes="0"/>
+                          <Component id="jLabel10" min="-2" max="-2" attributes="0"/>
                           <EmptySpace max="-2" attributes="0"/>
+                          <Component id="mapTypeCombo" min="-2" pref="166" max="-2" attributes="0"/>
+                      </Group>
+                      <Group type="102" alignment="0" attributes="0">
+                          <Component id="jLabel8" min="-2" max="-2" attributes="0"/>
+                          <EmptySpace min="-2" pref="21" max="-2" attributes="0"/>
                           <Component id="singleTexLoadButton" min="-2" max="-2" attributes="0"/>
-                          <EmptySpace max="-2" attributes="0"/>
+                          <EmptySpace min="-2" pref="2" max="-2" attributes="0"/>
                           <Component id="singlePic" min="-2" pref="20" max="-2" attributes="0"/>
                           <EmptySpace min="-2" pref="39" max="-2" attributes="0"/>
                           <Component id="jLabel9" min="-2" max="-2" attributes="0"/>
@@ -391,12 +397,8 @@
                           <EmptySpace min="6" pref="6" max="-2" attributes="0"/>
                           <Component id="normal2Z" min="-2" pref="21" max="-2" attributes="1"/>
                       </Group>
-                      <Group type="103" alignment="0" groupAlignment="1" max="-2" attributes="0">
-                          <Component id="flipYcheckBox" alignment="1" max="32767" attributes="1"/>
-                          <Component id="spheremapCheckBox" alignment="1" max="32767" attributes="1"/>
-                      </Group>
                   </Group>
-                  <EmptySpace pref="31" max="32767" attributes="0"/>
+                  <EmptySpace max="32767" attributes="0"/>
               </Group>
           </Group>
         </DimensionLayout>
@@ -420,7 +422,10 @@
                       </Group>
                   </Group>
                   <EmptySpace type="unrelated" max="-2" attributes="0"/>
-                  <Component id="spheremapCheckBox" min="-2" max="-2" attributes="0"/>
+                  <Group type="103" groupAlignment="3" attributes="0">
+                      <Component id="mapTypeCombo" alignment="3" min="-2" max="-2" attributes="0"/>
+                      <Component id="jLabel10" alignment="3" min="-2" max="-2" attributes="0"/>
+                  </Group>
                   <EmptySpace max="-2" attributes="0"/>
                   <Component id="flipYcheckBox" min="-2" max="-2" attributes="0"/>
                   <EmptySpace pref="75" max="32767" attributes="0"/>
@@ -474,24 +479,39 @@
             </Property>
           </Properties>
         </Component>
-        <Component class="javax.swing.JCheckBox" name="spheremapCheckBox">
+        <Component class="javax.swing.JLabel" name="singlePic">
           <Properties>
             <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
-              <ResourceString bundle="com/jme3/gde/terraineditor/sky/Bundle.properties" key="SkyboxVisualPanel2.spheremapCheckBox.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+              <ResourceString bundle="com/jme3/gde/terraineditor/sky/Bundle.properties" key="SkyboxVisualPanel2.singlePic.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
             </Property>
           </Properties>
         </Component>
-        <Component class="javax.swing.JLabel" name="singlePic">
+        <Component class="javax.swing.JCheckBox" name="flipYcheckBox">
           <Properties>
             <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
-              <ResourceString bundle="com/jme3/gde/terraineditor/sky/Bundle.properties" key="SkyboxVisualPanel2.singlePic.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+              <ResourceString bundle="com/jme3/gde/terraineditor/sky/Bundle.properties" key="SkyboxVisualPanel2.flipYcheckBox.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
             </Property>
           </Properties>
         </Component>
-        <Component class="javax.swing.JCheckBox" name="flipYcheckBox">
+        <Component class="javax.swing.JComboBox" name="mapTypeCombo">
+          <Properties>
+            <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor">
+              <StringArray count="4">
+                <StringItem index="0" value="Item 1"/>
+                <StringItem index="1" value="Item 2"/>
+                <StringItem index="2" value="Item 3"/>
+                <StringItem index="3" value="Item 4"/>
+              </StringArray>
+            </Property>
+          </Properties>
+          <Events>
+            <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="mapTypeComboActionPerformed"/>
+          </Events>
+        </Component>
+        <Component class="javax.swing.JLabel" name="jLabel10">
           <Properties>
             <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
-              <ResourceString bundle="com/jme3/gde/terraineditor/sky/Bundle.properties" key="SkyboxVisualPanel2.flipYcheckBox.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+              <ResourceString bundle="com/jme3/gde/terraineditor/sky/Bundle.properties" key="SkyboxVisualPanel2.jLabel10.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
             </Property>
           </Properties>
         </Component>

+ 75 - 118
sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/sky/SkyboxVisualPanel2.java

@@ -33,10 +33,12 @@ package com.jme3.gde.terraineditor.sky;
 
 import com.jme3.gde.core.assets.ProjectAssetManager;
 import com.jme3.gde.core.properties.TexturePropertyEditor;
-import com.jme3.gde.core.properties.preview.DDSPreview;
+import com.jme3.gde.core.properties.preview.TexturePreview;
 import com.jme3.gde.core.scene.SceneApplication;
 import com.jme3.texture.Texture;
+import com.jme3.util.SkyFactory;
 import java.awt.Component;
+import javax.swing.DefaultComboBoxModel;
 import javax.swing.Icon;
 import javax.swing.JCheckBox;
 import javax.swing.JPanel;
@@ -46,11 +48,17 @@ import org.openide.util.ImageUtilities;
 
 public final class SkyboxVisualPanel2 extends JPanel {
 
-    private DDSPreview ddsPreview;
+    private TexturePreview texPreview;
 
     /** Creates new form SkyboxVisualPanel2 */
     public SkyboxVisualPanel2() {
         initComponents();
+        
+        DefaultComboBoxModel<SkyFactory.EnvMapType> model = new DefaultComboBoxModel<SkyFactory.EnvMapType>();
+        for (SkyFactory.EnvMapType value : SkyFactory.EnvMapType.values()) {
+            model.addElement(value);
+        }
+        mapTypeCombo.setModel(model);
     }
 
     @Override
@@ -101,6 +109,14 @@ public final class SkyboxVisualPanel2 extends JPanel {
         return editorWest;
     }
 
+    
+    private TexturePreview getTexturePreview(){
+        if (texPreview == null) {
+            texPreview = new TexturePreview((ProjectAssetManager) SceneApplication.getApplication().getAssetManager());
+        }
+        return texPreview;
+    }
+    
     /** This method is called from within the constructor to
      * initialize the form.
      * WARNING: Do NOT modify this code. The content of this method is
@@ -140,11 +156,12 @@ public final class SkyboxVisualPanel2 extends JPanel {
         normal2X = new javax.swing.JTextField();
         normal2Y = new javax.swing.JTextField();
         normal2Z = new javax.swing.JTextField();
-        spheremapCheckBox = new javax.swing.JCheckBox();
         singlePic = new javax.swing.JLabel();
         flipYcheckBox = new javax.swing.JCheckBox();
+        mapTypeCombo = new javax.swing.JComboBox();
+        jLabel10 = new javax.swing.JLabel();
 
-        titleLabel.setFont(new java.awt.Font("Tahoma", 1, 14));
+        titleLabel.setFont(new java.awt.Font("Tahoma", 1, 14)); // NOI18N
         org.openide.awt.Mnemonics.setLocalizedText(titleLabel, org.openide.util.NbBundle.getMessage(SkyboxVisualPanel2.class, "SkyboxVisualPanel2.titleLabel.text")); // NOI18N
 
         org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(SkyboxVisualPanel2.class, "SkyboxVisualPanel2.jLabel1.text")); // NOI18N
@@ -279,7 +296,7 @@ public final class SkyboxVisualPanel2 extends JPanel {
                                 .addComponent(multipleTexTopLoadButton)
                                 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                 .addComponent(topPic, javax.swing.GroupLayout.PREFERRED_SIZE, 20, javax.swing.GroupLayout.PREFERRED_SIZE)))))
-                .addContainerGap(27, Short.MAX_VALUE))
+                .addContainerGap(29, Short.MAX_VALUE))
         );
         multipleTexturePanelLayout.setVerticalGroup(
             multipleTexturePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
@@ -315,7 +332,7 @@ public final class SkyboxVisualPanel2 extends JPanel {
                         .addGroup(multipleTexturePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                             .addComponent(jLabel5)
                             .addComponent(multipleTexTopLoadButton))
-                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED))
+                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                     .addGroup(multipleTexturePanelLayout.createSequentialGroup()
                         .addComponent(westPic, javax.swing.GroupLayout.PREFERRED_SIZE, 20, javax.swing.GroupLayout.PREFERRED_SIZE)
                         .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
@@ -346,12 +363,19 @@ public final class SkyboxVisualPanel2 extends JPanel {
 
         normal2Z.setText(org.openide.util.NbBundle.getMessage(SkyboxVisualPanel2.class, "SkyboxVisualPanel2.normal2Z.text")); // NOI18N
 
-        org.openide.awt.Mnemonics.setLocalizedText(spheremapCheckBox, org.openide.util.NbBundle.getMessage(SkyboxVisualPanel2.class, "SkyboxVisualPanel2.spheremapCheckBox.text")); // NOI18N
-
         org.openide.awt.Mnemonics.setLocalizedText(singlePic, org.openide.util.NbBundle.getMessage(SkyboxVisualPanel2.class, "SkyboxVisualPanel2.singlePic.text")); // NOI18N
 
         org.openide.awt.Mnemonics.setLocalizedText(flipYcheckBox, org.openide.util.NbBundle.getMessage(SkyboxVisualPanel2.class, "SkyboxVisualPanel2.flipYcheckBox.text")); // NOI18N
 
+        mapTypeCombo.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" }));
+        mapTypeCombo.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                mapTypeComboActionPerformed(evt);
+            }
+        });
+
+        org.openide.awt.Mnemonics.setLocalizedText(jLabel10, org.openide.util.NbBundle.getMessage(SkyboxVisualPanel2.class, "SkyboxVisualPanel2.jLabel10.text")); // NOI18N
+
         javax.swing.GroupLayout singleTexturePanelLayout = new javax.swing.GroupLayout(singleTexturePanel);
         singleTexturePanel.setLayout(singleTexturePanelLayout);
         singleTexturePanelLayout.setHorizontalGroup(
@@ -359,11 +383,16 @@ public final class SkyboxVisualPanel2 extends JPanel {
             .addGroup(singleTexturePanelLayout.createSequentialGroup()
                 .addContainerGap()
                 .addGroup(singleTexturePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addComponent(flipYcheckBox, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE)
                     .addGroup(singleTexturePanelLayout.createSequentialGroup()
-                        .addComponent(jLabel8)
+                        .addComponent(jLabel10)
                         .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                        .addComponent(mapTypeCombo, javax.swing.GroupLayout.PREFERRED_SIZE, 166, javax.swing.GroupLayout.PREFERRED_SIZE))
+                    .addGroup(singleTexturePanelLayout.createSequentialGroup()
+                        .addComponent(jLabel8)
+                        .addGap(21, 21, 21)
                         .addComponent(singleTexLoadButton)
-                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                        .addGap(2, 2, 2)
                         .addComponent(singlePic, javax.swing.GroupLayout.PREFERRED_SIZE, 20, javax.swing.GroupLayout.PREFERRED_SIZE)
                         .addGap(39, 39, 39)
                         .addComponent(jLabel9)
@@ -372,11 +401,8 @@ public final class SkyboxVisualPanel2 extends JPanel {
                         .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                         .addComponent(normal2Y, javax.swing.GroupLayout.PREFERRED_SIZE, 20, javax.swing.GroupLayout.PREFERRED_SIZE)
                         .addGap(6, 6, 6)
-                        .addComponent(normal2Z, javax.swing.GroupLayout.PREFERRED_SIZE, 21, javax.swing.GroupLayout.PREFERRED_SIZE))
-                    .addGroup(singleTexturePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false)
-                        .addComponent(flipYcheckBox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
-                        .addComponent(spheremapCheckBox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))
-                .addContainerGap(31, Short.MAX_VALUE))
+                        .addComponent(normal2Z, javax.swing.GroupLayout.PREFERRED_SIZE, 21, javax.swing.GroupLayout.PREFERRED_SIZE)))
+                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
         );
         singleTexturePanelLayout.setVerticalGroup(
             singleTexturePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
@@ -394,7 +420,9 @@ public final class SkyboxVisualPanel2 extends JPanel {
                             .addComponent(normal2Z, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                             .addComponent(jLabel9))))
                 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
-                .addComponent(spheremapCheckBox)
+                .addGroup(singleTexturePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(mapTypeCombo, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                    .addComponent(jLabel10))
                 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                 .addComponent(flipYcheckBox)
                 .addContainerGap(75, Short.MAX_VALUE))
@@ -428,148 +456,76 @@ public final class SkyboxVisualPanel2 extends JPanel {
     private void multipleTexSouthLoadButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_multipleTexSouthLoadButtonActionPerformed
         Component view = editorSouth.getCustomEditor();
         view.setVisible(true);
-        if (editorSouth.getValue() != null) {
-            Texture tex = (Texture) editorSouth.getValue();
-            String selected = tex.getKey().getName();
-
-            if (selected.toLowerCase().endsWith(".dds")) {
-                if (ddsPreview == null) {
-                    ddsPreview = new DDSPreview((ProjectAssetManager) SceneApplication.getApplication().getAssetManager());
-                }
-                ddsPreview.requestPreview(selected, "", 80, 80, southPic, null);
-
-            } else {
-                Icon newicon = ImageUtilities.image2Icon(ImageToAwt.convert(tex.getImage(), false, true, 0));
-                southPic.setIcon(newicon);
-            }
+        if (editorSouth.getAsText()!= null) {            
+            String selected = editorSouth.getAsText();
+            getTexturePreview().requestPreview(selected, "", 80, 80, southPic, null);
         }
     }//GEN-LAST:event_multipleTexSouthLoadButtonActionPerformed
 
     private void multipleTexNorthLoadButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_multipleTexNorthLoadButtonActionPerformed
         Component view = editorNorth.getCustomEditor();
         view.setVisible(true);
-        if (editorNorth.getValue() != null) {
-            Texture tex = (Texture) editorNorth.getValue();
-            String selected = tex.getKey().getName();
-
-            if (selected.toLowerCase().endsWith(".dds")) {
-                if (ddsPreview == null) {
-                    ddsPreview = new DDSPreview((ProjectAssetManager) SceneApplication.getApplication().getAssetManager());
-                }
-                ddsPreview.requestPreview(selected, "", 80, 80, northPic, null);
-
-            } else {
-                Icon newicon = ImageUtilities.image2Icon(ImageToAwt.convert(tex.getImage(), false, true, 0));
-                northPic.setIcon(newicon);
-            }
+        if (editorNorth.getAsText() != null) {            
+            String selected =  editorNorth.getAsText();
+            getTexturePreview().requestPreview(selected, "", 80, 80, northPic, null);
         }
     }//GEN-LAST:event_multipleTexNorthLoadButtonActionPerformed
 
     private void multipleTexEastLoadButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_multipleTexEastLoadButtonActionPerformed
         Component view = editorEast.getCustomEditor();
         view.setVisible(true);
-        if (editorEast.getValue() != null) {
-            Texture tex = (Texture) editorEast.getValue();
-            String selected = tex.getKey().getName();
-
-            if (selected.toLowerCase().endsWith(".dds")) {
-                if (ddsPreview == null) {
-                    ddsPreview = new DDSPreview((ProjectAssetManager) SceneApplication.getApplication().getAssetManager());
-                }
-                ddsPreview.requestPreview(selected, "", 80, 80, eastPic, null);
-
-            } else {
-                Icon newicon = ImageUtilities.image2Icon(ImageToAwt.convert(tex.getImage(), false, true, 0));
-                eastPic.setIcon(newicon);
-            }
+        if (editorEast.getAsText() != null) {            
+            String selected = editorEast.getAsText();
+            getTexturePreview().requestPreview(selected, "", 80, 80, eastPic, null);
         }
     }//GEN-LAST:event_multipleTexEastLoadButtonActionPerformed
 
     private void multipleTexWestLoadButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_multipleTexWestLoadButtonActionPerformed
         Component view = editorWest.getCustomEditor();
         view.setVisible(true);
-        if (editorWest.getValue() != null) {
-            Texture tex = (Texture) editorWest.getValue();
-            String selected = tex.getKey().getName();
-
-            if (selected.toLowerCase().endsWith(".dds")) {
-                if (ddsPreview == null) {
-                    ddsPreview = new DDSPreview((ProjectAssetManager) SceneApplication.getApplication().getAssetManager());
-                }
-                ddsPreview.requestPreview(selected, "", 80, 80, westPic, null);
-
-            } else {
-                Icon newicon = ImageUtilities.image2Icon(ImageToAwt.convert(tex.getImage(), false, true, 0));
-                westPic.setIcon(newicon);
-            }
+        if (editorWest.getAsText() != null) {            
+            String selected = editorWest.getAsText();
+            getTexturePreview().requestPreview(selected, "", 80, 80, westPic, null);
         }
     }//GEN-LAST:event_multipleTexWestLoadButtonActionPerformed
 
     private void multipleTexTopLoadButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_multipleTexTopLoadButtonActionPerformed
         Component view = editorTop.getCustomEditor();
         view.setVisible(true);
-        if (editorTop.getValue() != null) {
-            Texture tex = (Texture) editorTop.getValue();
-            String selected = tex.getKey().getName();
-
-            if (selected.toLowerCase().endsWith(".dds")) {
-                if (ddsPreview == null) {
-                    ddsPreview = new DDSPreview((ProjectAssetManager) SceneApplication.getApplication().getAssetManager());
-                }
-                ddsPreview.requestPreview(selected, "", 80, 80, topPic, null);
-
-            } else {
-                Icon newicon = ImageUtilities.image2Icon(ImageToAwt.convert(tex.getImage(), false, true, 0));
-                topPic.setIcon(newicon);
-            }
+        if (editorTop.getAsText() != null) {            
+            String selected = editorTop.getAsText();
+            getTexturePreview().requestPreview(selected, "", 80, 80, topPic, null);
         }
     }//GEN-LAST:event_multipleTexTopLoadButtonActionPerformed
 
     private void multipleTexBottomLoadButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_multipleTexBottomLoadButtonActionPerformed
         Component view = editorBottom.getCustomEditor();
         view.setVisible(true);
-        if (editorBottom.getValue() != null) {
-            Texture tex = (Texture) editorBottom.getValue();
-            String selected = tex.getKey().getName();
-
-            if (selected.toLowerCase().endsWith(".dds")) {
-                if (ddsPreview == null) {
-                    ddsPreview = new DDSPreview((ProjectAssetManager) SceneApplication.getApplication().getAssetManager());
-                }
-                ddsPreview.requestPreview(selected, "", 80, 80, bottomPic, null);
-
-            } else {
-                Icon newicon = ImageUtilities.image2Icon(ImageToAwt.convert(tex.getImage(), false, true, 0));
-                bottomPic.setIcon(newicon);
-            }
+        if (editorBottom.getAsText() != null) {            
+            String selected = editorBottom.getAsText();
+            getTexturePreview().requestPreview(selected, "", 80, 80, bottomPic, null);
         }
     }//GEN-LAST:event_multipleTexBottomLoadButtonActionPerformed
 
     private void singleTexLoadButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_singleTexLoadButtonActionPerformed
         Component view = editorSingle.getCustomEditor();
         view.setVisible(true);
-        if (editorSingle.getValue() != null) {
-            Texture tex = (Texture) editorSingle.getValue();
-            String selected = tex.getKey().getName();
-
-            if (selected.toLowerCase().endsWith(".dds")) {
-                if (ddsPreview == null) {
-                    ddsPreview = new DDSPreview((ProjectAssetManager) SceneApplication.getApplication().getAssetManager());
-                }
-                ddsPreview.requestPreview(selected, "", 80, 80, singlePic, null);
-
-            } else {
-
-                Icon newicon = ImageUtilities.image2Icon(ImageToAwt.convert(tex.getImage(), false, true, 0));
-                singlePic.setIcon(newicon);
-            }
+        if (editorSingle.getAsText()!= null) {                        
+            String selected = editorSingle.getAsText();
+            getTexturePreview().requestPreview(selected, "", 80, 80, singlePic, null);
         }
     }//GEN-LAST:event_singleTexLoadButtonActionPerformed
+
+    private void mapTypeComboActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_mapTypeComboActionPerformed
+        // TODO add your handling code here:
+    }//GEN-LAST:event_mapTypeComboActionPerformed
+
     // Variables declaration - do not modify//GEN-BEGIN:variables
     private javax.swing.JLabel bottomPic;
     private javax.swing.JLabel eastPic;
     private javax.swing.JCheckBox flipYcheckBox;
     private javax.swing.JLabel jLabel1;
+    private javax.swing.JLabel jLabel10;
     private javax.swing.JLabel jLabel2;
     private javax.swing.JLabel jLabel3;
     private javax.swing.JLabel jLabel4;
@@ -578,6 +534,7 @@ public final class SkyboxVisualPanel2 extends JPanel {
     private javax.swing.JLabel jLabel7;
     private javax.swing.JLabel jLabel8;
     private javax.swing.JLabel jLabel9;
+    private javax.swing.JComboBox mapTypeCombo;
     private javax.swing.JButton multipleTexBottomLoadButton;
     private javax.swing.JButton multipleTexEastLoadButton;
     private javax.swing.JButton multipleTexNorthLoadButton;
@@ -596,7 +553,6 @@ public final class SkyboxVisualPanel2 extends JPanel {
     private javax.swing.JButton singleTexLoadButton;
     private javax.swing.JPanel singleTexturePanel;
     private javax.swing.JLabel southPic;
-    private javax.swing.JCheckBox spheremapCheckBox;
     private javax.swing.JLabel titleLabel;
     private javax.swing.JLabel topPic;
     private javax.swing.JLabel westPic;
@@ -626,10 +582,11 @@ public final class SkyboxVisualPanel2 extends JPanel {
         return normal2Z;
     }
 
-    public JCheckBox getSpheremapCheckBox() {
-        return spheremapCheckBox;
+    public SkyFactory.EnvMapType getEnvMapType(){
+        return (SkyFactory.EnvMapType)mapTypeCombo.getSelectedItem();
     }
     
+    
     public JCheckBox getFlipYCheckBox() {
         return flipYcheckBox;
     }

+ 2 - 1
sdk/jme3-terrain-editor/src/com/jme3/gde/terraineditor/sky/SkyboxWizardPanel2.java

@@ -123,6 +123,7 @@ public class SkyboxWizardPanel2 implements WizardDescriptor.Panel {
         }
     }
 
+    @Override
     public void storeSettings(Object settings) {
         WizardDescriptor wiz = (WizardDescriptor) settings;
         SkyboxVisualPanel2 comp = (SkyboxVisualPanel2) getComponent();
@@ -143,7 +144,7 @@ public class SkyboxWizardPanel2 implements WizardDescriptor.Panel {
             float y = new Float(comp.getNormal2Y().getText());
             float z = new Float(comp.getNormal2Z().getText());
             wiz.putProperty("normalScale", new Vector3f(x,y,z) );
-            wiz.putProperty("useSpheremap", comp.getSpheremapCheckBox().isSelected());
+            wiz.putProperty("envMapType", comp.getEnvMapType());         
             wiz.putProperty("flipY", comp.getFlipYCheckBox().isSelected());
         }
     }